From 8e62984a28768c80c0a2fce08f522d632653ecbe Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 18 Jan 2024 17:22:47 -0600 Subject: [PATCH 001/272] add apple docs --- docs/building.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index e7128df2e..915f9c7d4 100644 --- a/docs/building.md +++ b/docs/building.md @@ -9,7 +9,7 @@ Here you will find instructions on how to install the necessary tools for buildi - 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. ### 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: @@ -44,6 +44,7 @@ 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.72.0 rustup default 1.67.1 ``` @@ -129,6 +130,44 @@ flutter pub get flutter run linux ``` +## Mac host + +### 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. + +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 cocoapods git cmake autoconf fontconfig libpng lz4 pkg-config automake freetype libssh2 lzo procs berkeley-db gdbm libtool m4 rtmpdump brotli gettext libunistring make rustup-init ca-certificates git-gui libx11 openldap tcl-tk cairo glib libxau openssl@1.1 unbound cbindgen gmp libxcb openssl@3 unzip cmake libevent libxdmcp pcre2 xorgproto coreutils libidn2 libxext perl xz curl libnghttp2 libxrender pixman zstd +``` + +Download and install [Rust](https://www.rust-lang.org/tools/install). [Rustup](https://rustup.rs/) is recommended for Rust setup. Use `rustc` to confirm successful installation. Install toolchains 1.67.1 and 1.72.0 and `cbindgen` and `cargo-lipo` too. You will also have to add the platform targets `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 +``` + +Download and install [Flutter](https://docs.flutter.dev/get-started/install). Versions 3.16.8 and 3.10.6 should both work. Use `flutter doctor` to confirm successful installation. + +Download [Android Studio](https://developer.android.com/studio). VS Code may work as an alternative, but this is not recommended. + +### Building libraries + +### Install Flutter on Mac host +Install Flutter 3.10.3 on your Mac host by following these instructions: https://docs.flutter.dev/get-started/install/macos. Run `flutter doctor` in PowerShell to confirm its installation. + + ## 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. From cd425d50c417d8e8683dd6bc9c6cbdd71d4a4756 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 18 Jan 2024 17:48:53 -0600 Subject: [PATCH 002/272] add xcode commandline tools note --- docs/building.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index 915f9c7d4..c7fc198c4 100644 --- a/docs/building.md +++ b/docs/building.md @@ -135,7 +135,7 @@ flutter run linux ### 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. +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: ``` From 00bc8d02f9a5d9bd408982e83d3c4b32b54564e7 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 18 Jan 2024 17:52:52 -0600 Subject: [PATCH 003/272] small final docs corrections for the day --- docs/building.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/building.md b/docs/building.md index c7fc198c4..977f08161 100644 --- a/docs/building.md +++ b/docs/building.md @@ -147,7 +147,7 @@ After installing Homebrew, install the following packages: brew install cocoapods git cmake autoconf fontconfig libpng lz4 pkg-config automake freetype libssh2 lzo procs berkeley-db gdbm libtool m4 rtmpdump brotli gettext libunistring make rustup-init ca-certificates git-gui libx11 openldap tcl-tk cairo glib libxau openssl@1.1 unbound cbindgen gmp libxcb openssl@3 unzip cmake libevent libxdmcp pcre2 xorgproto coreutils libidn2 libxext perl xz curl libnghttp2 libxrender pixman zstd ``` -Download and install [Rust](https://www.rust-lang.org/tools/install). [Rustup](https://rustup.rs/) is recommended for Rust setup. Use `rustc` to confirm successful installation. Install toolchains 1.67.1 and 1.72.0 and `cbindgen` and `cargo-lipo` too. You will also have to add the platform targets `aarch64-apple-ios` and/or `aarch64-apple-darwin`. You can use the command(s): +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 @@ -163,10 +163,10 @@ Download and install [Flutter](https://docs.flutter.dev/get-started/install). V Download [Android Studio](https://developer.android.com/studio). VS Code may work as an alternative, but this is not recommended. ### Building libraries +TODO ### Install Flutter on Mac host -Install Flutter 3.10.3 on your Mac host by following these instructions: https://docs.flutter.dev/get-started/install/macos. Run `flutter doctor` in PowerShell to confirm its installation. - +Install Flutter 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. ## Windows host ### Visual Studio From dd0fc6f369139c1e368f3ad519fec7e76af5b8c1 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 18 Jan 2024 13:33:50 -0600 Subject: [PATCH 004/272] refactor unnecessary provider watch --- .../transaction_views/tx_v2/transaction_v2_list.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart index f47417d99..ac868aee9 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart @@ -23,6 +23,7 @@ import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -44,6 +45,7 @@ class _TransactionsV2ListState extends ConsumerState { late final StreamSubscription> _subscription; late final Query _query; + late final Coin coin; BorderRadius get _borderRadiusFirst { return BorderRadius.only( @@ -69,6 +71,7 @@ class _TransactionsV2ListState extends ConsumerState { @override void initState() { + coin = ref.read(pWallets).getWallet(widget.walletId).info.coin; _query = ref .read(mainDBProvider) .isar @@ -110,8 +113,6 @@ class _TransactionsV2ListState extends ConsumerState { @override Widget build(BuildContext context) { - final coin = ref.watch(pWallets).getWallet(widget.walletId).info.coin; - return FutureBuilder( future: _query.findAll(), builder: (fbContext, AsyncSnapshot> snapshot) { From fbbd175d0f21b5c94a74ebb4725f0614de10559d Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 18 Jan 2024 13:34:11 -0600 Subject: [PATCH 005/272] change wording on successful restore --- .../sub_widgets/restore_succeeded_dialog.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart index 3963fc139..0b816cbe9 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart @@ -51,7 +51,7 @@ class RestoreSucceededDialog extends StatelessWidget { height: 16, ), Text( - "You can use your wallet now.", + "You may access your wallet now.", style: STextStyles.desktopTextMedium(context).copyWith( color: Theme.of(context).extension()!.textDark3, ), @@ -80,7 +80,7 @@ class RestoreSucceededDialog extends StatelessWidget { } else { return StackDialog( title: "Wallet restored", - message: "You can use your wallet now.", + message: "You may access your wallet now.", icon: SvgPicture.asset( Assets.svg.checkCircle, width: 24, From 755cc049b095bd73e4543c17fa7c693065e5caba Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 18 Jan 2024 14:02:41 -0600 Subject: [PATCH 006/272] add frostdart dependency --- .gitmodules | 3 +++ crypto_plugins/frostdart | 1 + linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 7 +++++++ pubspec.yaml | 3 +++ scripts/android/build_all.sh | 6 ++---- scripts/ios/build_all.sh | 5 ++--- scripts/linux/build_all.sh | 6 ++---- scripts/macos/build_all.sh | 2 ++ scripts/windows/build_all.sh | 5 ++--- windows/flutter/generated_plugins.cmake | 1 + 11 files changed, 26 insertions(+), 14 deletions(-) create mode 160000 crypto_plugins/frostdart diff --git a/.gitmodules b/.gitmodules index 7474c8a54..98bb17794 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "crypto_plugins/flutter_liblelantus"] path = crypto_plugins/flutter_liblelantus url = https://github.com/cypherstack/flutter_liblelantus.git +[submodule "crypto_plugins/frostdart"] + path = crypto_plugins/frostdart + url = https://www.github.com/cypherstack/frostdart diff --git a/crypto_plugins/frostdart b/crypto_plugins/frostdart new file mode 160000 index 000000000..2fa7e4666 --- /dev/null +++ b/crypto_plugins/frostdart @@ -0,0 +1 @@ +Subproject commit 2fa7e46669a023d270cad4552b5151b138738790 diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index bb9965d23..e1af526f4 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -17,6 +17,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST coinlib_flutter flutter_libsparkmobile + frostdart tor_ffi_plugin ) diff --git a/pubspec.lock b/pubspec.lock index 840efc472..fdb040a28 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -816,6 +816,13 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + frostdart: + dependency: "direct main" + description: + path: "crypto_plugins/frostdart" + relative: true + source: path + version: "0.0.1" fuchsia_remote_debug_protocol: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index abeb7a821..9a243905b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,9 @@ dependencies: lelantus: path: ./crypto_plugins/flutter_liblelantus + frostdart: + path: ./crypto_plugins/frostdart + flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index b67cd92a4..484fb7d03 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -13,10 +13,8 @@ mkdir -p build (cd ../../crypto_plugins/flutter_liblelantus/scripts/android && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/android && ./install_ndk.sh && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libmonero/scripts/android/ && ./build_all.sh ) & +set_rust_to_1720 & +(cd ../../crypto_plugins/frostdart/scripts/android && ./build_all.sh ) & wait echo "Done building" - -# set rust (back) to a more recent stable release to allow stack wallet to build tor -set_rust_to_1720 - diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index dd6ad38ff..db806c3bb 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -17,13 +17,12 @@ rustup target add x86_64-apple-ios (cd ../../crypto_plugins/flutter_liblelantus/scripts/ios && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh ) & +set_rust_to_1720 & +(cd ../../crypto_plugins/frostdart/scripts/ios && ./build_all.sh ) & wait echo "Done building" -# set rust (back) to a more recent stable release to allow stack wallet to build tor -set_rust_to_1720 - # ensure ios rust triples are there rustup target add aarch64-apple-ios rustup target add x86_64-apple-ios diff --git a/scripts/linux/build_all.sh b/scripts/linux/build_all.sh index 672668c13..2b6bd1ffd 100755 --- a/scripts/linux/build_all.sh +++ b/scripts/linux/build_all.sh @@ -15,10 +15,8 @@ mkdir -p build (cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_monero_all.sh && ./build_sharedfile.sh ) & +set_rust_to_1720 & +(cd ../../crypto_plugins/frostdart/scripts/linux && ./build_all.sh ) & wait echo "Done building" - -# set rust (back) to a more recent stable release to allow stack wallet to build tor -set_rust_to_1720 - diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh index 0e086fc71..53d6f9bac 100755 --- a/scripts/macos/build_all.sh +++ b/scripts/macos/build_all.sh @@ -9,6 +9,8 @@ set_rust_to_1671 (cd ../../crypto_plugins/flutter_liblelantus/scripts/macos && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh ) & +set_rust_to_1720 & +(cd ../../crypto_plugins/frostdart/scripts/macos && ./build_all.sh ) & wait echo "Done building" diff --git a/scripts/windows/build_all.sh b/scripts/windows/build_all.sh index ee3c1b558..1a585e276 100755 --- a/scripts/windows/build_all.sh +++ b/scripts/windows/build_all.sh @@ -10,9 +10,8 @@ mkdir -p build (cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_liblelantus/scripts/windows && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libmonero/scripts/windows && ./build_all.sh) & +set_rust_to_1720 & +(cd ../../crypto_plugins/frostdart/scripts/windows && ./build_all.sh ) & wait echo "Done building" - -# set rust (back) to a more recent stable release to allow stack wallet to build tor -set_rust_to_1720 diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a774c684a..02d70698f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -18,6 +18,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST coinlib_flutter flutter_libsparkmobile + frostdart tor_ffi_plugin ) From 85b66fd8493d963c4ee8b9b6514ae5b362df650b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 18 Jan 2024 17:47:06 -0600 Subject: [PATCH 007/272] WIP bitcoin frost wallet addition --- .../isar/models/blockchain_data/address.dart | 3 + .../models/blockchain_data/address.g.dart | 2 + .../transaction_fee_selection_sheet.dart | 5 +- .../add_edit_node_view.dart | 4 + .../manage_nodes_views/node_details_view.dart | 2 + .../wallet_view/sub_widgets/desktop_send.dart | 4 +- lib/services/notifications_service.dart | 3 +- lib/themes/color_theme.dart | 2 + lib/themes/stack_colors.dart | 2 + lib/utilities/amount/amount_unit.dart | 2 + lib/utilities/block_explorers.dart | 2 + lib/utilities/constants.dart | 14 + lib/utilities/default_nodes.dart | 2 + lib/utilities/enums/coin_enum.dart | 54 +- .../enums/derive_path_type_enum.dart | 2 + .../crypto_currency/coins/bitcoin.dart | 24 +- .../crypto_currency/coins/bitcoin_frost.dart | 65 ++ .../intermediate/private_key_currency.dart | 9 + .../isar/models/frost_wallet_info.dart | 38 + .../isar/models/frost_wallet_info.g.dart | 818 ++++++++++++++++++ lib/wallets/isar/models/wallet_info.g.dart | 2 + .../wallet/impl/bitcoin_frost_wallet.dart | 475 ++++++++++ lib/wallets/wallet/wallet.dart | 6 + .../electrumx_interface.dart | 9 +- lib/widgets/node_card.dart | 2 + lib/widgets/node_options_sheet.dart | 2 + 26 files changed, 1492 insertions(+), 61 deletions(-) create mode 100644 lib/wallets/crypto_currency/coins/bitcoin_frost.dart create mode 100644 lib/wallets/crypto_currency/intermediate/private_key_currency.dart create mode 100644 lib/wallets/isar/models/frost_wallet_info.dart create mode 100644 lib/wallets/isar/models/frost_wallet_info.g.dart create mode 100644 lib/wallets/wallet/impl/bitcoin_frost_wallet.dart diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index e3368a119..8adaa4ce5 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -163,6 +163,7 @@ enum AddressType { spark, stellar, tezos, + frostMS, ; String get readableName { @@ -193,6 +194,8 @@ enum AddressType { return "Stellar"; case AddressType.tezos: return "Tezos"; + case AddressType.frostMS: + return "FrostMS"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 796c29f29..7d3aff776 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -266,6 +266,7 @@ const _AddresstypeEnumValueMap = { 'spark': 10, 'stellar': 11, 'tezos': 12, + 'frostMS': 13, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -281,6 +282,7 @@ const _AddresstypeValueEnumMap = { 10: AddressType.spark, 11: AddressType.stellar, 12: AddressType.tezos, + 13: AddressType.frostMS, }; Id _addressGetId(Address object) { diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index f2178a450..8572d5037 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'package:stackwallet/widgets/animated_text.dart'; final feeSheetSessionCacheProvider = @@ -697,7 +698,7 @@ class _TransactionFeeSelectionSheetState const SizedBox( height: 24, ), - if (coin.isElectrumXCoin) + if (wallet is ElectrumXInterface) GestureDetector( onTap: () { final state = @@ -766,7 +767,7 @@ class _TransactionFeeSelectionSheetState ), ), ), - if (coin.isElectrumXCoin) + if (wallet is ElectrumXInterface) const SizedBox( height: 24, ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index e03c3ab21..7dc743aab 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -166,6 +166,8 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: @@ -757,6 +759,8 @@ class _NodeFormState extends ConsumerState { case Coin.eCash: case Coin.stellar: case Coin.stellarTestnet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: return false; case Coin.ethereum: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 6bf0092e8..3605a2815 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -148,6 +148,8 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.eCash: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: final client = ElectrumXClient( host: node!.host, port: node.port, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 90c5ae041..160de0367 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -52,6 +52,7 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/animated_text.dart'; @@ -1566,7 +1567,8 @@ class _DesktopSendState extends ConsumerState { if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos] .contains(coin))) ConditionalParent( - condition: coin.isElectrumXCoin && + condition: ref.watch(pWallets).getWallet(walletId) + is ElectrumXInterface && !(((coin == Coin.firo || coin == Coin.firoTestNet) && (ref.watch(publicPrivateBalanceStateProvider.state).state == FiroType.lelantus || diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 1512c12c6..c019768fc 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -24,6 +24,7 @@ import 'package:stackwallet/services/wallets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'exchange/exchange.dart'; @@ -123,7 +124,7 @@ class NotificationsService extends ChangeNotifier { final node = nodeService.getPrimaryNodeFor(coin: coin); if (node != null) { - if (coin.isElectrumXCoin) { + if (wallet is ElectrumXInterface) { final eNode = ElectrumXNode( address: node.host, port: node.port, diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index abec28d4e..38de6c636 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -37,6 +37,8 @@ class CoinThemeColorDefault { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: return bitcoin; case Coin.litecoin: case Coin.litecoinTestNet: diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index cbec0077a..0cc83b04f 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1680,6 +1680,8 @@ class StackColors extends ThemeExtension { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: return _coin.bitcoin; case Coin.litecoin: case Coin.litecoinTestNet: diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index 6a646fd11..87efcc4cd 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -40,6 +40,8 @@ enum AmountUnit { case Coin.litecoin: case Coin.particl: case Coin.namecoin: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index bb4ac06fb..9f3e92c5d 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -18,6 +18,7 @@ Uri getDefaultBlockExplorerUrlFor({ required String txid, }) { switch (coin) { + case Coin.bitcoinFrost: case Coin.bitcoin: return Uri.parse("https://mempool.space/tx/$txid"); case Coin.litecoin: @@ -25,6 +26,7 @@ Uri getDefaultBlockExplorerUrlFor({ case Coin.litecoinTestNet: return Uri.parse("https://chain.so/tx/LTCTEST/$txid"); case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: return Uri.parse("https://mempool.space/testnet/tx/$txid"); case Coin.dogecoin: return Uri.parse("https://chain.so/tx/DOGE/$txid"); diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index db0543044..f7a6faeb2 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -69,6 +69,7 @@ abstract class Constants { static BigInt satsPerCoin(Coin coin) { switch (coin) { case Coin.bitcoin: + case Coin.bitcoinFrost: case Coin.litecoin: case Coin.litecoinTestNet: case Coin.bitcoincash: @@ -76,6 +77,7 @@ abstract class Constants { case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: @@ -113,6 +115,7 @@ abstract class Constants { static int decimalPlacesForCoin(Coin coin) { switch (coin) { case Coin.bitcoin: + case Coin.bitcoinFrost: case Coin.litecoin: case Coin.litecoinTestNet: case Coin.bitcoincash: @@ -120,6 +123,7 @@ abstract class Constants { case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: @@ -189,6 +193,10 @@ abstract class Constants { case Coin.wownero: values.addAll([14, 25]); break; + + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + throw ArgumentError("Frost mnemonic lengths unsupported"); } return values; } @@ -198,6 +206,8 @@ abstract class Constants { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.bitcoincash: case Coin.bitcoincashTestnet: case Coin.eCash: @@ -277,6 +287,10 @@ abstract class Constants { case Coin.monero: return 25; + + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + throw ArgumentError("Frost mnemonic length unsupported"); // // default: // -1; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 5d80784a3..b8f296b68 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -312,6 +312,7 @@ abstract class DefaultNodes { static NodeModel getNodeFor(Coin coin) { switch (coin) { case Coin.bitcoin: + case Coin.bitcoinFrost: return bitcoin; case Coin.litecoin: @@ -360,6 +361,7 @@ abstract class DefaultNodes { return tezos; case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: return bitcoinTestnet; case Coin.litecoinTestNet: diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index c71d39ba4..305183448 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/utilities/constants.dart'; enum Coin { bitcoin, + bitcoinFrost, monero, banano, bitcoincash, @@ -35,6 +36,7 @@ enum Coin { /// bitcoinTestNet, + bitcoinFrostTestNet, bitcoincashTestnet, dogecoinTestNet, firoTestNet, @@ -47,6 +49,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "Bitcoin"; + case Coin.bitcoinFrost: + return "Bitcoin Frost"; case Coin.litecoin: return "Litecoin"; case Coin.bitcoincash: @@ -79,6 +83,8 @@ extension CoinExt on Coin { return "Banano"; case Coin.bitcoinTestNet: return "tBitcoin"; + case Coin.bitcoinFrostTestNet: + return "tBitcoin Frost"; case Coin.litecoinTestNet: return "tLitecoin"; case Coin.bitcoincashTestnet: @@ -95,6 +101,7 @@ extension CoinExt on Coin { String get ticker { switch (this) { case Coin.bitcoin: + case Coin.bitcoinFrost: return "BTC"; case Coin.litecoin: return "LTC"; @@ -127,6 +134,7 @@ extension CoinExt on Coin { case Coin.banano: return "BAN"; case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: return "tBTC"; case Coin.litecoinTestNet: return "tLTC"; @@ -144,6 +152,7 @@ extension CoinExt on Coin { String get uriScheme { switch (this) { case Coin.bitcoin: + case Coin.bitcoinFrost: return "bitcoin"; case Coin.litecoin: return "litecoin"; @@ -177,6 +186,7 @@ extension CoinExt on Coin { case Coin.banano: return "ban"; case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: return "bitcoin"; case Coin.litecoinTestNet: return "litecoin"; @@ -191,36 +201,6 @@ extension CoinExt on Coin { } } - bool get isElectrumXCoin { - switch (this) { - case Coin.bitcoin: - case Coin.litecoin: - case Coin.bitcoincash: - case Coin.dogecoin: - case Coin.firo: - case Coin.namecoin: - case Coin.particl: - case Coin.bitcoinTestNet: - case Coin.litecoinTestNet: - case Coin.bitcoincashTestnet: - case Coin.firoTestNet: - case Coin.dogecoinTestNet: - case Coin.eCash: - return true; - - case Coin.epicCash: - case Coin.ethereum: - case Coin.monero: - case Coin.tezos: - case Coin.wownero: - case Coin.nano: - case Coin.banano: - case Coin.stellar: - case Coin.stellarTestnet: - return false; - } - } - bool get hasMnemonicPassphraseSupport { switch (this) { case Coin.bitcoin: @@ -241,6 +221,8 @@ extension CoinExt on Coin { case Coin.stellarTestnet: return true; + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.epicCash: case Coin.monero: case Coin.wownero: @@ -260,6 +242,8 @@ extension CoinExt on Coin { case Coin.ethereum: return true; + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.firo: case Coin.namecoin: case Coin.particl: @@ -284,6 +268,7 @@ extension CoinExt on Coin { bool get isTestNet { switch (this) { case Coin.bitcoin: + case Coin.bitcoinFrost: case Coin.litecoin: case Coin.bitcoincash: case Coin.dogecoin: @@ -303,6 +288,7 @@ extension CoinExt on Coin { case Coin.dogecoinTestNet: case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: @@ -314,6 +300,7 @@ extension CoinExt on Coin { Coin get mainNetVersion { switch (this) { case Coin.bitcoin: + case Coin.bitcoinFrost: case Coin.litecoin: case Coin.bitcoincash: case Coin.dogecoin: @@ -337,6 +324,9 @@ extension CoinExt on Coin { case Coin.bitcoinTestNet: return Coin.bitcoin; + case Coin.bitcoinFrostTestNet: + return Coin.bitcoinFrost; + case Coin.litecoinTestNet: return Coin.litecoin; @@ -364,6 +354,10 @@ extension CoinExt on Coin { case Coin.particl: return AddressType.p2wpkh; + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + return AddressType.frostMS; + case Coin.eCash: case Coin.bitcoincash: case Coin.bitcoincashTestnet: diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 5b94f41f6..6d2371735 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -44,6 +44,8 @@ extension DerivePathTypeExt on DerivePathType { case Coin.ethereum: // TODO: do we need something here? return DerivePathType.eth; + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.epicCash: case Coin.monero: case Coin.wownero: diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index 2402a977f..d441961a7 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -170,30 +170,10 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { NodeModel get defaultNode { switch (network) { case CryptoCurrencyNetwork.main: - return NodeModel( - host: "bitcoin.stackwallet.com", - port: 50002, - name: DefaultNodes.defaultName, - id: DefaultNodes.buildId(Coin.bitcoin), - useSSL: true, - enabled: true, - coinName: Coin.bitcoin.name, - isFailover: true, - isDown: false, - ); + return DefaultNodes.bitcoin; case CryptoCurrencyNetwork.test: - return NodeModel( - host: "bitcoin-testnet.stackwallet.com", - port: 51002, - name: DefaultNodes.defaultName, - id: DefaultNodes.buildId(Coin.bitcoinTestNet), - useSSL: true, - enabled: true, - coinName: Coin.bitcoinTestNet.name, - isFailover: true, - isDown: false, - ); + return DefaultNodes.bitcoinTestnet; default: throw UnimplementedError(); diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart new file mode 100644 index 000000000..f968818e1 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -0,0 +1,65 @@ +import 'dart:typed_data'; + +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart'; + +class BitcoinFrost extends FrostCurrency { + BitcoinFrost(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.bitcoin; + case CryptoCurrencyNetwork.test: + coin = Coin.bitcoinTestNet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return DefaultNodes.bitcoin; + + case CryptoCurrencyNetwork.test: + return DefaultNodes.bitcoinTestnet; + + default: + throw UnimplementedError(); + } + } + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; + case CryptoCurrencyNetwork.test: + return "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + String pubKeyToScriptHash({required Uint8List pubKey}) { + try { + return Bip39HDCurrency.convertBytesToScriptHash(pubKey); + } catch (e) { + rethrow; + } + } + + @override + bool validateAddress(String address) { + // TODO: implement validateAddress for frost addresses + return true; + } +} diff --git a/lib/wallets/crypto_currency/intermediate/private_key_currency.dart b/lib/wallets/crypto_currency/intermediate/private_key_currency.dart new file mode 100644 index 000000000..8cbf11b27 --- /dev/null +++ b/lib/wallets/crypto_currency/intermediate/private_key_currency.dart @@ -0,0 +1,9 @@ +import 'dart:typed_data'; + +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +abstract class FrostCurrency extends CryptoCurrency { + FrostCurrency(super.network); + + String pubKeyToScriptHash({required Uint8List pubKey}); +} diff --git a/lib/wallets/isar/models/frost_wallet_info.dart b/lib/wallets/isar/models/frost_wallet_info.dart new file mode 100644 index 000000000..817f78f39 --- /dev/null +++ b/lib/wallets/isar/models/frost_wallet_info.dart @@ -0,0 +1,38 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/wallets/isar/isar_id_interface.dart'; + +part 'frost_wallet_info.g.dart'; + +@Collection(accessor: "frostWalletInfo", inheritance: false) +class FrostWalletInfo implements IsarId { + @override + Id id = Isar.autoIncrement; + + @Index(unique: true, replace: false) + final String walletId; + + final List knownSalts; + + FrostWalletInfo({ + required this.walletId, + required this.knownSalts, + }); + + FrostWalletInfo copyWith({ + List? knownSalts, + }) { + return FrostWalletInfo( + walletId: walletId, + knownSalts: knownSalts ?? this.knownSalts, + ); + } + + Future updateKnownSalts( + List knownSalts, { + required Isar isar, + }) async { + // await isar.writeTxn(() async { + // await isar. + // }) + } +} diff --git a/lib/wallets/isar/models/frost_wallet_info.g.dart b/lib/wallets/isar/models/frost_wallet_info.g.dart new file mode 100644 index 000000000..ce5ae2aae --- /dev/null +++ b/lib/wallets/isar/models/frost_wallet_info.g.dart @@ -0,0 +1,818 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'frost_wallet_info.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters + +extension GetFrostWalletInfoCollection on Isar { + IsarCollection get frostWalletInfo => this.collection(); +} + +const FrostWalletInfoSchema = CollectionSchema( + name: r'FrostWalletInfo', + id: -4182879703273806681, + properties: { + r'knownSalts': PropertySchema( + id: 0, + name: r'knownSalts', + type: IsarType.stringList, + ), + r'walletId': PropertySchema( + id: 1, + name: r'walletId', + type: IsarType.string, + ) + }, + estimateSize: _frostWalletInfoEstimateSize, + serialize: _frostWalletInfoSerialize, + deserialize: _frostWalletInfoDeserialize, + deserializeProp: _frostWalletInfoDeserializeProp, + idName: r'id', + indexes: { + r'walletId': IndexSchema( + id: -1783113319798776304, + name: r'walletId', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _frostWalletInfoGetId, + getLinks: _frostWalletInfoGetLinks, + attach: _frostWalletInfoAttach, + version: '3.0.5', +); + +int _frostWalletInfoEstimateSize( + FrostWalletInfo object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.knownSalts.length * 3; + { + for (var i = 0; i < object.knownSalts.length; i++) { + final value = object.knownSalts[i]; + bytesCount += value.length * 3; + } + } + bytesCount += 3 + object.walletId.length * 3; + return bytesCount; +} + +void _frostWalletInfoSerialize( + FrostWalletInfo object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeStringList(offsets[0], object.knownSalts); + writer.writeString(offsets[1], object.walletId); +} + +FrostWalletInfo _frostWalletInfoDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = FrostWalletInfo( + knownSalts: reader.readStringList(offsets[0]) ?? [], + walletId: reader.readString(offsets[1]), + ); + object.id = id; + return object; +} + +P _frostWalletInfoDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringList(offset) ?? []) as P; + case 1: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _frostWalletInfoGetId(FrostWalletInfo object) { + return object.id; +} + +List> _frostWalletInfoGetLinks(FrostWalletInfo object) { + return []; +} + +void _frostWalletInfoAttach( + IsarCollection col, Id id, FrostWalletInfo object) { + object.id = id; +} + +extension FrostWalletInfoByIndex on IsarCollection { + Future getByWalletId(String walletId) { + return getByIndex(r'walletId', [walletId]); + } + + FrostWalletInfo? getByWalletIdSync(String walletId) { + return getByIndexSync(r'walletId', [walletId]); + } + + Future deleteByWalletId(String walletId) { + return deleteByIndex(r'walletId', [walletId]); + } + + bool deleteByWalletIdSync(String walletId) { + return deleteByIndexSync(r'walletId', [walletId]); + } + + Future> getAllByWalletId(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return getAllByIndex(r'walletId', values); + } + + List getAllByWalletIdSync(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'walletId', values); + } + + Future deleteAllByWalletId(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'walletId', values); + } + + int deleteAllByWalletIdSync(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'walletId', values); + } + + Future putByWalletId(FrostWalletInfo object) { + return putByIndex(r'walletId', object); + } + + Id putByWalletIdSync(FrostWalletInfo object, {bool saveLinks = true}) { + return putByIndexSync(r'walletId', object, saveLinks: saveLinks); + } + + Future> putAllByWalletId(List objects) { + return putAllByIndex(r'walletId', objects); + } + + List putAllByWalletIdSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'walletId', objects, saveLinks: saveLinks); + } +} + +extension FrostWalletInfoQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension FrostWalletInfoQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + walletIdEqualTo(String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'walletId', + value: [walletId], + )); + }); + } + + QueryBuilder + walletIdNotEqualTo(String walletId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [], + upper: [walletId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [walletId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [walletId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [], + upper: [walletId], + includeUpper: false, + )); + } + }); + } +} + +extension FrostWalletInfoQueryFilter + on QueryBuilder { + QueryBuilder + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + knownSaltsElementEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'knownSalts', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'knownSalts', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'knownSalts', + value: '', + )); + }); + } + + QueryBuilder + knownSaltsElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'knownSalts', + value: '', + )); + }); + } + + QueryBuilder + knownSaltsLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + knownSaltsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + knownSaltsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + knownSaltsLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + knownSaltsLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + knownSaltsLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder + walletIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'walletId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'walletId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: '', + )); + }); + } + + QueryBuilder + walletIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'walletId', + value: '', + )); + }); + } +} + +extension FrostWalletInfoQueryObject + on QueryBuilder {} + +extension FrostWalletInfoQueryLinks + on QueryBuilder {} + +extension FrostWalletInfoQuerySortBy + on QueryBuilder { + QueryBuilder + sortByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder + sortByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension FrostWalletInfoQuerySortThenBy + on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder + thenByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension FrostWalletInfoQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByKnownSalts() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'knownSalts'); + }); + } + + QueryBuilder distinctByWalletId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); + }); + } +} + +extension FrostWalletInfoQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder, QQueryOperations> + knownSaltsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'knownSalts'); + }); + } + + QueryBuilder walletIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'walletId'); + }); + } +} diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index db50a581a..0b809ddcb 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -265,6 +265,7 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'spark': 10, 'stellar': 11, 'tezos': 12, + 'frostMS': 13, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -280,6 +281,7 @@ const _WalletInfomainAddressTypeValueEnumMap = { 10: AddressType.spark, 11: AddressType.stellar, 12: AddressType.tezos, + 13: AddressType.frostMS, }; Id _walletInfoGetId(WalletInfo object) { diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart new file mode 100644 index 000000000..203ea5877 --- /dev/null +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -0,0 +1,475 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:frostdart/frostdart.dart' as frost; +import 'package:frostdart/frostdart_bindings_generated.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart'; + +class BitcoinFrostWallet extends Wallet + with PrivateKeyInterface { + FrostWalletInfo get frostInfo => throw UnimplementedError(); + + late ElectrumXClient electrumXClient; + late CachedElectrumXClient electrumXCachedClient; + + @override + int get isarTransactionVersion => 2; + + BitcoinFrostWallet(CryptoCurrencyNetwork network) + : super(BitcoinFrost(network) as T); + + @override + FilterOperation? get changeAddressFilterOperation => FilterGroup.and( + [ + FilterCondition.equalTo( + property: r"type", + value: info.mainAddressType, + ), + const FilterCondition.equalTo( + property: r"subType", + value: AddressSubType.change, + ), + ], + ); + + @override + FilterOperation? get receivingAddressFilterOperation => FilterGroup.and( + [ + FilterCondition.equalTo( + property: r"type", + value: info.mainAddressType, + ), + const FilterCondition.equalTo( + property: r"subType", + value: AddressSubType.receiving, + ), + ], + ); + + // Future> fetchAddressesForElectrumXScan() async { + // final allAddresses = await mainDB + // .getAddresses(walletId) + // .filter() + // .typeEqualTo(AddressType.frostMS) + // .and() + // .group( + // (q) => q + // .subTypeEqualTo(AddressSubType.receiving) + // .or() + // .subTypeEqualTo(AddressSubType.change), + // ) + // .findAll(); + // return allAddresses; + // } + + @override + Future updateTransactions() { + // TODO: implement updateTransactions + throw UnimplementedError(); + } + + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } + + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + Future checkSaveInitialReceivingAddress() { + // TODO: implement checkSaveInitialReceivingAddress + throw UnimplementedError(); + } + + @override + Future confirmSend({required TxData txData}) { + // TODO: implement confirmSend + throw UnimplementedError(); + } + + @override + Future estimateFeeFor(Amount amount, int feeRate) { + // TODO: implement estimateFeeFor + throw UnimplementedError(); + } + + @override + // TODO: implement fees + Future get fees => throw UnimplementedError(); + + @override + Future prepareSend({required TxData txData}) { + // TODO: implement prepareSendpu + throw UnimplementedError(); + } + + @override + Future recover({ + required bool isRescan, + String? serializedKeys, + String? multisigConfig, + }) async { + if (serializedKeys == null || multisigConfig == null) { + throw Exception( + "Failed to recover $runtimeType: " + "Missing serializedKeys and/or multisigConfig.", + ); + } + + try { + await refreshMutex.protect(() async { + if (!isRescan) { + final salt = frost + .multisigSalt( + multisigConfig: multisigConfig, + ) + .toHex; + final knownSalts = frostInfo.knownSalts; + if (knownSalts.contains(salt)) { + throw Exception("Known frost multisig salt found!"); + } + knownSalts.add(salt); + await frostInfo.updateKnownSalts(knownSalts, isar: mainDB.isar); + } + + final keys = frost.deserializeKeys(keys: serializedKeys); + await _saveSerializedKeys(serializedKeys); + await _saveMultisigConfig(multisigConfig); + + final addressString = frost.addressForKeys( + network: cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet, + keys: keys, + ); + + final publicKey = frost.scriptPubKeyForKeys(keys: keys); + + final address = Address( + walletId: walletId, + value: addressString, + publicKey: publicKey.toUint8ListFromHex, + derivationIndex: 0, + derivationPath: null, + subType: AddressSubType.receiving, + type: AddressType.frostMS, + ); + + await mainDB.updateOrPutAddresses([address]); + }); + + unawaited(refresh()); + } catch (e, s) { + Logging.instance.log( + "recoverFromSerializedKeys failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + @override + Future updateBalance() async { + final utxos = await mainDB.getUTXOs(walletId).findAll(); + + final currentChainHeight = await chainHeight; + + Amount satoshiBalanceTotal = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalancePending = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalanceSpendable = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalanceBlocked = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + for (final utxo in utxos) { + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + satoshiBalanceTotal += utxoAmount; + + if (utxo.isBlocked) { + satoshiBalanceBlocked += utxoAmount; + } else { + if (utxo.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + )) { + satoshiBalanceSpendable += utxoAmount; + } else { + satoshiBalancePending += utxoAmount; + } + } + } + + final balance = Balance( + total: satoshiBalanceTotal, + spendable: satoshiBalanceSpendable, + blockedTotal: satoshiBalanceBlocked, + pendingSpendable: satoshiBalancePending, + ); + + await info.updateBalance(newBalance: balance, isar: mainDB.isar); + } + + @override + Future updateChainHeight() async { + final int height; + try { + final result = await electrumXClient.getBlockHeadTip(); + height = result["height"] as int; + } catch (e) { + rethrow; + } + + await info.updateCachedChainHeight( + newHeight: height, + isar: mainDB.isar, + ); + } + + @override + Future pingCheck() async { + try { + final result = await electrumXClient.ping(); + return result; + } catch (_) { + return false; + } + } + + @override + Future updateNode() async { + await _updateElectrumX(); + } + + @override + Future updateUTXOs() async { + final address = await getCurrentReceivingAddress(); + + try { + final scriptHash = cryptoCurrency.pubKeyToScriptHash( + pubKey: Uint8List.fromList(address!.publicKey), + ); + + final utxos = await electrumXClient.getUTXOs(scripthash: scriptHash); + + final List outputArray = []; + + for (int i = 0; i < utxos.length; i++) { + final utxo = await _parseUTXO( + jsonUTXO: utxos[i], + ); + + outputArray.add(utxo); + } + + return await mainDB.updateUTXOs(walletId, outputArray); + } catch (e, s) { + Logging.instance.log( + "Output fetch unsuccessful: $e\n$s", + level: LogLevel.Error, + ); + return false; + } + } + + // =================== Secure storage ======================================== + + Future get getSerializedKeys async => + await secureStorageInterface.read( + key: "{$walletId}_serializedFROSTKeys", + ); + Future _saveSerializedKeys(String keys) async { + final current = await getSerializedKeys; + + if (current == null) { + // do nothing + } else if (current == keys) { + // should never occur + } else { + // save current as prev gen before updating current + await secureStorageInterface.write( + key: "{$walletId}_serializedFROSTKeysPrevGen", + value: current, + ); + } + + await secureStorageInterface.write( + key: "{$walletId}_serializedFROSTKeys", + value: keys, + ); + } + + Future get getSerializedKeysPrevGen async => + await secureStorageInterface.read( + key: "{$walletId}_serializedFROSTKeysPrevGen", + ); + + Future get multisigConfig async => await secureStorageInterface.read( + key: "{$walletId}_multisigConfig", + ); + Future get multisigConfigPrevGen async => + await secureStorageInterface.read( + key: "{$walletId}_multisigConfigPrevGen", + ); + Future _saveMultisigConfig(String multisigConfig) async { + final current = await this.multisigConfig; + + if (current == null) { + // do nothing + } else if (current == multisigConfig) { + // should never occur + } else { + // save current as prev gen before updating current + await secureStorageInterface.write( + key: "{$walletId}_multisigConfigPrevGen", + value: current, + ); + } + + await secureStorageInterface.write( + key: "{$walletId}_multisigConfig", + value: multisigConfig, + ); + } + + Future get multisigId async { + final id = await secureStorageInterface.read( + key: "{$walletId}_multisigIdFROST", + ); + if (id == null) { + return null; + } else { + return id.toUint8ListFromHex; + } + } + + Future saveMultisigId(Uint8List id) async => + await secureStorageInterface.write( + key: "{$walletId}_multisigIdFROST", + value: id.toHex, + ); + + Future get recoveryString async => await secureStorageInterface.read( + key: "{$walletId}_recoveryStringFROST", + ); + Future saveRecoveryString(String recoveryString) async => + await secureStorageInterface.write( + key: "{$walletId}_recoveryStringFROST", + value: recoveryString, + ); + + // =================== Private =============================================== + + Future _getCurrentElectrumXNode() async { + final node = getCurrentNode(); + + return ElectrumXNode( + address: node.host, + port: node.port, + name: node.name, + useSSL: node.useSSL, + id: node.id, + ); + } + + Future _updateElectrumX() async { + final failovers = nodeService + .failoverNodesFor(coin: cryptoCurrency.coin) + .map((e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + )) + .toList(); + + final newNode = await _getCurrentElectrumXNode(); + electrumXClient = ElectrumXClient.from( + node: newNode, + prefs: prefs, + failovers: failovers, + ); + electrumXCachedClient = CachedElectrumXClient.from( + electrumXClient: electrumXClient, + ); + } + + Future _parseUTXO({ + required Map jsonUTXO, + }) async { + final txn = await electrumXCachedClient.getTransaction( + txHash: jsonUTXO["tx_hash"] as String, + verbose: true, + coin: cryptoCurrency.coin, + ); + + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? scriptPubKey; + String? utxoOwnerAddress; + // get UTXO owner address + for (final output in outputs) { + if (output["n"] == vout) { + scriptPubKey = output["scriptPubKey"]?["hex"] as String?; + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + } + } + + final utxo = UTXO( + walletId: walletId, + txid: txn["txid"] as String, + vout: vout, + value: jsonUTXO["value"] as int, + name: "", + isBlocked: false, + blockedReason: null, + isCoinbase: txn["is_coinbase"] as bool? ?? false, + blockHash: txn["blockhash"] as String?, + blockHeight: jsonUTXO["height"] as int?, + blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, + ); + + return utxo; + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 796760dbd..711a895a1 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -25,6 +25,7 @@ import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/dogecoin_wallet.dart'; @@ -311,6 +312,11 @@ abstract class Wallet { case Coin.bitcoinTestNet: return BitcoinWallet(CryptoCurrencyNetwork.test); + case Coin.bitcoinFrost: + return BitcoinFrostWallet(CryptoCurrencyNetwork.main); + case Coin.bitcoinFrostTestNet: + return BitcoinFrostWallet(CryptoCurrencyNetwork.test); + case Coin.bitcoincash: return BitcoincashWallet(CryptoCurrencyNetwork.main); case Coin.bitcoincashTestnet: diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 1f425d498..2bb78e228 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -832,7 +832,7 @@ mixin ElectrumXInterface on Bip39HDWallet { } } - Future getCurrentElectrumXNode() async { + Future _getCurrentElectrumXNode() async { final node = getCurrentNode(); return ElectrumXNode( @@ -844,7 +844,7 @@ mixin ElectrumXInterface on Bip39HDWallet { ); } - Future updateElectrumX({required ElectrumXNode newNode}) async { + Future updateElectrumX() async { final failovers = nodeService .failoverNodesFor(coin: cryptoCurrency.coin) .map((e) => ElectrumXNode( @@ -856,7 +856,7 @@ mixin ElectrumXInterface on Bip39HDWallet { )) .toList(); - final newNode = await getCurrentElectrumXNode(); + final newNode = await _getCurrentElectrumXNode(); electrumXClient = ElectrumXClient.from( node: newNode, prefs: prefs, @@ -1160,8 +1160,7 @@ mixin ElectrumXInterface on Bip39HDWallet { @override Future updateNode() async { - final node = await getCurrentElectrumXNode(); - await updateElectrumX(newNode: node); + await updateElectrumX(); } FeeObject? _cachedFees; diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 7576999de..0e801490f 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -169,6 +169,8 @@ class _NodeCardState extends ConsumerState { case Coin.namecoin: case Coin.bitcoincashTestnet: case Coin.eCash: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: final client = ElectrumXClient( host: node.host, port: node.port, diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index c14b6a2ec..31fd13c30 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -151,6 +151,8 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.namecoin: case Coin.bitcoincashTestnet: case Coin.eCash: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: final client = ElectrumXClient( host: node.host, port: node.port, From 8ae2faa91ff255c8860228a846beb0b4fa262cc9 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 19 Jan 2024 15:42:38 -0600 Subject: [PATCH 008/272] WIP frost wallet logic --- lib/services/frost.dart | 613 ++++++++++++++ .../crypto_currency/coins/bitcoin_frost.dart | 7 + .../intermediate/private_key_currency.dart | 3 + .../isar/models/frost_wallet_info.dart | 23 +- .../isar/models/frost_wallet_info.g.dart | 552 ++++++++++++- .../wallet/impl/bitcoin_frost_wallet.dart | 768 ++++++++++++++++-- lib/wallets/wallet/wallet.dart | 2 +- 7 files changed, 1889 insertions(+), 79 deletions(-) create mode 100644 lib/services/frost.dart diff --git a/lib/services/frost.dart b/lib/services/frost.dart new file mode 100644 index 000000000..a420a5b16 --- /dev/null +++ b/lib/services/frost.dart @@ -0,0 +1,613 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:frostdart/frostdart.dart'; +import 'package:frostdart/frostdart_bindings_generated.dart'; +import 'package:frostdart/output.dart'; +import 'package:frostdart/util.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +abstract class Frost { + //==================== utility =============================================== + static List getParticipants({ + required String multisigConfig, + }) { + try { + final numberOfParticipants = multisigParticipants( + multisigConfig: multisigConfig, + ); + + final List participants = []; + for (int i = 0; i < numberOfParticipants; i++) { + participants.add( + multisigParticipant( + multisigConfig: multisigConfig, + index: i, + ), + ); + } + + return participants; + } catch (e, s) { + Logging.instance.log( + "getParticipants failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static bool validateEncodedMultisigConfig({required String encodedConfig}) { + try { + decodeMultisigConfig(multisigConfig: encodedConfig); + return true; + } catch (e, s) { + Logging.instance.log( + "validateEncodedMultisigConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + return false; + } + } + + static int getThreshold({ + required String multisigConfig, + }) { + try { + final threshold = multisigThreshold( + multisigConfig: multisigConfig, + ); + + return threshold; + } catch (e, s) { + Logging.instance.log( + "getThreshold failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + List<({String address, Amount amount})> recipients, + String changeAddress, + int feePerWeight, + List inputs, + }) extractDataFromSignConfig({ + required String signConfig, + required CryptoCurrency coin, + }) { + try { + final network = coin.network == CryptoCurrencyNetwork.test + ? Network.Testnet + : Network.Mainnet; + final signConfigPointer = decodedSignConfig( + encodedConfig: signConfig, + network: network, + ); + + // get various data from config + final feePerWeight = + signFeePerWeight(signConfigPointer: signConfigPointer); + final changeAddress = signChange(signConfigPointer: signConfigPointer); + final recipientsCount = signPayments( + signConfigPointer: signConfigPointer, + ); + + // get tx recipient info + final List<({String address, Amount amount})> recipients = []; + for (int i = 0; i < recipientsCount; i++) { + final String address = signPaymentAddress( + signConfigPointer: signConfigPointer, + index: i, + ); + final int amount = signPaymentAmount( + signConfigPointer: signConfigPointer, + index: i, + ); + recipients.add( + ( + address: address, + amount: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.fractionDigits, + ), + ), + ); + } + + // get utxos + final count = signInputs(signConfigPointer: signConfigPointer); + final List outputs = []; + for (int i = 0; i < count; i++) { + final output = signInput( + signConfig: signConfig, + index: i, + network: network, + ); + + outputs.add(output); + } + + return ( + recipients: recipients, + changeAddress: changeAddress, + feePerWeight: feePerWeight, + inputs: outputs, + ); + } catch (e, s) { + Logging.instance.log( + "extractDataFromSignConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + //==================== wallet creation ======================================= + + static String createMultisigConfig({ + required String name, + required int threshold, + required List participants, + }) { + try { + final config = newMultisigConfig( + name: name, + threshold: threshold, + participants: participants, + ); + + return config; + } catch (e, s) { + Logging.instance.log( + "createMultisigConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + String seed, + String commitments, + Pointer multisigConfigWithNamePtr, + Pointer secretShareMachineWrapperPtr, + }) startKeyGeneration({ + required String multisigConfig, + required String myName, + }) { + try { + final startKeyGenResPtr = startKeyGen( + multisigConfig: multisigConfig, + myName: myName, + language: Language.english, + ); + + final seed = startKeyGenResPtr.ref.seed.toDartString(); + final commitments = startKeyGenResPtr.ref.commitments.toDartString(); + final configWithNamePtr = startKeyGenResPtr.ref.config; + final machinePtr = startKeyGenResPtr.ref.machine; + + return ( + seed: seed, + commitments: commitments, + multisigConfigWithNamePtr: configWithNamePtr, + secretShareMachineWrapperPtr: machinePtr, + ); + } catch (e, s) { + Logging.instance.log( + "startKeyGeneration failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + String share, + Pointer secretSharesResPtr, + }) generateSecretShares({ + required Pointer multisigConfigWithNamePtr, + required String mySeed, + required Pointer secretShareMachineWrapperPtr, + required List commitments, + }) { + try { + final secretSharesResPtr = getSecretShares( + multisigConfigWithName: multisigConfigWithNamePtr, + seed: mySeed, + language: Language.english, + machine: secretShareMachineWrapperPtr, + commitments: commitments, + ); + + final share = secretSharesResPtr.ref.shares.toDartString(); + + return (share: share, secretSharesResPtr: secretSharesResPtr); + } catch (e, s) { + Logging.instance.log( + "generateSecretShares failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + Uint8List multisigId, + String recoveryString, + String serializedKeys, + }) completeKeyGeneration({ + required Pointer multisigConfigWithNamePtr, + required Pointer secretSharesResPtr, + required List shares, + }) { + try { + final keyGenResPtr = completeKeyGen( + multisigConfigWithName: multisigConfigWithNamePtr, + machineAndCommitments: secretSharesResPtr, + shares: shares, + ); + + final id = Uint8List.fromList( + List.generate( + MULTISIG_ID_LENGTH, + (index) => keyGenResPtr.ref.multisig_id[index], + ), + ); + + final recoveryString = keyGenResPtr.ref.recovery.toDartString(); + + final serializedKeys = serializeKeys(keys: keyGenResPtr.ref.keys); + + return ( + multisigId: id, + recoveryString: recoveryString, + serializedKeys: serializedKeys, + ); + } catch (e, s) { + Logging.instance.log( + "completeKeyGeneration failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + //=================== transaction creation =================================== + + static String createSignConfig({ + required int network, + required List<({UTXO utxo, Uint8List scriptPubKey})> inputs, + required List<({String address, Amount amount, bool isChange})> outputs, + required String changeAddress, + required int feePerWeight, + }) { + try { + final signConfig = newSignConfig( + network: network, + outputs: inputs + .map( + (e) => Output( + hash: e.utxo.txid.toUint8ListFromHex, + vout: e.utxo.vout, + value: e.utxo.value, + scriptPubKey: e.scriptPubKey, + ), + ) + .toList(), + paymentAddresses: outputs.map((e) => e.address).toList(), + paymentAmounts: outputs.map((e) => e.amount.raw.toInt()).toList(), + change: changeAddress, + feePerWeight: feePerWeight, + ); + + return signConfig; + } catch (e, s) { + Logging.instance.log( + "createSignConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + Pointer machinePtr, + String preprocess, + }) attemptSignConfig({ + required int network, + required String config, + required String serializedKeys, + }) { + try { + final keys = deserializeKeys(keys: serializedKeys); + + final attemptSignRes = attemptSign( + thresholdKeysWrapperPointer: keys, + network: network, + signConfig: config, + ); + + return ( + preprocess: attemptSignRes.ref.preprocess.toDartString(), + machinePtr: attemptSignRes.ref.machine, + ); + } catch (e, s) { + Logging.instance.log( + "attemptSignConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + Pointer machinePtr, + String share, + }) continueSigning({ + required Pointer machinePtr, + required List preprocesses, + }) { + try { + final continueSignRes = continueSign( + machine: machinePtr, + preprocesses: preprocesses, + ); + + return ( + share: continueSignRes.ref.preprocess.toDartString(), + machinePtr: continueSignRes.ref.machine, + ); + } catch (e, s) { + Logging.instance.log( + "continueSigning failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static String completeSigning({ + required Pointer machinePtr, + required List shares, + }) { + try { + final rawTransaction = completeSign( + machine: machinePtr, + shares: shares, + ); + + return rawTransaction; + } catch (e, s) { + Logging.instance.log( + "completeSigning failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static Pointer decodedSignConfig({ + required String encodedConfig, + required int network, + }) { + try { + final configPtr = + decodeSignConfig(encodedSignConfig: encodedConfig, network: network); + return configPtr; + } catch (e, s) { + Logging.instance.log( + "decodedSignConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + //========================== resharing ======================================= + + static String createResharerConfig({ + required int newThreshold, + required List resharers, + required List newParticipants, + }) { + try { + final config = newResharerConfig( + newThreshold: newThreshold, + newParticipants: newParticipants, + resharers: resharers, + ); + + return config; + } catch (e, s) { + Logging.instance.log( + "createResharerConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + String resharerStart, + Pointer machine, + }) beginResharer({ + required String serializedKeys, + required String config, + }) { + try { + final result = startResharer( + serializedKeys: serializedKeys, + config: config, + ); + + return ( + resharerStart: result.encoded, + machine: result.machine, + ); + } catch (e, s) { + Logging.instance.log( + "beginResharer failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + /// expects [resharerStarts] of length equal to resharers. + static ({ + String resharedStart, + Pointer prior, + }) beginReshared({ + required String myName, + required String resharerConfig, + required List resharerStarts, + }) { + try { + final result = startReshared( + newMultisigName: 'unused_property', + myName: myName, + resharerConfig: resharerConfig, + resharerStarts: resharerStarts, + ); + return ( + resharedStart: result.encoded, + prior: result.machine, + ); + } catch (e, s) { + Logging.instance.log( + "beginReshared failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + /// expects [encryptionKeysOfResharedTo] of length equal to new participants + static String finishResharer({ + required StartResharerRes machine, + required List encryptionKeysOfResharedTo, + }) { + try { + final result = completeResharer( + machine: machine, + encryptionKeysOfResharedTo: encryptionKeysOfResharedTo, + ); + return result; + } catch (e, s) { + Logging.instance.log( + "finishResharer failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + /// expects [resharerCompletes] of length equal to resharers + static ({ + String multisigConfig, + String serializedKeys, + String resharedId, + }) finishReshared({ + required StartResharedRes prior, + required List resharerCompletes, + }) { + try { + final result = completeReshared( + prior: prior, + resharerCompletes: resharerCompletes, + ); + return result; + } catch (e, s) { + Logging.instance.log( + "finishReshared failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static Pointer decodedResharerConfig({ + required String resharerConfig, + }) { + try { + final config = decodeResharerConfig(resharerConfig: resharerConfig); + + return config; + } catch (e, s) { + Logging.instance.log( + "decodedResharerConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + int newThreshold, + List resharers, + List newParticipants, + }) extractResharerConfigData({ + required String resharerConfig, + }) { + try { + final newThreshold = resharerNewThreshold( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + ); + + final resharersCount = resharerResharers( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + ); + final List resharers = []; + for (int i = 0; i < resharersCount; i++) { + resharers.add( + resharerResharer( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + index: i, + ), + ); + } + + final newParticipantsCount = resharerNewParticipants( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + ); + final List newParticipants = []; + for (int i = 0; i < newParticipantsCount; i++) { + newParticipants.add( + resharerNewParticipant( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + index: i, + ), + ); + } + + return ( + newThreshold: newThreshold, + resharers: resharers, + newParticipants: newParticipants, + ); + } catch (e, s) { + Logging.instance.log( + "extractResharerConfigData failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } +} diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index f968818e1..b82d3987c 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; @@ -48,6 +49,12 @@ class BitcoinFrost extends FrostCurrency { } } + @override + Amount get dustLimit => Amount( + rawValue: BigInt.from(294), + fractionDigits: fractionDigits, + ); + @override String pubKeyToScriptHash({required Uint8List pubKey}) { try { diff --git a/lib/wallets/crypto_currency/intermediate/private_key_currency.dart b/lib/wallets/crypto_currency/intermediate/private_key_currency.dart index 8cbf11b27..0c10937fa 100644 --- a/lib/wallets/crypto_currency/intermediate/private_key_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/private_key_currency.dart @@ -1,9 +1,12 @@ import 'dart:typed_data'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; abstract class FrostCurrency extends CryptoCurrency { FrostCurrency(super.network); String pubKeyToScriptHash({required Uint8List pubKey}); + + Amount get dustLimit; } diff --git a/lib/wallets/isar/models/frost_wallet_info.dart b/lib/wallets/isar/models/frost_wallet_info.dart index 817f78f39..b5c7476d2 100644 --- a/lib/wallets/isar/models/frost_wallet_info.dart +++ b/lib/wallets/isar/models/frost_wallet_info.dart @@ -12,27 +12,30 @@ class FrostWalletInfo implements IsarId { final String walletId; final List knownSalts; + final List participants; + final String myName; + final int threshold; FrostWalletInfo({ required this.walletId, required this.knownSalts, + required this.participants, + required this.myName, + required this.threshold, }); FrostWalletInfo copyWith({ List? knownSalts, + List? participants, + String? myName, + int? threshold, }) { return FrostWalletInfo( walletId: walletId, knownSalts: knownSalts ?? this.knownSalts, - ); - } - - Future updateKnownSalts( - List knownSalts, { - required Isar isar, - }) async { - // await isar.writeTxn(() async { - // await isar. - // }) + participants: participants ?? this.participants, + myName: myName ?? this.myName, + threshold: threshold ?? this.threshold, + )..id = id; } } diff --git a/lib/wallets/isar/models/frost_wallet_info.g.dart b/lib/wallets/isar/models/frost_wallet_info.g.dart index ce5ae2aae..6c80125e2 100644 --- a/lib/wallets/isar/models/frost_wallet_info.g.dart +++ b/lib/wallets/isar/models/frost_wallet_info.g.dart @@ -22,8 +22,23 @@ const FrostWalletInfoSchema = CollectionSchema( name: r'knownSalts', type: IsarType.stringList, ), - r'walletId': PropertySchema( + r'myName': PropertySchema( id: 1, + name: r'myName', + type: IsarType.string, + ), + r'participants': PropertySchema( + id: 2, + name: r'participants', + type: IsarType.stringList, + ), + r'threshold': PropertySchema( + id: 3, + name: r'threshold', + type: IsarType.long, + ), + r'walletId': PropertySchema( + id: 4, name: r'walletId', type: IsarType.string, ) @@ -69,6 +84,14 @@ int _frostWalletInfoEstimateSize( bytesCount += value.length * 3; } } + bytesCount += 3 + object.myName.length * 3; + bytesCount += 3 + object.participants.length * 3; + { + for (var i = 0; i < object.participants.length; i++) { + final value = object.participants[i]; + bytesCount += value.length * 3; + } + } bytesCount += 3 + object.walletId.length * 3; return bytesCount; } @@ -80,7 +103,10 @@ void _frostWalletInfoSerialize( Map> allOffsets, ) { writer.writeStringList(offsets[0], object.knownSalts); - writer.writeString(offsets[1], object.walletId); + writer.writeString(offsets[1], object.myName); + writer.writeStringList(offsets[2], object.participants); + writer.writeLong(offsets[3], object.threshold); + writer.writeString(offsets[4], object.walletId); } FrostWalletInfo _frostWalletInfoDeserialize( @@ -91,7 +117,10 @@ FrostWalletInfo _frostWalletInfoDeserialize( ) { final object = FrostWalletInfo( knownSalts: reader.readStringList(offsets[0]) ?? [], - walletId: reader.readString(offsets[1]), + myName: reader.readString(offsets[1]), + participants: reader.readStringList(offsets[2]) ?? [], + threshold: reader.readLong(offsets[3]), + walletId: reader.readString(offsets[4]), ); object.id = id; return object; @@ -108,6 +137,12 @@ P _frostWalletInfoDeserializeProp

( return (reader.readStringList(offset) ?? []) as P; case 1: return (reader.readString(offset)) as P; + case 2: + return (reader.readStringList(offset) ?? []) as P; + case 3: + return (reader.readLong(offset)) as P; + case 4: + return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -589,6 +624,423 @@ extension FrostWalletInfoQueryFilter }); } + QueryBuilder + myNameEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'myName', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'myName', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'myName', + value: '', + )); + }); + } + + QueryBuilder + myNameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'myName', + value: '', + )); + }); + } + + QueryBuilder + participantsElementEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'participants', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'participants', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'participants', + value: '', + )); + }); + } + + QueryBuilder + participantsElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'participants', + value: '', + )); + }); + } + + QueryBuilder + participantsLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + participantsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + participantsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + participantsLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + participantsLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + participantsLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder + thresholdEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'threshold', + value: value, + )); + }); + } + + QueryBuilder + thresholdGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'threshold', + value: value, + )); + }); + } + + QueryBuilder + thresholdLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'threshold', + value: value, + )); + }); + } + + QueryBuilder + thresholdBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'threshold', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder walletIdEqualTo( String value, { @@ -734,6 +1186,33 @@ extension FrostWalletInfoQueryLinks extension FrostWalletInfoQuerySortBy on QueryBuilder { + QueryBuilder sortByMyName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'myName', Sort.asc); + }); + } + + QueryBuilder + sortByMyNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'myName', Sort.desc); + }); + } + + QueryBuilder + sortByThreshold() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'threshold', Sort.asc); + }); + } + + QueryBuilder + sortByThresholdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'threshold', Sort.desc); + }); + } + QueryBuilder sortByWalletId() { return QueryBuilder.apply(this, (query) { @@ -763,6 +1242,33 @@ extension FrostWalletInfoQuerySortThenBy }); } + QueryBuilder thenByMyName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'myName', Sort.asc); + }); + } + + QueryBuilder + thenByMyNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'myName', Sort.desc); + }); + } + + QueryBuilder + thenByThreshold() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'threshold', Sort.asc); + }); + } + + QueryBuilder + thenByThresholdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'threshold', Sort.desc); + }); + } + QueryBuilder thenByWalletId() { return QueryBuilder.apply(this, (query) { @@ -787,6 +1293,27 @@ extension FrostWalletInfoQueryWhereDistinct }); } + QueryBuilder distinctByMyName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'myName', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByParticipants() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'participants'); + }); + } + + QueryBuilder + distinctByThreshold() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'threshold'); + }); + } + QueryBuilder distinctByWalletId( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -810,6 +1337,25 @@ extension FrostWalletInfoQueryProperty }); } + QueryBuilder myNameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'myName'); + }); + } + + QueryBuilder, QQueryOperations> + participantsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'participants'); + }); + } + + QueryBuilder thresholdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'threshold'); + }); + } + QueryBuilder walletIdProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'walletId'); diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 203ea5877..102beb1e3 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ffi'; import 'package:flutter/foundation.dart'; import 'package:frostdart/frostdart.dart' as frost; @@ -8,8 +9,15 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -19,21 +27,275 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_cur import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; -import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart'; -class BitcoinFrostWallet extends Wallet - with PrivateKeyInterface { - FrostWalletInfo get frostInfo => throw UnimplementedError(); +class BitcoinFrostWallet extends Wallet { + BitcoinFrostWallet(CryptoCurrencyNetwork network) + : super(BitcoinFrost(network) as T); + + FrostWalletInfo get frostInfo => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .findFirstSync()!; late ElectrumXClient electrumXClient; late CachedElectrumXClient electrumXCachedClient; + Future initializeNewFrost({ + required String mnemonic, + required String multisigConfig, + required String recoveryString, + required String serializedKeys, + required Uint8List multisigId, + required String myName, + required List participants, + required int threshold, + }) async { + Logging.instance.log( + "Generating new FROST wallet.", + level: LogLevel.Info, + ); + + try { + final salt = frost + .multisigSalt( + multisigConfig: multisigConfig, + ) + .toHex; + + final FrostWalletInfo frostWalletInfo = FrostWalletInfo( + walletId: info.walletId, + knownSalts: [salt], + participants: participants, + myName: myName, + threshold: threshold, + ); + + await secureStorageInterface.write( + key: Wallet.mnemonicKey(walletId: info.walletId), + value: mnemonic, + ); + await secureStorageInterface.write( + key: Wallet.mnemonicPassphraseKey(walletId: info.walletId), + value: "", + ); + await _saveSerializedKeys(serializedKeys); + await _saveRecoveryString(recoveryString); + await _saveMultisigId(multisigId); + await _saveMultisigConfig(multisigConfig); + + await mainDB.isar.frostWalletInfo.put(frostWalletInfo); + + final keys = frost.deserializeKeys(keys: serializedKeys); + + final addressString = frost.addressForKeys( + network: cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet, + keys: keys, + ); + + final publicKey = frost.scriptPubKeyForKeys(keys: keys); + + final address = Address( + walletId: info.walletId, + value: addressString, + publicKey: publicKey.toUint8ListFromHex, + derivationIndex: 0, + derivationPath: null, + subType: AddressSubType.receiving, + type: AddressType.unknown, + ); + + await mainDB.putAddresses([address]); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from initializeNewFrost(): $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + Future frostCreateSignConfig({ + required TxData txData, + required String changeAddress, + required int feePerWeight, + }) async { + try { + if (txData.recipients == null || txData.recipients!.isEmpty) { + throw Exception("No recipients found!"); + } + + final total = txData.recipients! + .map((e) => e.amount) + .reduce((value, e) => value += e); + + final utxos = await mainDB + .getUTXOs(walletId) + .filter() + .isBlockedEqualTo(false) + .findAll(); + + if (utxos.isEmpty) { + throw Exception("No UTXOs found"); + } else { + final currentHeight = await chainHeight; + utxos.removeWhere( + (e) => !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + ), + ); + if (utxos.isEmpty) { + throw Exception("No confirmed UTXOs found"); + } + } + + if (total.raw > + utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e)) { + throw Exception("Insufficient available funds"); + } + + Amount sum = Amount.zeroWith( + fractionDigits: cryptoCurrency.fractionDigits, + ); + final Set utxosToUse = {}; + for (final utxo in utxos) { + sum += Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + utxosToUse.add(utxo); + if (sum > total) { + break; + } + } + + final serializedKeys = await _getSerializedKeys(); + final keys = frost.deserializeKeys(keys: serializedKeys!); + + final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet; + + final publicKey = frost + .scriptPubKeyForKeys( + keys: keys, + ) + .toUint8ListFromHex; + + final config = Frost.createSignConfig( + network: network, + inputs: utxosToUse + .map((e) => ( + utxo: e, + scriptPubKey: publicKey, + )) + .toList(), + outputs: txData.recipients!, + changeAddress: (await getCurrentReceivingAddress())!.value, + feePerWeight: feePerWeight, + ); + + return txData.copyWith(frostMSConfig: config, utxos: utxosToUse); + } catch (_) { + rethrow; + } + } + + Future< + ({ + Pointer machinePtr, + String preprocess, + })> frostAttemptSignConfig({ + required String config, + }) async { + final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet; + final serializedKeys = await _getSerializedKeys(); + + return Frost.attemptSignConfig( + network: network, + config: config, + serializedKeys: serializedKeys!, + ); + } + + Future updateWithResharedData({ + required String serializedKeys, + required String multisigConfig, + required bool isNewWallet, + }) async { + await _saveSerializedKeys(serializedKeys); + await _saveMultisigConfig(multisigConfig); + + await _updateThreshold( + frost.getThresholdFromKeys( + serializedKeys: serializedKeys, + ), + ); + + final myNameIndex = frost.getParticipantIndexFromKeys( + serializedKeys: serializedKeys, + ); + final participants = Frost.getParticipants( + multisigConfig: multisigConfig, + ); + final myName = participants[myNameIndex]; + + await _updateParticipants(participants); + await _updateMyName(myName); + + if (isNewWallet) { + await recover( + serializedKeys: serializedKeys, + multisigConfig: multisigConfig, + isRescan: false, + ); + } + } + + Future sweepAllEstimate(int feeRate) async { + int available = 0; + int inputCount = 0; + final height = await chainHeight; + for (final output in (await mainDB.getUTXOs(walletId).findAll())) { + if (!output.isBlocked && + output.isConfirmed(height, cryptoCurrency.minConfirms)) { + available += output.value; + inputCount++; + } + } + + // transaction will only have 1 output minus the fee + final estimatedFee = _roughFeeEstimate(inputCount, 1, feeRate); + + return Amount( + rawValue: BigInt.from(available), + fractionDigits: cryptoCurrency.fractionDigits, + ) - + estimatedFee; + } + + // int _estimateTxFee({required int vSize, required int feeRatePerKB}) { + // return vSize * (feeRatePerKB / 1000).ceil(); + // } + + Amount _roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + // ==================== Overrides ============================================ + @override int get isarTransactionVersion => 2; - BitcoinFrostWallet(CryptoCurrencyNetwork network) - : super(BitcoinFrost(network) as T); - @override FilterOperation? get changeAddressFilterOperation => FilterGroup.and( [ @@ -62,62 +324,321 @@ class BitcoinFrostWallet extends Wallet ], ); - // Future> fetchAddressesForElectrumXScan() async { - // final allAddresses = await mainDB - // .getAddresses(walletId) - // .filter() - // .typeEqualTo(AddressType.frostMS) - // .and() - // .group( - // (q) => q - // .subTypeEqualTo(AddressSubType.receiving) - // .or() - // .subTypeEqualTo(AddressSubType.change), - // ) - // .findAll(); - // return allAddresses; - // } + @override + Future updateTransactions() async { + final myAddress = (await getCurrentReceivingAddress())!; + + final scriptHash = cryptoCurrency.pubKeyToScriptHash( + pubKey: Uint8List.fromList(myAddress.publicKey), + ); + final allTxHashes = + (await electrumXClient.getHistory(scripthash: scriptHash)).toSet(); + + final currentHeight = await chainHeight; + final coin = info.coin; + + List> allTransactions = []; + + for (final txHash in allTxHashes) { + final storedTx = await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(txHash["tx_hash"] as String) + .findFirst(); + + if (storedTx == null || + !storedTx.isConfirmed(currentHeight, cryptoCurrency.minConfirms)) { + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + coin: cryptoCurrency.coin, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) + as Map); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.fromElectrumxJson( + json: map, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + coinbase: coinbase, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (input.addresses.contains(myAddress.value)) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (output.addresses.contains(myAddress.value)) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + TransactionSubType subType = TransactionSubType.none; + if (outputs.length > 1 && inputs.isNotEmpty) { + for (int i = 0; i < outputs.length; i++) { + List? scriptChunks = outputs[i].scriptPubKeyAsm?.split(" "); + if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { + final blindedPaymentCode = scriptChunks![1]; + final bytes = blindedPaymentCode.toUint8ListFromHex; + + // https://en.bitcoin.it/wiki/BIP_0047#Sending + if (bytes.length == 80 && bytes.first == 1) { + subType = TransactionSubType.bip47Notification; + break; + } + } + } + } + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (amountReceivedInThisWallet == totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.log( + "Unexpected tx found (ignoring it): $txData", + level: LogLevel.Error, + ); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } @override - Future updateTransactions() { - // TODO: implement updateTransactions - throw UnimplementedError(); + Future checkSaveInitialReceivingAddress() async { + // should not be needed for frost as we explicitly save the address + // on new init and restore } - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + @override + Future confirmSend({required TxData txData}) async { + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + + final hex = txData.raw!; + + final txHash = await electrumXClient.broadcastTransaction(rawTx: hex); + Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + + // mark utxos as used + final usedUTXOs = txData.utxos!.map((e) => e.copyWith(used: true)); + await mainDB.putUTXOs(usedUTXOs.toList()); + + txData = txData.copyWith( + utxos: usedUTXOs.toSet(), + txHash: txHash, + txid: txHash, + ); + + return txData; + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error); + rethrow; + } } - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return Amount( - rawValue: BigInt.from( - ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil()), + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + final available = info.cachedBalance.spendable; + + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { + return _roughFeeEstimate(1, 2, feeRate); + } + + Amount runningBalance = Amount( + rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits, ); + int inputCount = 0; + for (final output in (await mainDB.getUTXOs(walletId).findAll())) { + if (!output.isBlocked) { + runningBalance += Amount( + rawValue: BigInt.from(output.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + inputCount++; + if (runningBalance > amount) { + break; + } + } + } + + final oneOutPutFee = _roughFeeEstimate(inputCount, 1, feeRate); + final twoOutPutFee = _roughFeeEstimate(inputCount, 2, feeRate); + + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + cryptoCurrency.dustLimit) { + final change = runningBalance - amount - twoOutPutFee; + if (change > cryptoCurrency.dustLimit && + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; + } else { + return runningBalance - amount; + } + } else { + return runningBalance - amount; + } + } else if (runningBalance - amount == oneOutPutFee) { + return oneOutPutFee; + } else { + return twoOutPutFee; + } } @override - Future checkSaveInitialReceivingAddress() { - // TODO: implement checkSaveInitialReceivingAddress - throw UnimplementedError(); - } + Future get fees async { + try { + // adjust numbers for different speeds? + const int f = 1, m = 5, s = 20; - @override - Future confirmSend({required TxData txData}) { - // TODO: implement confirmSend - throw UnimplementedError(); - } + final fast = await electrumXClient.estimateFee(blocks: f); + final medium = await electrumXClient.estimateFee(blocks: m); + final slow = await electrumXClient.estimateFee(blocks: s); - @override - Future estimateFeeFor(Amount amount, int feeRate) { - // TODO: implement estimateFeeFor - throw UnimplementedError(); - } + final feeObject = FeeObject( + numberOfBlocksFast: f, + numberOfBlocksAverage: m, + numberOfBlocksSlow: s, + fast: Amount.fromDecimal( + fast, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw.toInt(), + ); - @override - // TODO: implement fees - Future get fees => throw UnimplementedError(); + Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); + return feeObject; + } catch (e) { + Logging.instance + .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); + rethrow; + } + } @override Future prepareSend({required TxData txData}) { @@ -138,6 +659,16 @@ class BitcoinFrostWallet extends Wallet ); } + final coin = info.coin; + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + try { await refreshMutex.protect(() async { if (!isRescan) { @@ -146,12 +677,16 @@ class BitcoinFrostWallet extends Wallet multisigConfig: multisigConfig, ) .toHex; - final knownSalts = frostInfo.knownSalts; + final knownSalts = _getKnownSalts(); if (knownSalts.contains(salt)) { throw Exception("Known frost multisig salt found!"); } knownSalts.add(salt); - await frostInfo.updateKnownSalts(knownSalts, isar: mainDB.isar); + await _updateKnownSalts(knownSalts); + } else { + // clear cache + await electrumXCachedClient.clearSharedTransactionCache(coin: coin); + await mainDB.deleteWalletBlockchainData(walletId); } final keys = frost.deserializeKeys(keys: serializedKeys); @@ -180,12 +715,27 @@ class BitcoinFrostWallet extends Wallet await mainDB.updateOrPutAddresses([address]); }); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + unawaited(refresh()); } catch (e, s) { Logging.instance.log( "recoverFromSerializedKeys failed: $e\n$s", level: LogLevel.Fatal, ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); rethrow; } } @@ -309,12 +859,15 @@ class BitcoinFrostWallet extends Wallet // =================== Secure storage ======================================== - Future get getSerializedKeys async => + Future _getSerializedKeys() async => await secureStorageInterface.read( key: "{$walletId}_serializedFROSTKeys", ); - Future _saveSerializedKeys(String keys) async { - final current = await getSerializedKeys; + + Future _saveSerializedKeys( + String keys, + ) async { + final current = await _getSerializedKeys(); if (current == null) { // do nothing @@ -334,20 +887,24 @@ class BitcoinFrostWallet extends Wallet ); } - Future get getSerializedKeysPrevGen async => + Future _getSerializedKeysPrevGen() async => await secureStorageInterface.read( key: "{$walletId}_serializedFROSTKeysPrevGen", ); - Future get multisigConfig async => await secureStorageInterface.read( + Future _multisigConfig() async => await secureStorageInterface.read( key: "{$walletId}_multisigConfig", ); - Future get multisigConfigPrevGen async => + + Future _multisigConfigPrevGen() async => await secureStorageInterface.read( key: "{$walletId}_multisigConfigPrevGen", ); - Future _saveMultisigConfig(String multisigConfig) async { - final current = await this.multisigConfig; + + Future _saveMultisigConfig( + String multisigConfig, + ) async { + final current = await _multisigConfig(); if (current == null) { // do nothing @@ -367,7 +924,7 @@ class BitcoinFrostWallet extends Wallet ); } - Future get multisigId async { + Future _multisigId() async { final id = await secureStorageInterface.read( key: "{$walletId}_multisigIdFROST", ); @@ -378,21 +935,92 @@ class BitcoinFrostWallet extends Wallet } } - Future saveMultisigId(Uint8List id) async => + Future _saveMultisigId( + Uint8List id, + ) async => await secureStorageInterface.write( key: "{$walletId}_multisigIdFROST", value: id.toHex, ); - Future get recoveryString async => await secureStorageInterface.read( + Future _recoveryString() async => await secureStorageInterface.read( key: "{$walletId}_recoveryStringFROST", ); - Future saveRecoveryString(String recoveryString) async => + + Future _saveRecoveryString( + String recoveryString, + ) async => await secureStorageInterface.write( key: "{$walletId}_recoveryStringFROST", value: recoveryString, ); + // =================== DB ==================================================== + + List _getKnownSalts() => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .knownSaltsProperty() + .findFirstSync()!; + + Future _updateKnownSalts(List knownSalts) async { + await mainDB.isar.writeTxn(() async { + final info = frostInfo; + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(knownSalts: knownSalts), + ); + }); + } + + List _getParticipants() => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .participantsProperty() + .findFirstSync()!; + + Future _updateParticipants(List participants) async { + await mainDB.isar.writeTxn(() async { + final info = frostInfo; + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(participants: participants), + ); + }); + } + + int _getThreshold() => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .thresholdProperty() + .findFirstSync()!; + + Future _updateThreshold(int threshold) async { + await mainDB.isar.writeTxn(() async { + final info = frostInfo; + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(threshold: threshold), + ); + }); + } + + String _getMyName() => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .myNameProperty() + .findFirstSync()!; + + Future _updateMyName(String myName) async { + await mainDB.isar.writeTxn(() async { + final info = frostInfo; + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(myName: myName), + ); + }); + } + // =================== Private =============================================== Future _getCurrentElectrumXNode() async { @@ -430,6 +1058,16 @@ class BitcoinFrostWallet extends Wallet ); } + bool _duplicateTxCheck( + List> allTransactions, String txid) { + for (int i = 0; i < allTransactions.length; i++) { + if (allTransactions[i]["txid"] == txid) { + return true; + } + } + return false; + } + Future _parseUTXO({ required Map jsonUTXO, }) async { @@ -443,12 +1081,12 @@ class BitcoinFrostWallet extends Wallet final outputs = txn["vout"] as List; - String? scriptPubKey; + // String? scriptPubKey; String? utxoOwnerAddress; // get UTXO owner address for (final output in outputs) { if (output["n"] == vout) { - scriptPubKey = output["scriptPubKey"]?["hex"] as String?; + // scriptPubKey = output["scriptPubKey"]?["hex"] as String?; utxoOwnerAddress = output["scriptPubKey"]?["addresses"]?[0] as String? ?? output["scriptPubKey"]?["address"] as String?; diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 711a895a1..959b7b635 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -289,7 +289,7 @@ abstract class Wallet { wallet.prefs = prefs; wallet.nodeService = nodeService; - if (wallet is ElectrumXInterface) { + if (wallet is ElectrumXInterface || wallet is BitcoinFrostWallet) { // initialize electrumx instance await wallet.updateNode(); } From 6a7ec2d5d26e26ad199dcc56400794a9c04d17da Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 19 Jan 2024 17:44:01 -0600 Subject: [PATCH 009/272] untested: Bitcoin frost --- crypto_plugins/frostdart | 2 +- ...irm_new_frost_ms_wallet_creation_view.dart | 343 +++++++++++++ .../new/create_new_frost_ms_wallet_view.dart | 285 +++++++++++ .../new/frost_share_commitments_view.dart | 443 ++++++++++++++++ .../frost_ms/new/frost_share_shares_view.dart | 409 +++++++++++++++ .../new/import_new_frost_ms_wallet_view.dart | 386 ++++++++++++++ .../new/share_new_multisig_config_view.dart | 162 ++++++ .../restore/restore_frost_ms_wallet_view.dart | 478 ++++++++++++++++++ .../name_your_wallet_view.dart | 216 +++++--- .../frost_ms/frost_ms_options_view.dart | 162 ++++++ .../frost_ms/frost_participants_view.dart | 119 +++++ .../resharing/finish_resharing_view.dart | 433 ++++++++++++++++ .../step_1a/begin_reshare_config_view.dart | 196 +++++++ .../step_1a/complete_reshare_config_view.dart | 335 ++++++++++++ .../step_1a/display_reshare_config_view.dart | 214 ++++++++ .../step_1b/import_reshare_config_view.dart | 338 +++++++++++++ .../involved/step_2/begin_resharing_view.dart | 439 ++++++++++++++++ .../step_2/continue_resharing_view.dart | 429 ++++++++++++++++ .../new/new_continue_sharing_view.dart | 204 ++++++++ .../new/new_import_resharer_config_view.dart | 426 ++++++++++++++++ .../new/new_start_resharing_view.dart | 379 ++++++++++++++ .../resharing/verify_updated_wallet_view.dart | 315 ++++++++++++ .../frost_wallet/frost_wallet_providers.dart | 103 ++++ lib/route_generator.dart | 333 ++++++++++++ lib/themes/coin_card_provider.dart | 14 + lib/themes/coin_icon_provider.dart | 7 + lib/themes/coin_image_provider.dart | 14 + lib/utilities/enums/coin_enum.dart | 20 + .../models/incomplete_frost_wallet.dart | 42 ++ .../wallet/impl/bitcoin_frost_wallet.dart | 8 +- lib/widgets/detail_item.dart | 90 ++++ .../dialogs/frost_interruption_dialog.dart | 70 +++ lib/widgets/stack_dialog.dart | 12 +- 33 files changed, 7349 insertions(+), 77 deletions(-) create mode 100644 lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart create mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart create mode 100644 lib/providers/frost_wallet/frost_wallet_providers.dart create mode 100644 lib/wallets/models/incomplete_frost_wallet.dart create mode 100644 lib/widgets/detail_item.dart create mode 100644 lib/widgets/dialogs/frost_interruption_dialog.dart diff --git a/crypto_plugins/frostdart b/crypto_plugins/frostdart index 2fa7e4666..0fbc038a2 160000 --- a/crypto_plugins/frostdart +++ b/crypto_plugins/frostdart @@ -1 +1 @@ -Subproject commit 2fa7e46669a023d270cad4552b5151b138738790 +Subproject commit 0fbc038a262e3c2d82c7c6e34e194e9a47011d91 diff --git a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart new file mode 100644 index 000000000..08018b43c --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart @@ -0,0 +1,343 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/transaction_details_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_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/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/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/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/custom_buttons/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; + +import '../../../../wallets/isar/models/wallet_info.dart'; + +class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget { + const ConfirmNewFrostMSWalletCreationView({ + super.key, + required this.walletName, + required this.coin, + }); + + static const String routeName = "/confirmNewFrostMSWalletCreationView"; + + final String walletName; + final Coin coin; + + @override + ConsumerState createState() => + _ConfirmNewFrostMSWalletCreationViewState(); +} + +class _ConfirmNewFrostMSWalletCreationViewState + extends ConsumerState { + late final String seed, recoveryString, serializedKeys, multisigConfig; + late final Uint8List multisigId; + + @override + void initState() { + seed = ref.read(pFrostStartKeyGenData.state).state!.seed; + serializedKeys = + ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys; + recoveryString = + ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString; + multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; + multisigConfig = ref.read(pFrostMultisigConfig.state).state!; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: + Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, + ), + ); + + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: HomeView.routeName, + ), + ); + }, + ), + title: Text( + "Finalize 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: [ + Text( + "Ensure your multisig ID matches that of each other participant", + style: STextStyles.pageTitleH2(context), + ), + const _Div(), + DetailItem( + title: "ID", + detail: multisigId.toString(), + button: Util.isDesktop + ? IconCopyButton( + data: multisigId.toString(), + ) + : SimpleCopyButton( + data: multisigId.toString(), + ), + ), + const _Div(), + const _Div(), + Text( + "Back up your keys and config", + style: STextStyles.pageTitleH2(context), + ), + const _Div(), + DetailItem( + title: "Multisig Config", + detail: multisigConfig, + button: Util.isDesktop + ? IconCopyButton( + data: multisigConfig, + ) + : SimpleCopyButton( + data: multisigConfig, + ), + ), + const _Div(), + DetailItem( + title: "Keys", + detail: serializedKeys, + button: Util.isDesktop + ? IconCopyButton( + data: serializedKeys, + ) + : SimpleCopyButton( + data: serializedKeys, + ), + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Confirm", + onPressed: () async { + bool progressPopped = false; + try { + unawaited( + showDialog( + context: context, + barrierDismissible: false, + useSafeArea: true, + builder: (ctx) { + return const Center( + child: LoadingIndicator( + width: 50, + height: 50, + ), + ); + }, + ), + ); + + final info = WalletInfo.createNew( + coin: widget.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), + ); + + await (wallet as BitcoinFrostWallet).initializeNewFrost( + mnemonic: seed, + 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 (mounted) { + Navigator.pop(context); + progressPopped = true; + } + + if (mounted) { + if (Util.isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + } + + ref.read(pFrostMultisigConfig.state).state = null; + ref.read(pFrostStartKeyGenData.state).state = null; + ref.read(pFrostSecretSharesData.state).state = null; + + 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, + ); + + // pop progress dialog + if (mounted && !progressPopped) { + Navigator.pop(context); + progressPopped = true; + } + // TODO: handle gracefully + rethrow; + } + }, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart new file mode 100644 index 000000000..b408b61ef --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -0,0 +1,285 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.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/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/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/stack_dialog.dart'; + +class CreateNewFrostMsWalletView extends ConsumerStatefulWidget { + const CreateNewFrostMsWalletView({ + super.key, + required this.walletName, + required this.coin, + }); + + static const String routeName = "/createNewFrostMsWalletView"; + + final String walletName; + final Coin coin; + + @override + ConsumerState createState() => + _NewFrostMsWalletViewState(); +} + +class _NewFrostMsWalletViewState + extends ConsumerState { + final _thresholdController = TextEditingController(); + final _participantsController = TextEditingController(); + + final List controllers = []; + + int _participantsCount = 0; + + String _validateInputData() { + final threshold = int.tryParse(_thresholdController.text); + if (threshold == null) { + return "Choose a threshold"; + } + + final partsCount = int.tryParse(_participantsController.text); + if (partsCount == null) { + return "Choose total number of participants"; + } + + if (threshold > partsCount) { + return "Threshold cannot be greater than the number of participants"; + } + + if (partsCount < 2) { + return "At least two participants required"; + } + + if (controllers.length != partsCount) { + return "Participants count error"; + } + + final hasEmptyParticipants = controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element); + if (hasEmptyParticipants) { + return "Participants must not be empty"; + } + + if (controllers.length != controllers.map((e) => e.text).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(() {}); + } + } + } + + @override + void dispose() { + _thresholdController.dispose(); + _participantsController.dispose(); + for (final e in controllers) { + e.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "New FROST multisig config", + 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: [ + Text( + "Threshold", + style: STextStyles.label(context), + ), + const SizedBox( + height: 10, + ), + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _thresholdController, + ), + const SizedBox( + height: 16, + ), + Text( + "Number of participants", + style: STextStyles.label(context), + ), + const SizedBox( + height: 10, + ), + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _participantsController, + onChanged: _participantsCountChanged, + ), + const SizedBox( + height: 16, + ), + if (controllers.isNotEmpty) + Text( + "My name", + style: STextStyles.label(context), + ), + if (controllers.isNotEmpty) + const SizedBox( + height: 10, + ), + if (controllers.isNotEmpty) + TextField( + controller: controllers.first, + ), + if (controllers.length > 1) + const SizedBox( + height: 16, + ), + if (controllers.length > 1) + Text( + "Remaining participants", + 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], + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Generate", + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final validationMessage = _validateInputData(); + + if (validationMessage != "valid") { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: validationMessage, + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + final config = Frost.createMultisigConfig( + name: controllers.first.text, + threshold: int.parse(_thresholdController.text), + participants: controllers.map((e) => e.text).toList(), + ); + + ref.read(pFrostMyName.notifier).state = controllers.first.text; + ref.read(pFrostMultisigConfig.notifier).state = config; + + await Navigator.of(context).pushNamed( + ShareNewMultisigConfigView.routeName, + arguments: ( + walletName: widget.walletName, + coin: widget.coin, + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart new file mode 100644 index 000000000..bf3649a37 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart @@ -0,0 +1,443 @@ +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.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_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.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/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/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/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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 FrostShareCommitmentsView extends ConsumerStatefulWidget { + const FrostShareCommitmentsView({ + super.key, + required this.walletName, + required this.coin, + }); + + static const String routeName = "/frostShareCommitmentsView"; + + final String walletName; + final Coin coin; + + @override + ConsumerState createState() => + _FrostShareCommitmentsViewState(); +} + +class _FrostShareCommitmentsViewState + extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List participants; + late final String myCommitment; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + @override + void initState() { + participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); + 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 WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: + Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, + ), + ); + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: HomeView.routeName, + ), + ); + }, + ), + title: Text( + "Commitments", + 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: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myCommitment, + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const _Div(), + DetailItem( + title: "My name", + detail: ref.watch(pFrostMyName.state).state!, + ), + const _Div(), + DetailItem( + title: "My commitment", + detail: myCommitment, + button: Util.isDesktop + ? IconCopyButton( + data: myCommitment, + ) + : SimpleCopyButton( + data: myCommitment, + ), + ), + const _Div(), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participants.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostCommitmentsTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter ${participants[i]}'s commitment", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Commitment Field Input.", + key: Key( + "frostCommitmentsClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Commitment Field Input.", + key: Key( + "frostCommitmentsPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .text + .isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: Key( + "frostCommitmentsScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .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(), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Generate shares", + enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: () async { + // check for empty commitments + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing commitments", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect commitment strings and insert my own at the correct index + final commitments = controllers.map((e) => e.text).toList(); + commitments.insert(myIndex, myCommitment); + + try { + ref.read(pFrostSecretSharesData.notifier).state = + Frost.generateSecretShares( + multisigConfigWithNamePtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .multisigConfigWithNamePtr, + mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed, + secretShareMachineWrapperPtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .secretShareMachineWrapperPtr, + commitments: commitments, + ); + + await Navigator.of(context).pushNamed( + FrostShareSharesView.routeName, + arguments: ( + walletName: widget.walletName, + coin: widget.coin, + ), + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to generate shares", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + }, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart new file mode 100644 index 000000000..0f5e70ee7 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart @@ -0,0 +1,409 @@ +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.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_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.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/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/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/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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 FrostShareSharesView extends ConsumerStatefulWidget { + const FrostShareSharesView({ + super.key, + required this.walletName, + required this.coin, + }); + + static const String routeName = "/frostShareSharesView"; + + final String walletName; + final Coin coin; + + @override + ConsumerState createState() => + _FrostShareSharesViewState(); +} + +class _FrostShareSharesViewState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List participants; + late final String myShare; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + @override + void initState() { + participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); + myShare = ref.read(pFrostSecretSharesData.state).state!.share; + + // temporarily remove my name. Added back later + participants.removeAt(myIndex); + + for (int i = 0; i < participants.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: + Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, + ), + ); + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.walletCreation, + popUntilOnYesRouteName: HomeView.routeName, + ), + ); + }, + ), + title: Text( + "Generate shares", + 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: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myShare, + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const _Div(), + DetailItem( + title: "My name", + detail: ref.watch(pFrostMyName.state).state!, + ), + const _Div(), + DetailItem( + title: "My share", + detail: myShare, + button: Util.isDesktop + ? IconCopyButton( + data: myShare, + ) + : SimpleCopyButton( + data: myShare, + ), + ), + const _Div(), + for (int i = 0; i < participants.length; i++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frSharesTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${participants[i]}'s share", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Share Field Input.", + key: Key("frSharesClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Share Field Input.", + key: Key("frSharesPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: Key("frSharesScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ) + ], + ), + ), + ), + ), + ), + ), + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Generate", + onPressed: () async { + // check for empty commitments + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing shares", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect commitment strings and insert my own at the correct index + final shares = controllers.map((e) => e.text).toList(); + shares.insert(myIndex, myShare); + + try { + ref.read(pFrostCompletedKeyGenData.notifier).state = + Frost.completeKeyGeneration( + multisigConfigWithNamePtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .multisigConfigWithNamePtr, + secretSharesResPtr: ref + .read(pFrostSecretSharesData.state) + .state! + .secretSharesResPtr, + shares: shares, + ); + await Navigator.of(context).pushNamed( + ConfirmNewFrostMSWalletCreationView.routeName, + arguments: ( + walletName: widget.walletName, + coin: widget.coin, + ), + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to complete key generation", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + }, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart new file mode 100644 index 000000000..1b08b045d --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart @@ -0,0 +1,386 @@ +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:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.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/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/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/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 ImportNewFrostMsWalletView extends ConsumerStatefulWidget { + const ImportNewFrostMsWalletView({ + super.key, + required this.walletName, + required this.coin, + }); + + static const String routeName = "/importNewFrostMsWalletView"; + + final String walletName; + final Coin coin; + + @override + ConsumerState createState() => + _ImportNewFrostMsWalletViewState(); +} + +class _ImportNewFrostMsWalletViewState + extends ConsumerState { + late final TextEditingController myNameFieldController, configFieldController; + late final FocusNode myNameFocusNode, configFocusNode; + + bool _nameEmpty = true, _configEmpty = true; + + @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 ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Import FROST multisig config", + 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: myNameFieldController, + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, + focusNode: myNameFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "My name", + myNameFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _nameEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_nameEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frMyNameClearButtonKey"), + onTap: () { + myNameFieldController.text = ""; + + setState(() { + _nameEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Name Field.", + key: const Key("frMyNamePasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + myNameFieldController.text = + data.text!.trim(); + } + + setState(() { + _nameEmpty = + myNameFieldController.text.isEmpty; + }); + }, + child: _nameEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start key generation", + enabled: !_nameEmpty && !_configEmpty, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final config = configFieldController.text; + + if (!Frost.validateEncodedMultisigConfig( + encodedConfig: config)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Invalid config", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + if (!Frost.getParticipants(multisigConfig: config) + .contains(myNameFieldController.text)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "My name not found in config participants", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + ref.read(pFrostMyName.state).state = myNameFieldController.text; + ref.read(pFrostMultisigConfig.notifier).state = config; + + ref.read(pFrostStartKeyGenData.state).state = + Frost.startKeyGeneration( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + myName: ref.read(pFrostMyName.state).state!, + ); + + await Navigator.of(context).pushNamed( + FrostShareCommitmentsView.routeName, + arguments: ( + walletName: widget.walletName, + coin: widget.coin, + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart new file mode 100644 index 000000000..4afb4c0c5 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.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/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/conditional_parent.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/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/detail_item.dart'; + +class ShareNewMultisigConfigView extends ConsumerStatefulWidget { + const ShareNewMultisigConfigView({ + super.key, + required this.walletName, + required this.coin, + }); + + static const String routeName = "/shareNewMultisigConfigView"; + + final String walletName; + final Coin coin; + + @override + ConsumerState createState() => + _ShareNewMultisigConfigViewState(); +} + +class _ShareNewMultisigConfigViewState + extends ConsumerState { + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Multisig config", + 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( + children: [ + if (!Util.isDesktop) const Spacer(), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: + ref.watch(pFrostMultisigConfig.state).state ?? "Error", + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + 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, + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + PrimaryButton( + label: "Start key generation", + onPressed: () async { + ref.read(pFrostStartKeyGenData.notifier).state = + Frost.startKeyGeneration( + multisigConfig: ref.watch(pFrostMultisigConfig.state).state!, + myName: ref.read(pFrostMyName.state).state!, + ); + + await Navigator.of(context).pushNamed( + FrostShareCommitmentsView.routeName, + arguments: ( + walletName: widget.walletName, + coin: widget.coin, + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart new file mode 100644 index 000000000..e99655f06 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -0,0 +1,478 @@ +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'; +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/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/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/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/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/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.coin, + }); + + static const String routeName = "/restoreFrostMsWalletView"; + + final String walletName; + final Coin coin; + + @override + ConsumerState createState() => + _RestoreFrostMsWalletViewState(); +} + +class _RestoreFrostMsWalletViewState + extends ConsumerState { + late final TextEditingController keysFieldController, configFieldController; + late final FocusNode keysFocusNode, configFocusNode; + + bool _keysEmpty = true, _configEmpty = true; + + bool _restoreButtonLock = false; + + Future _createWalletAndRecover() async { + final keys = keysFieldController.text; + final config = configFieldController.text; + + final myNameIndex = getParticipantIndexFromKeys(serializedKeys: keys); + final participants = Frost.getParticipants(multisigConfig: config); + final myName = participants[myNameIndex]; + + final info = WalletInfo.createNew( + coin: widget.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: multisigThreshold( + multisigConfig: config, + ), + ); + + await ref.read(mainDBProvider).isar.frostWalletInfo.put(frostInfo); + + await (wallet as BitcoinFrostWallet).recover( + serializedKeys: keys, + multisigConfig: config, + isRescan: false, + ); + + await info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + return wallet; + } + + Future _restore() async { + if (_restoreButtonLock) { + return; + } + _restoreButtonLock = true; + + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + Exception? ex; + final wallet = await showLoading( + whileFuture: _createWalletAndRecover(), + context: context, + message: "Restoring wallet...", + isDesktop: Util.isDesktop, + onException: (e) { + ex = e; + }, + ); + + if (ex != null) { + throw ex!; + } + + ref.read(pWallets).addWallet(wallet!); + + if (mounted) { + if (Util.isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + } + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Your wallet is set up.", + iconAsset: Assets.svg.check, + context: context, + ), + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to restore", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _restoreButtonLock = false; + } + } + + @override + void initState() { + keysFieldController = TextEditingController(); + configFieldController = TextEditingController(); + keysFocusNode = FocusNode(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + keysFieldController.dispose(); + configFieldController.dispose(); + keysFocusNode.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Restore FROST multisig wallet", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frMyNameTextFieldKey"), + controller: keysFieldController, + onChanged: (_) { + setState(() { + _keysEmpty = keysFieldController.text.isEmpty; + }); + }, + focusNode: keysFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Keys", + keysFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _keysEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_keysEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Keys Field.", + key: const Key("frMyNameClearButtonKey"), + onTap: () { + keysFieldController.text = ""; + + setState(() { + _keysEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Keys Field.", + key: const Key("frKeysPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + keysFieldController.text = + data.text!.trim(); + } + + setState(() { + _keysEmpty = + keysFieldController.text.isEmpty; + }); + }, + child: _keysEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Restore", + enabled: !_keysEmpty && !_configEmpty, + onPressed: _restore, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index 350c839f8..ed91d265e 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -14,9 +14,13 @@ 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/import_new_frost_ms_wallet_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'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_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/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart'; @@ -32,6 +36,8 @@ 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 +83,52 @@ class _NameYourWalletViewState extends ConsumerState { return name; } + Future _nextPressed() async { + final name = textEditingController.text; + + if (mounted) { + // hide keyboard if has focus + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 50)); + } + + if (mounted) { + ref.read(mnemonicWordCountStateProvider.state).state = + Constants.possibleLengthsForCoin(coin).last; + ref.read(pNewWalletOptions.notifier).state = null; + + switch (widget.addWalletType) { + case AddWalletType.New: + unawaited( + Navigator.of(context).pushNamed( + coin.hasMnemonicPassphraseSupport + ? NewWalletOptionsView.routeName + : NewWalletRecoveryPhraseWarningView.routeName, + arguments: Tuple2( + name, + coin, + ), + ), + ); + break; + + case AddWalletType.Restore: + unawaited( + Navigator.of(context).pushNamed( + RestoreOptionsView.routeName, + arguments: Tuple2( + name, + coin, + ), + ), + ); + break; + } + } + } + } + @override void initState() { isDesktop = Util.isDesktop; @@ -330,78 +382,104 @@ class _NameYourWalletViewState extends ConsumerState { const SizedBox( height: 32, ), - ConstrainedBox( - constraints: BoxConstraints( - minWidth: isDesktop ? 480 : 0, - minHeight: isDesktop ? 70 : 0, + if (widget.coin.isFrost) + if (widget.addWalletType == AddWalletType.Restore) + PrimaryButton( + label: "Next", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; + + await Navigator.of(context).pushNamed( + RestoreFrostMsWalletView.routeName, + arguments: ( + walletName: name, + coin: coin, + ), + ); + }, + ), + if (widget.addWalletType == AddWalletType.New) + Column( + children: [ + PrimaryButton( + label: "Create config", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; + + await Navigator.of(context).pushNamed( + CreateNewFrostMsWalletView.routeName, + arguments: ( + walletName: name, + coin: coin, + ), + ); + }, + ), + const SizedBox( + height: 12, + ), + SecondaryButton( + label: "Import multisig config", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; + + await Navigator.of(context).pushNamed( + ImportNewFrostMsWalletView.routeName, + arguments: ( + walletName: name, + coin: coin, + ), + ); + }, + ), + const SizedBox( + height: 12, + ), + SecondaryButton( + label: "Import resharer config", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; + + await Navigator.of(context).pushNamed( + NewImportResharerConfigView.routeName, + arguments: ( + walletName: name, + coin: coin, + ), + ); + }, + ), + ], ), - child: TextButton( - onPressed: _nextEnabled - ? () async { - final name = textEditingController.text; - - if (mounted) { - // hide keyboard if has focus - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 50)); - } - - if (mounted) { - ref.read(mnemonicWordCountStateProvider.state).state = - Constants.possibleLengthsForCoin(coin).last; - ref.read(pNewWalletOptions.notifier).state = null; - - switch (widget.addWalletType) { - case AddWalletType.New: - unawaited( - Navigator.of(context).pushNamed( - coin.hasMnemonicPassphraseSupport - ? NewWalletOptionsView.routeName - : NewWalletRecoveryPhraseWarningView - .routeName, - arguments: Tuple2( - name, - coin, - ), - ), - ); - break; - - case AddWalletType.Restore: - unawaited( - Navigator.of(context).pushNamed( - RestoreOptionsView.routeName, - arguments: Tuple2( - name, - coin, - ), - ), - ); - break; - } - } - } - } - : null, - style: _nextEnabled - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Next", - style: isDesktop - ? _nextEnabled - ? STextStyles.desktopButtonEnabled(context) - : STextStyles.desktopButtonDisabled(context) - : STextStyles.button(context), + if (!widget.coin.isFrost) + ConstrainedBox( + constraints: BoxConstraints( + minWidth: isDesktop ? 480 : 0, + minHeight: isDesktop ? 70 : 0, + ), + child: TextButton( + onPressed: _nextEnabled ? _nextPressed : null, + style: _nextEnabled + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Next", + style: isDesktop + ? _nextEnabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context) + : STextStyles.button(context), + ), ), ), - ), if (isDesktop) const Spacer( flex: 15, diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart new file mode 100644 index 000000000..169a96b64 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -0,0 +1,162 @@ +/* + * 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/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.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/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostMSWalletOptionsView extends ConsumerWidget { + const FrostMSWalletOptionsView({ + Key? key, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/frostMSWalletOptionsView"; + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Multisig settings", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _OptionButton( + label: "Show participants", + onPressed: () { + Navigator.of(context).pushNamed( + FrostParticipantsView.routeName, + arguments: walletId, + ); + }, + ), + const SizedBox( + height: 8, + ), + _OptionButton( + label: "Initiate resharing", + 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( + BeginReshareConfigView.routeName, + arguments: walletId, + ); + }, + ), + const SizedBox( + height: 8, + ), + _OptionButton( + label: "Import reshare config", + 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( + ImportReshareConfigView.routeName, + arguments: walletId, + ); + }, + ), + ], + ), + ), + ), + ), + ); + } +} + +class _OptionButton extends StatelessWidget { + const _OptionButton({ + super.key, + required this.label, + required this.onPressed, + }); + + final String label; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: onPressed, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + label, + style: STextStyles.titleBold12(context), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart new file mode 100644 index 000000000..b4710bfe7 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/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'; + +class FrostParticipantsView extends ConsumerWidget { + const FrostParticipantsView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/frostParticipantsView"; + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; + + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Participants", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < frostInfo.participants.length; i++) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Index $i", + style: STextStyles.label(context), + ), + const SizedBox( + height: 6, + ), + SelectableText( + frostInfo.participants[i] == frostInfo.myName + ? "${frostInfo.participants[i]} (me)" + : frostInfo.participants[i], + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart new file mode 100644 index 000000000..8d23e9ed4 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart @@ -0,0 +1,433 @@ +import 'dart:ffi'; + +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_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/my_stack_view/exit_to_my_stack_button.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/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/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/custom_buttons/simple_copy_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/detail_item.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 FinishResharingView extends ConsumerStatefulWidget { + const FinishResharingView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/finishResharingView"; + + final String walletId; + + @override + ConsumerState createState() => + _FinishResharingViewState(); +} + +class _FinishResharingViewState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List resharerIndexes; + late final String myName; + late final int? myResharerIndexIndex; + late final String? myResharerComplete; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + if (amOutgoingParticipant) { + ref.read(pFrostResharingData).reset(); + Navigator.of(context).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; + + await Navigator.of(context).pushNamed( + VerifyUpdatedWalletView.routeName, + arguments: widget.walletId, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + final amNewParticipant = + ref.read(pFrostResharingData).startResharerData == null && + ref.read(pFrostResharingData).incompleteWallet != null && + ref.read(pFrostResharingData).incompleteWallet?.walletId == + widget.walletId; + + myName = ref.read(pFrostResharingData).myName!; + + resharerIndexes = 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(widget.walletId)!; + final myOldIndex = + frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); + + myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); + if (myResharerIndexIndex! >= 0) { + // remove my name for now as we don't need a text field for it + resharerIndexes.removeAt(myResharerIndexIndex!); + } + + amOutgoingParticipant = !ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!); + } + + for (int i = 0; i < resharerIndexes.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 ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Resharer completes", + 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( + children: [ + if (myResharerComplete != null) + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myResharerComplete!, + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + if (myResharerComplete != null) const _Div(), + if (myResharerComplete != null) + DetailItem( + title: "My resharer complete", + detail: myResharerComplete!, + button: Util.isDesktop + ? IconCopyButton( + data: myResharerComplete!, + ) + : SimpleCopyButton( + data: myResharerComplete!, + ), + ), + if (!amOutgoingParticipant) const _Div(), + if (!amOutgoingParticipant) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharerIndexes.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostEncryptionKeyTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter index " + "${resharerIndexes[i]}" + "'s resharer complete", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Encryption Key Field Input.", + key: Key( + "frostEncryptionKeyClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", + key: Key( + "frostEncryptionKeyPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .text + .isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: Key("frostScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .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(), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: amOutgoingParticipant ? "Exit" : "Complete", + enabled: amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart new file mode 100644 index 000000000..94d2de0b2 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/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/constants.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'; + +final class BeginReshareConfigView extends ConsumerStatefulWidget { + const BeginReshareConfigView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/beginReshareConfigView"; + + final String walletId; + + @override + ConsumerState createState() => + _BeginReshareConfigViewState(); +} + +class _BeginReshareConfigViewState + extends ConsumerState { + late final int currentThreshold; + late final List currentParticipants; + + final Map pFrostResharersMap = {}; + + @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; + currentParticipants = frostInfo.participants; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + // title: Text( + // "Modify 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( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Select participants for resharing", + style: STextStyles.label(context), + ), + const SizedBox( + height: 16, + ), + Column( + children: [ + for (int i = 0; i < currentParticipants.length; i++) + Padding( + padding: const EdgeInsets.only( + top: 10, + ), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + if (pFrostResharersMap[currentParticipants[i]] == + null) { + pFrostResharersMap[currentParticipants[i]] = i; + } else { + pFrostResharersMap.remove(currentParticipants[i]); + } + + setState(() {}); + }, + child: Container( + color: Colors.transparent, + child: IgnorePointer( + child: Row( + children: [ + Checkbox( + value: pFrostResharersMap[ + currentParticipants[i]] == + i, + onChanged: (bool? value) {}, + ), + const SizedBox( + width: 10, + ), + Text( + currentParticipants[i], + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ), + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: pFrostResharersMap.length >= currentThreshold, + onPressed: () async { + await Navigator.of(context).pushNamed( + CompleteReshareConfigView.routeName, + arguments: ( + walletId: widget.walletId, + resharers: + pFrostResharersMap.values.toList(growable: false), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart new file mode 100644 index 000000000..0e2e1e111 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart @@ -0,0 +1,335 @@ +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/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_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/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/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/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 List resharers; + + @override + ConsumerState createState() => + _CompleteReshareConfigViewState(); +} + +class _CompleteReshareConfigViewState + extends ConsumerState { + final _newThresholdController = TextEditingController(); + final _newParticipantsCountController = TextEditingController(); + + final List controllers = []; + + int _participantsCount = 0; + + bool _buttonLock = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(widget.walletId)!; + final validationMessage = _validateInputData(); + + if (validationMessage != "valid") { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: validationMessage, + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + final config = Frost.createResharerConfig( + newThreshold: int.parse(_newThresholdController.text), + resharers: widget.resharers, + newParticipants: controllers.map((e) => e.text).toList(), + ); + + final salt = Format.uint8listToString( + resharerSalt(resharerConfig: config), + ); + + if (frostInfo.knownSalts.contains(salt)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Duplicate config salt", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } else { + final salts = frostInfo.knownSalts; + 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: salts), + ); + }); + } + + ref.read(pFrostResharingData).myName = frostInfo.myName; + ref.read(pFrostResharingData).resharerConfig = config; + + if (mounted) { + await Navigator.of(context).pushNamed( + DisplayReshareConfigView.routeName, + arguments: widget.walletId, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + String _validateInputData() { + final threshold = int.tryParse(_newThresholdController.text); + if (threshold == null) { + return "Choose a threshold"; + } + + final partsCount = int.tryParse(_newParticipantsCountController.text); + if (partsCount == null) { + return "Choose total number of participants"; + } + + if (threshold > partsCount) { + return "Threshold cannot be greater than the number of participants"; + } + + if (partsCount < 2) { + return "At least two participants required"; + } + + if (controllers.length != partsCount) { + return "Participants count error"; + } + + final hasEmptyParticipants = controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element); + if (hasEmptyParticipants) { + return "Participants must not be empty"; + } + + if (controllers.length != controllers.map((e) => e.text).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(() {}); + } + } + } + + @override + void dispose() { + _newThresholdController.dispose(); + _newParticipantsCountController.dispose(); + for (final e in controllers) { + e.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Modify 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( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "New threshold", + style: STextStyles.label(context), + ), + const SizedBox( + height: 10, + ), + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _newThresholdController, + ), + const SizedBox( + height: 16, + ), + Text( + "Number of participants", + style: STextStyles.label(context), + ), + const SizedBox( + height: 10, + ), + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _newParticipantsCountController, + onChanged: _participantsCountChanged, + ), + const SizedBox( + height: 16, + ), + if (controllers.isNotEmpty) + Text( + "Participants", + style: STextStyles.label(context), + ), + if (controllers.isNotEmpty) + const SizedBox( + height: 10, + ), + 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], + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Generate config", + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + await _onPressed(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart new file mode 100644 index 000000000..2b7f1f899 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart @@ -0,0 +1,214 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.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/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/custom_buttons/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class DisplayReshareConfigView extends ConsumerStatefulWidget { + const DisplayReshareConfigView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/displayReshareConfigView"; + + final String walletId; + + @override + ConsumerState createState() => + _DisplayReshareConfigViewState(); +} + +class _DisplayReshareConfigViewState + extends ConsumerState { + late final bool iAmInvolved; + + bool _buttonLock = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; + + final serializedKeys = await wallet.getSerializedKeys(); + if (mounted) { + final result = Frost.beginResharer( + serializedKeys: serializedKeys!, + config: ref.read(pFrostResharingData).resharerConfig!, + ); + + ref.read(pFrostResharingData).startResharerData = result; + + await Navigator.of(context).pushNamed( + BeginResharingView.routeName, + arguments: widget.walletId, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(widget.walletId)!; + + final myOldIndex = frostInfo.participants.indexOf(frostInfo.myName); + + iAmInvolved = ref + .read(pFrostResharingData) + .configData! + .resharers + .contains(myOldIndex); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Resharer config", + 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( + children: [ + if (!Util.isDesktop) const Spacer(), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostResharingData).resharerConfig!, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + DetailItem( + title: "Config", + detail: ref.watch(pFrostResharingData).resharerConfig!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostResharingData).resharerConfig!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostResharingData).resharerConfig!, + ), + ), + SizedBox( + height: Util.isDesktop ? 64 : 16, + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + if (iAmInvolved) + PrimaryButton( + label: "Start resharing", + onPressed: _onPressed, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart new file mode 100644 index 000000000..966a24710 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart @@ -0,0 +1,338 @@ +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'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_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/secure_store_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.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/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/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 ImportReshareConfigView extends ConsumerStatefulWidget { + const ImportReshareConfigView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/importReshareConfigView"; + + final String walletId; + + @override + ConsumerState createState() => + _ImportReshareConfigViewState(); +} + +class _ImportReshareConfigViewState + extends ConsumerState { + late final TextEditingController configFieldController; + late final FocusNode configFocusNode; + + bool _configEmpty = true; + + bool _buttonLock = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(widget.walletId)!; + + ref.read(pFrostResharingData).reset(); + ref.read(pFrostResharingData).myName = frostInfo.myName; + ref.read(pFrostResharingData).resharerConfig = configFieldController.text; + + String? salt; + try { + salt = Format.uint8listToString( + resharerSalt( + resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + ), + ); + } catch (_) { + throw Exception("Bad resharer config"); + } + + if (frostInfo.knownSalts.contains(salt)) { + throw Exception("Duplicate config salt"); + } else { + final salts = frostInfo.knownSalts; + 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: salts), + ); + }); + } + + final serializedKeys = await ref.read(secureStoreProvider).read( + key: "{${widget.walletId}}_serializedFROSTKeys", + ); + if (mounted) { + final result = Frost.beginResharer( + serializedKeys: serializedKeys!, + config: ref.read(pFrostResharingData).resharerConfig!, + ); + + ref.read(pFrostResharingData).startResharerData = result; + + await Navigator.of(context).pushNamed( + BeginResharingView.routeName, + arguments: widget.walletId, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + configFieldController = TextEditingController(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + configFieldController.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + title: Text( + "Import FROST reshare config", + 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("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start resharing", + enabled: !_configEmpty, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + await _onPressed(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart new file mode 100644 index 000000000..90218529f --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart @@ -0,0 +1,439 @@ +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_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/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/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/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/custom_buttons/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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 BeginResharingView extends ConsumerStatefulWidget { + const BeginResharingView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/beginResharingView"; + + final String walletId; + + @override + ConsumerState createState() => _BeginResharingViewState(); +} + +class _BeginResharingViewState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List resharerIndexes; + late final int myResharerIndexIndex; + late final String myResharerStart; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + if (!amOutgoingParticipant) { + // collect resharer strings + final resharerStarts = controllers.map((e) => e.text).toList(); + if (myResharerIndexIndex >= 0) { + // only insert my own at the correct index if I am a resharer + resharerStarts.insert(myResharerIndexIndex, myResharerStart); + } + + final result = Frost.beginReshared( + myName: ref.read(pFrostResharingData).myName!, + resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + resharerStarts: resharerStarts, + ); + + ref.read(pFrostResharingData).startResharedData = result; + } + await Navigator.of(context).pushNamed( + ContinueResharingView.routeName, + arguments: widget.walletId, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } 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(widget.walletId)!; + final myOldIndex = + frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); + + myResharerStart = + ref.read(pFrostResharingData).startResharerData!.resharerStart; + + resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; + myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); + if (myResharerIndexIndex >= 0) { + // remove my name for now as we don't need a text field for it + resharerIndexes.removeAt(myResharerIndexIndex); + } + + amOutgoingParticipant = !ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!); + + for (int i = 0; i < resharerIndexes.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 WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: Util.isDesktop + ? DesktopWalletView.routeName + : WalletView.routeName, + ), + ); + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: DesktopWalletView.routeName, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: WalletView.routeName, + ), + ); + }, + ), + title: Text( + "Resharers", + 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( + children: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myResharerStart, + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const _Div(), + DetailItem( + title: "My resharer", + detail: myResharerStart, + button: Util.isDesktop + ? IconCopyButton( + data: myResharerStart, + ) + : SimpleCopyButton( + data: myResharerStart, + ), + ), + const _Div(), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharerIndexes.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostResharerTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter index " + "${resharerIndexes[i]}" + "'s resharer", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Resharer Field Input.", + key: Key( + "frostResharerClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Resharer Field Input.", + key: Key( + "frostResharerPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .text + .isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: Key( + "frostCommitmentsScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .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(), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Continue", + enabled: amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart new file mode 100644 index 000000000..75359d266 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart @@ -0,0 +1,429 @@ +import 'dart:ffi'; + +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_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/my_stack_view/wallet_view/desktop_wallet_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/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/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/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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 ContinueResharingView extends ConsumerStatefulWidget { + const ContinueResharingView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/continueResharingView"; + + final String walletId; + + @override + ConsumerState createState() => + _ContinueResharingViewState(); +} + +class _ContinueResharingViewState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List newParticipants; + late final int myIndex; + late final String? myEncryptionKey; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // collect encryptionKeys strings and insert my own at the correct index + final encryptionKeys = controllers.map((e) => e.text).toList(); + if (!amOutgoingParticipant) { + encryptionKeys.insert(myIndex, myEncryptionKey!); + } + + final result = Frost.finishResharer( + machine: ref.read(pFrostResharingData).startResharerData!.machine.ref, + encryptionKeysOfResharedTo: encryptionKeys, + ); + + ref.read(pFrostResharingData).resharerComplete = result; + + await Navigator.of(context).pushNamed( + FinishResharingView.routeName, + arguments: widget.walletId, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } 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 WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: Util.isDesktop + ? DesktopWalletView.routeName + : WalletView.routeName, + ), + ); + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: DesktopWalletView.routeName, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: WalletView.routeName, + ), + ); + }, + ), + title: Text( + "Encryption keys", + 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( + children: [ + if (!amOutgoingParticipant) + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myEncryptionKey!, + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + if (!amOutgoingParticipant) const _Div(), + if (!amOutgoingParticipant) + DetailItem( + title: "My encryption key", + detail: myEncryptionKey!, + button: Util.isDesktop + ? IconCopyButton( + data: myEncryptionKey!, + ) + : SimpleCopyButton( + data: myEncryptionKey!, + ), + ), + if (!amOutgoingParticipant) const _Div(), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < newParticipants.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostEncryptionKeyTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter " + "${newParticipants[i]}" + "'s encryption key", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Encryption Key Field Input.", + key: Key( + "frostEncryptionKeyClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", + key: Key( + "frostEncryptionKeyPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .text + .isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: Key( + "frostCommitmentsScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .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(), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Continue", + enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart new file mode 100644 index 000000000..86ff2ebe0 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.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/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/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; + +class NewContinueSharingView extends ConsumerStatefulWidget { + const NewContinueSharingView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/NewContinueSharingView"; + + final String walletId; + + @override + ConsumerState createState() => + _NewContinueSharingViewState(); +} + +class _NewContinueSharingViewState + extends ConsumerState { + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: + Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, + ), + ); + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: HomeView.routeName, + ), + ); + }, + ), + title: Text( + "Encryption keys", + 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( + children: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref + .watch(pFrostResharingData) + .startResharedData! + .resharedStart, + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const _Div(), + 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, + ), + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Continue", + onPressed: () { + Navigator.of(context).pushNamed( + FinishResharingView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart new file mode 100644 index 000000000..f3ef1ec0b --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart @@ -0,0 +1,426 @@ +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:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.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/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/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/models/incomplete_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/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 NewImportResharerConfigView extends ConsumerStatefulWidget { + const NewImportResharerConfigView({ + super.key, + required this.walletName, + required this.coin, + }); + + static const String routeName = "/newImportResharerConfigView"; + + final String walletName; + final Coin coin; + + @override + ConsumerState createState() => + _NewImportResharerConfigViewState(); +} + +class _NewImportResharerConfigViewState + extends ConsumerState { + late final TextEditingController myNameFieldController, configFieldController; + late final FocusNode myNameFocusNode, configFocusNode; + + bool _nameEmpty = true, _configEmpty = true; + + bool _buttonLock = false; + + Future _createWallet() async { + final info = WalletInfo.createNew( + name: widget.walletName, + coin: widget.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 ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Import FROST reshare config", + 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: myNameFieldController, + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, + focusNode: myNameFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "My name", + myNameFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _nameEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_nameEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frMyNameClearButtonKey"), + onTap: () { + myNameFieldController.text = ""; + + setState(() { + _nameEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Name Field.", + key: const Key("frMyNamePasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + myNameFieldController.text = + data.text!.trim(); + } + + setState(() { + _nameEmpty = + myNameFieldController.text.isEmpty; + }); + }, + child: _nameEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start", + enabled: !_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).resharerConfig = + configFieldController.text; + + if (!ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!)) { + ref.read(pFrostResharingData).reset(); + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "My name not found in config participants", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + Exception? ex; + final wallet = await showLoading( + whileFuture: _createWallet(), + context: context, + message: "Setting up wallet", + isDesktop: Util.isDesktop, + onException: (e) => ex = e, + ); + + if (ex != null) { + throw ex!; + } + + if (mounted) { + ref.read(pFrostResharingData).incompleteWallet = wallet!; + await Navigator.of(context).pushNamed( + NewStartResharingView.routeName, + arguments: wallet.walletId, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart new file mode 100644 index 000000000..fb6107c2c --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart @@ -0,0 +1,379 @@ +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:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.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/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/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/frost_interruption_dialog.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 NewStartResharingView extends ConsumerStatefulWidget { + const NewStartResharingView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/newStartResharingView"; + + final String walletId; + + @override + ConsumerState createState() => + _NewStartResharingViewState(); +} + +class _NewStartResharingViewState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List resharerIndexes; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // collect resharer strings + final resharerStarts = controllers.map((e) => e.text).toList(); + + final result = Frost.beginReshared( + myName: ref.read(pFrostResharingData).myName!, + resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + resharerStarts: resharerStarts, + ); + + ref.read(pFrostResharingData).startResharedData = result; + + await Navigator.of(context).pushNamed( + NewContinueSharingView.routeName, + arguments: widget.walletId, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; + + for (int i = 0; i < resharerIndexes.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 WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: + Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, + ), + ); + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: DesktopHomeView.routeName, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: HomeView.routeName, + ), + ); + }, + ), + title: Text( + "Resharers", + 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( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharerIndexes.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostResharerTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter index " + "${resharerIndexes[i]}" + "'s resharer", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Resharer Field Input.", + key: Key( + "frostResharerClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Resharer Field Input.", + key: Key( + "frostResharerPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .text + .isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: Key( + "frostCommitmentsScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .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(), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Continue", + enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart new file mode 100644 index 000000000..85d02c0ff --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart @@ -0,0 +1,315 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/exit_to_my_stack_button.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/themes/stack_colors.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/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/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class VerifyUpdatedWalletView extends ConsumerStatefulWidget { + const VerifyUpdatedWalletView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/verifyUpdatedWalletView"; + + final String walletId; + + @override + ConsumerState createState() => + _VerifyUpdatedWalletViewState(); +} + +class _VerifyUpdatedWalletViewState + extends ConsumerState { + late final String config; + late final String serializedKeys; + late final String reshareId; + + late final bool isNew; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + Exception? ex; + + final BitcoinFrostWallet wallet; + + if (isNew) { + wallet = await ref + .read(pFrostResharingData) + .incompleteWallet! + .toBitcoinFrostWallet( + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + ); + + await wallet.info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + ref.read(pWallets).addWallet(wallet); + } else { + wallet = + ref.read(pWallets).getWallet(widget.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", + isDesktop: Util.isDesktop, + onException: (e) => ex = e, + ); + + if (ex != null) { + throw ex!; + } + + if (mounted) { + ref.read(pFrostResharingData).reset(); + + Navigator.of(context).popUntil( + ModalRoute.withName( + _popUntilPath, + ), + ); + } + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } 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 == + widget.walletId; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: _popUntilPath, + ), + ); + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: _popUntilPath, + ), + ); + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: _popUntilPath, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.resharing, + popUntilOnYesRouteName: _popUntilPath, + ), + ); + }, + ), + ), + 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( + children: [ + Text( + "Ensure your reshare ID matches that of each other participant", + style: STextStyles.pageTitleH2(context), + ), + const _Div(), + DetailItem( + title: "ID", + detail: reshareId, + button: Util.isDesktop + ? IconCopyButton( + data: reshareId, + ) + : SimpleCopyButton( + data: reshareId, + ), + ), + const _Div(), + const _Div(), + Text( + "Back up your keys and config", + style: STextStyles.pageTitleH2(context), + ), + const _Div(), + DetailItem( + title: "Config", + detail: config, + button: Util.isDesktop + ? IconCopyButton( + data: config, + ) + : SimpleCopyButton( + data: config, + ), + ), + const _Div(), + DetailItem( + title: "Keys", + detail: serializedKeys, + button: Util.isDesktop + ? IconCopyButton( + data: serializedKeys, + ) + : SimpleCopyButton( + data: serializedKeys, + ), + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Confirm", + onPressed: _onPressed, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/providers/frost_wallet/frost_wallet_providers.dart b/lib/providers/frost_wallet/frost_wallet_providers.dart new file mode 100644 index 000000000..3b181b7b8 --- /dev/null +++ b/lib/providers/frost_wallet/frost_wallet_providers.dart @@ -0,0 +1,103 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frostdart/frostdart_bindings_generated.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; + +// =================== wallet creation ========================================= +final pFrostMultisigConfig = StateProvider((ref) => null); +final pFrostMyName = StateProvider((ref) => null); + +final pFrostStartKeyGenData = StateProvider< + ({ + String seed, + String commitments, + Pointer multisigConfigWithNamePtr, + Pointer secretShareMachineWrapperPtr, + })?>((_) => null); + +final pFrostSecretSharesData = StateProvider< + ({ + String share, + Pointer secretSharesResPtr, + })?>((ref) => null); + +final pFrostCompletedKeyGenData = StateProvider< + ({ + Uint8List multisigId, + String recoveryString, + String serializedKeys, + })?>((ref) => null); + +// ================= transaction creation ====================================== +final pFrostTxData = StateProvider((ref) => null); + +final pFrostAttemptSignData = StateProvider< + ({ + Pointer machinePtr, + String preprocess, + })?>((ref) => null); + +final pFrostContinueSignData = StateProvider< + ({ + Pointer machinePtr, + String share, + })?>((ref) => null); + +// ===================== shared/util =========================================== +final pFrostSelectParticipantsUnordered = + StateProvider?>((ref) => null); + +// ========================= resharing ========================================= +final pFrostResharingData = Provider((ref) => _ResharingData()); + +class _ResharingData { + String? myName; + + IncompleteFrostWallet? incompleteWallet; + + // resharer encoded config string + String? resharerConfig; + ({ + int newThreshold, + List resharers, + List newParticipants, + })? get configData => resharerConfig != null + ? Frost.extractResharerConfigData(resharerConfig: resharerConfig!) + : null; + + // resharer start string (for sharing) and machine + ({ + String resharerStart, + Pointer machine, + })? startResharerData; + + // reshared start string (for sharing) and machine + ({ + String resharedStart, + Pointer prior, + })? startResharedData; + + // resharer complete string (for sharing) + String? resharerComplete; + + // new keys and config with an ID + ({ + String multisigConfig, + String serializedKeys, + String resharedId, + })? newWalletData; + + // reset/clear all data + void reset() { + resharerConfig = null; + startResharerData = null; + startResharedData = null; + resharerComplete = null; + newWalletData = null; + incompleteWallet = null; + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index a046cc01d..faf8967e8 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -26,6 +26,13 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; @@ -113,6 +120,19 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_pr import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; @@ -423,6 +443,319 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case CreateNewFrostMsWalletView.routeName: + if (args is ({ + String walletName, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => CreateNewFrostMsWalletView( + walletName: args.walletName, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case RestoreFrostMsWalletView.routeName: + if (args is ({ + String walletName, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => RestoreFrostMsWalletView( + walletName: args.walletName, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case ShareNewMultisigConfigView.routeName: + if (args is ({ + String walletName, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ShareNewMultisigConfigView( + walletName: args.walletName, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case ImportNewFrostMsWalletView.routeName: + if (args is ({ + String walletName, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ImportNewFrostMsWalletView( + walletName: args.walletName, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case NewImportResharerConfigView.routeName: + if (args is ({ + String walletName, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => NewImportResharerConfigView( + walletName: args.walletName, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case NewStartResharingView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => NewStartResharingView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case NewContinueSharingView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => NewContinueSharingView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FrostShareCommitmentsView.routeName: + if (args is ({ + String walletName, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostShareCommitmentsView( + walletName: args.walletName, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FrostShareSharesView.routeName: + if (args is ({ + String walletName, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostShareSharesView( + walletName: args.walletName, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case ConfirmNewFrostMSWalletCreationView.routeName: + if (args is ({ + String walletName, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ConfirmNewFrostMSWalletCreationView( + walletName: args.walletName, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FrostMSWalletOptionsView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostMSWalletOptionsView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FrostParticipantsView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostParticipantsView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case ImportReshareConfigView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ImportReshareConfigView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case BeginReshareConfigView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => BeginReshareConfigView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case CompleteReshareConfigView.routeName: + if (args is ({String walletId, List resharers})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => CompleteReshareConfigView( + walletId: args.walletId, + resharers: args.resharers, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case DisplayReshareConfigView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => DisplayReshareConfigView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case BeginResharingView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => BeginResharingView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case ContinueResharingView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ContinueResharingView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FinishResharingView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FinishResharingView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case VerifyUpdatedWalletView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => VerifyUpdatedWalletView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // case MonkeyLoadedView.routeName: // if (args is Tuple2>) { // return getRoute( diff --git a/lib/themes/coin_card_provider.dart b/lib/themes/coin_card_provider.dart index b34e9e6f1..ce5e71038 100644 --- a/lib/themes/coin_card_provider.dart +++ b/lib/themes/coin_card_provider.dart @@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; final coinCardProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); + // TODO: handle this differently by adding proper frost assets to themes + if (coin == Coin.bitcoinFrost) { + coin = Coin.bitcoin; + } else if (coin == Coin.bitcoinFrostTestNet) { + coin = Coin.bitcoinTestNet; + } + if (assets is ThemeAssetsV3) { return assets.coinCardImages?[coin.mainNetVersion]; } else { @@ -26,6 +33,13 @@ final coinCardProvider = Provider.family((ref, coin) { final coinCardFavoritesProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); + // TODO: handle this differently by adding proper frost assets to themes + if (coin == Coin.bitcoinFrost) { + coin = Coin.bitcoin; + } else if (coin == Coin.bitcoinFrostTestNet) { + coin = Coin.bitcoinTestNet; + } + if (assets is ThemeAssetsV3) { return assets.coinCardFavoritesImages?[coin.mainNetVersion] ?? assets.coinCardImages?[coin.mainNetVersion]; diff --git a/lib/themes/coin_icon_provider.dart b/lib/themes/coin_icon_provider.dart index 9bd3990bb..f0c0df842 100644 --- a/lib/themes/coin_icon_provider.dart +++ b/lib/themes/coin_icon_provider.dart @@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; final coinIconProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); + // TODO: handle this differently by adding proper frost assets to themes + if (coin == Coin.bitcoinFrost) { + coin = Coin.bitcoin; + } else if (coin == Coin.bitcoinFrostTestNet) { + coin = Coin.bitcoinTestNet; + } + if (assets is ThemeAssets) { switch (coin) { case Coin.bitcoin: diff --git a/lib/themes/coin_image_provider.dart b/lib/themes/coin_image_provider.dart index 6ca839fb9..fa1fdde4c 100644 --- a/lib/themes/coin_image_provider.dart +++ b/lib/themes/coin_image_provider.dart @@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; final coinImageProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); + // TODO: handle this differently by adding proper frost assets to themes + if (coin == Coin.bitcoinFrost) { + coin = Coin.bitcoin; + } else if (coin == Coin.bitcoinFrostTestNet) { + coin = Coin.bitcoinTestNet; + } + if (assets is ThemeAssets) { switch (coin) { case Coin.bitcoin: @@ -64,6 +71,13 @@ final coinImageProvider = Provider.family((ref, coin) { final coinImageSecondaryProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); + // TODO: handle this differently by adding proper frost assets to themes + if (coin == Coin.bitcoinFrost) { + coin = Coin.bitcoin; + } else if (coin == Coin.bitcoinFrostTestNet) { + coin = Coin.bitcoinTestNet; + } + if (assets is ThemeAssets) { switch (coin) { case Coin.bitcoin: diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 305183448..0356c1e7c 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -297,6 +297,17 @@ extension CoinExt on Coin { } } + bool get isFrost { + switch (this) { + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + return true; + + default: + return false; + } + } + Coin get mainNetVersion { switch (this) { case Coin.bitcoin: @@ -495,6 +506,15 @@ Coin coinFromPrettyName(String name) { case "tStellar": return Coin.stellarTestnet; + case "Bitcoin Frost": + case "bitcoinFrost": + return Coin.bitcoinFrost; + + case "Bitcoin Frost Testnet": + case "tBitcoin Frost": + case "bitcoinFrostTestNet": + return Coin.bitcoinFrostTestNet; + default: throw ArgumentError.value( name, diff --git a/lib/wallets/models/incomplete_frost_wallet.dart b/lib/wallets/models/incomplete_frost_wallet.dart new file mode 100644 index 000000000..e5075da63 --- /dev/null +++ b/lib/wallets/models/incomplete_frost_wallet.dart @@ -0,0 +1,42 @@ +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; + +class IncompleteFrostWallet { + WalletInfo? info; + + String? get walletId => info?.walletId; + + Future toBitcoinFrostWallet({ + required MainDB mainDB, + required SecureStorageInterface secureStorageInterface, + required NodeService nodeService, + required Prefs prefs, + }) async { + final wallet = await Wallet.create( + walletInfo: info!, + mainDB: mainDB, + secureStorageInterface: secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + ); + + // dummy entry so updaters work when `wallet.updateWithResharedData` is called + final frostInfo = FrostWalletInfo( + walletId: info!.walletId, + knownSalts: [], + participants: [], + myName: "", + threshold: -1, + ); + + await mainDB.isar.frostWalletInfo.put(frostInfo); + + return wallet as BitcoinFrostWallet; + } +} diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 102beb1e3..4f1c3febf 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -171,7 +171,7 @@ class BitcoinFrostWallet extends Wallet { } } - final serializedKeys = await _getSerializedKeys(); + final serializedKeys = await getSerializedKeys(); final keys = frost.deserializeKeys(keys: serializedKeys!); final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main @@ -213,7 +213,7 @@ class BitcoinFrostWallet extends Wallet { final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main ? Network.Mainnet : Network.Testnet; - final serializedKeys = await _getSerializedKeys(); + final serializedKeys = await getSerializedKeys(); return Frost.attemptSignConfig( network: network, @@ -859,7 +859,7 @@ class BitcoinFrostWallet extends Wallet { // =================== Secure storage ======================================== - Future _getSerializedKeys() async => + Future getSerializedKeys() async => await secureStorageInterface.read( key: "{$walletId}_serializedFROSTKeys", ); @@ -867,7 +867,7 @@ class BitcoinFrostWallet extends Wallet { Future _saveSerializedKeys( String keys, ) async { - final current = await _getSerializedKeys(); + final current = await getSerializedKeys(); if (current == null) { // do nothing diff --git a/lib/widgets/detail_item.dart b/lib/widgets/detail_item.dart new file mode 100644 index 000000000..75e0e6a1e --- /dev/null +++ b/lib/widgets/detail_item.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DetailItem extends StatelessWidget { + const DetailItem({ + Key? key, + required this.title, + required this.detail, + this.button, + this.showEmptyDetail = true, + this.disableSelectableText = false, + }) : super(key: key); + + final String title; + final String detail; + final Widget? button; + final bool showEmptyDetail; + final bool disableSelectableText; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => RoundedWhiteContainer( + child: child, + ), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + disableSelectableText + ? Text( + title, + style: STextStyles.itemSubtitle(context), + ) + : SelectableText( + title, + style: STextStyles.itemSubtitle(context), + ), + button ?? Container(), + ], + ), + const SizedBox( + height: 5, + ), + detail.isEmpty && showEmptyDetail + ? disableSelectableText + ? Text( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ) + : SelectableText( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ) + : disableSelectableText + ? Text( + detail, + style: STextStyles.w500_14(context), + ) + : SelectableText( + detail, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/dialogs/frost_interruption_dialog.dart b/lib/widgets/dialogs/frost_interruption_dialog.dart new file mode 100644 index 000000000..4d36456bb --- /dev/null +++ b/lib/widgets/dialogs/frost_interruption_dialog.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +enum FrostInterruptionDialogType { + walletCreation, + resharing, + transactionCreation; +} + +class FrostInterruptionDialog extends StatelessWidget { + const FrostInterruptionDialog({ + super.key, + required this.type, + required this.popUntilOnYesRouteName, + this.onNoPressedOverride, + this.onYesPressedOverride, + }); + + final FrostInterruptionDialogType type; + final String popUntilOnYesRouteName; + final VoidCallback? onNoPressedOverride; + final VoidCallback? onYesPressedOverride; + + String get message { + switch (type) { + case FrostInterruptionDialogType.walletCreation: + return "wallet creation"; + case FrostInterruptionDialogType.resharing: + return "resharing"; + case FrostInterruptionDialogType.transactionCreation: + return "transaction signing"; + } + } + + @override + Widget build(BuildContext context) { + return StackDialog( + title: "Cancel $message process", + message: "Are you sure you want to cancel the $message process?", + leftButton: SecondaryButton( + label: "No", + onPressed: onNoPressedOverride ?? + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, + ), + rightButton: PrimaryButton( + label: "Yes", + onPressed: onYesPressedOverride ?? + () { + // pop dialog + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop(); + + Navigator.of(context).popUntil( + ModalRoute.withName( + popUntilOnYesRouteName, + ), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/stack_dialog.dart b/lib/widgets/stack_dialog.dart index be7f22ed9..bc247c2f2 100644 --- a/lib/widgets/stack_dialog.dart +++ b/lib/widgets/stack_dialog.dart @@ -147,8 +147,10 @@ class StackOkDialog extends StatelessWidget { this.icon, required this.title, this.message, + this.desktopPopRootNavigator = false, }) : super(key: key); + final bool desktopPopRootNavigator; final Widget? leftButton; final void Function(String)? onOkPressed; @@ -208,9 +210,13 @@ class StackOkDialog extends StatelessWidget { onOkPressed?.call("OK"); } : () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - // onOkPressed?.call("OK"); + if (desktopPopRootNavigator) { + Navigator.of(context, rootNavigator: true).pop(); + } else { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + // onOkPressed?.call("OK"); + } }, style: Theme.of(context) .extension()! From 9deb8a5d0c4688f434c640a5ea337e836652b79d Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Fri, 19 Jan 2024 20:16:01 -0700 Subject: [PATCH 010/272] Update version (v1.9.1, build 200) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9a243905b..1355dbd4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.9.0+199 +version: 1.9.1+200 environment: sdk: ">=3.0.2 <4.0.0" From 444afb88ae90b504921d2eada0eceeb397f3813a Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Jan 2024 18:33:40 -0600 Subject: [PATCH 011/272] WIP frost send --- .../frost_attempt_sign_config_view.dart | 405 ++++++++++++ .../frost_ms/frost_complete_sign_view.dart | 206 ++++++ .../frost_continue_sign_config_view.dart | 445 +++++++++++++ .../frost_create_sign_config_view.dart | 180 ++++++ .../frost_import_sign_config_view.dart | 330 ++++++++++ .../send_view/frost_ms/frost_send_view.dart | 602 ++++++++++++++++++ lib/pages/send_view/frost_ms/recipient.dart | 501 +++++++++++++++ lib/pages/wallet_view/wallet_view.dart | 13 +- .../wallet_view/sub_widgets/my_wallet.dart | 49 +- lib/route_generator.dart | 18 + .../wallet/impl/bitcoin_frost_wallet.dart | 6 + lib/wallets/wallet/wallet.dart | 3 + 12 files changed, 2746 insertions(+), 12 deletions(-) create mode 100644 lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart create mode 100644 lib/pages/send_view/frost_ms/frost_complete_sign_view.dart create mode 100644 lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart create mode 100644 lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart create mode 100644 lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart create mode 100644 lib/pages/send_view/frost_ms/frost_send_view.dart create mode 100644 lib/pages/send_view/frost_ms/recipient.dart diff --git a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart new file mode 100644 index 000000000..64b33e9f9 --- /dev/null +++ b/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart @@ -0,0 +1,405 @@ +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_continue_sign_config_view.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/coins/bitcoin/frost_wallet.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/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/simple_copy_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/detail_item.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 FrostAttemptSignConfigView extends ConsumerStatefulWidget { + const FrostAttemptSignConfigView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/frostAttemptSignConfigView"; + + final String walletId; + + @override + ConsumerState createState() => + _FrostAttemptSignConfigViewState(); +} + +class _FrostAttemptSignConfigViewState + extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final String myName; + late final List participantsWithoutMe; + late final String myPreprocess; + late final int myIndex; + late final int threshold; + + final List fieldIsEmptyFlags = []; + + bool hasEnoughPreprocesses() { + // 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 >= threshold; + } + + @override + void initState() { + final wallet = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as FrostWallet; + + myName = wallet.myName; + threshold = wallet.threshold; + participantsWithoutMe = wallet.participants; + myIndex = participantsWithoutMe.indexOf(wallet.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 ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Preprocesses", + 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: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myPreprocess, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const _Div(), + DetailItem( + title: "My name", + detail: myName, + ), + const _Div(), + DetailItem( + title: "My preprocess", + detail: myPreprocess, + button: Util.isDesktop + ? IconCopyButton( + data: myPreprocess, + ) + : SimpleCopyButton( + data: myPreprocess, + ), + ), + const _Div(), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participantsWithoutMe.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostPreprocessesTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter ${participantsWithoutMe[i]}'s preprocess", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Preprocess Field Input.", + key: Key( + "frostPreprocessesClearButtonKey_$i", + ), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Preprocess Field Input.", + key: Key( + "frostPreprocessesPasteButtonKey_$i", + ), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .text + .isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: Key( + "frostPreprocessesScanQrButtonKey_$i", + ), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Continue signing", + enabled: hasEnoughPreprocesses(), + onPressed: () async { + // collect Preprocess strings (not including my own) + final preprocesses = controllers.map((e) => e.text).toList(); + + // collect participants who are involved in this transaction + final List requiredParticipantsUnordered = []; + for (int i = 0; i < participantsWithoutMe.length; i++) { + if (preprocesses[i].isNotEmpty) { + requiredParticipantsUnordered.add(participantsWithoutMe[i]); + } + } + ref.read(pFrostSelectParticipantsUnordered.notifier).state = + requiredParticipantsUnordered; + + // insert an empty string at my index + preprocesses.insert(myIndex, ""); + + try { + ref.read(pFrostContinueSignData.notifier).state = + Frost.continueSigning( + machinePtr: + ref.read(pFrostAttemptSignData.state).state!.machinePtr, + preprocesses: preprocesses, + ); + + await Navigator.of(context).pushNamed( + FrostContinueSignView.routeName, + arguments: widget.walletId, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to continue signing", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + }, + ), + ], + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart b/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart new file mode 100644 index 000000000..c63dca04d --- /dev/null +++ b/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.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/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/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/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/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostCompleteSignView extends ConsumerStatefulWidget { + const FrostCompleteSignView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/frostCompleteSignView"; + + final String walletId; + + @override + ConsumerState createState() => + _FrostCompleteSignViewState(); +} + +class _FrostCompleteSignViewState extends ConsumerState { + bool _broadcastLock = false; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Preview transaction", + 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: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostTxData.state).state!.raw!, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const _Div(), + DetailItem( + title: "Raw transaction hex", + detail: ref.watch(pFrostTxData.state).state!.raw!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostTxData.state).state!.raw!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostTxData.state).state!.raw!, + ), + ), + const _Div(), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Broadcast Transaction", + onPressed: () async { + if (_broadcastLock) { + return; + } + _broadcastLock = true; + + try { + Exception? ex; + final txData = await showLoading( + whileFuture: ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .confirmSend( + txData: ref.read(pFrostTxData.state).state!, + ), + context: context, + message: "Broadcasting transaction to network", + isDesktop: Util.isDesktop, + onException: (e) { + ex = e; + }, + ); + + if (ex != null) { + throw ex!; + } + + if (mounted) { + if (txData != null) { + ref.read(pFrostTxData.state).state = txData; + Navigator.of(context).popUntil( + ModalRoute.withName( + Util.isDesktop + ? MyStackView.routeName + : WalletView.routeName, + ), + ); + } + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Broadcast error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _broadcastLock = false; + } + }, + ), + ], + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart new file mode 100644 index 000000000..9d6494c62 --- /dev/null +++ b/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart @@ -0,0 +1,445 @@ +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_complete_sign_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/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/bitcoin/frost_wallet.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/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/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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 FrostContinueSignView extends ConsumerStatefulWidget { + const FrostContinueSignView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/frostContinueSignView"; + + final String walletId; + + @override + ConsumerState createState() => + _FrostContinueSignViewState(); +} + +class _FrostContinueSignViewState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final String myName; + late final List participantsWithoutMe; + late final List participantsAll; + late final String myShare; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + @override + void initState() { + final wallet = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as FrostWallet; + + myName = wallet.myName; + participantsAll = wallet.participants; + myIndex = wallet.participants.indexOf(wallet.myName); + myShare = ref.read(pFrostContinueSignData.state).state!.share; + + participantsWithoutMe = wallet.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 WillPopScope( + onWillPop: () async { + await showDialog( + context: context, + builder: (_) => FrostInterruptionDialog( + type: FrostInterruptionDialogType.transactionCreation, + popUntilOnYesRouteName: Util.isDesktop + ? DesktopWalletView.routeName + : WalletView.routeName, + ), + ); + return false; + }, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.transactionCreation, + popUntilOnYesRouteName: DesktopWalletView.routeName, + ), + ); + }, + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const FrostInterruptionDialog( + type: FrostInterruptionDialogType.transactionCreation, + popUntilOnYesRouteName: WalletView.routeName, + ), + ); + }, + ), + title: Text( + "Shares", + 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: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myShare, + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const _Div(), + DetailItem( + title: "My name", + detail: myName, + ), + const _Div(), + DetailItem( + title: "My shares", + detail: myShare, + button: Util.isDesktop + ? IconCopyButton( + data: myShare, + ) + : SimpleCopyButton( + data: myShare, + ), + ), + const _Div(), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participantsWithoutMe.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostSharesTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${participantsWithoutMe[i]}'s share", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears " + "The Share Field Input.", + key: Key( + "frostSharesClearButtonKey_$i", + ), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From " + "Clipboard To Share Field Input.", + key: Key( + "frostSharesPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .text + .isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera " + "For Scanning QR Code.", + key: Key( + "frostSharesScanQrButtonKey_$i", + ), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .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(), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const _Div(), + PrimaryButton( + label: "Complete signing", + onPressed: () async { + // check for empty shares + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing Shares", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect Share strings + final sharesCollected = + controllers.map((e) => e.text).toList(); + + final List shares = []; + for (final participant in participantsAll) { + if (participantsWithoutMe.contains(participant)) { + shares.add(sharesCollected[ + participantsWithoutMe.indexOf(participant)]); + } else { + shares.add(""); + } + } + + try { + final rawTx = Frost.completeSigning( + machinePtr: ref + .read(pFrostContinueSignData.state) + .state! + .machinePtr, + shares: shares, + ); + + ref.read(pFrostTxData.state).state = + ref.read(pFrostTxData.state).state!.copyWith( + raw: rawTx, + ); + + await Navigator.of(context).pushNamed( + FrostCompleteSignView.routeName, + arguments: widget.walletId, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to complete signing process", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + }, + ), + ], + ), + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 12, + ); + } +} diff --git a/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart new file mode 100644 index 000000000..104160a76 --- /dev/null +++ b/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.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/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/simple_copy_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/detail_item.dart'; + +class FrostCreateSignConfigView extends ConsumerStatefulWidget { + const FrostCreateSignConfigView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/frostCreateSignConfigView"; + + final String walletId; + + @override + ConsumerState createState() => + _FrostCreateSignConfigViewState(); +} + +class _FrostCreateSignConfigViewState + extends ConsumerState { + bool _attemptSignLock = false; + + Future _attemptSign() async { + if (_attemptSignLock) { + return; + } + + _attemptSignLock = true; + + try { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; + + final attemptSignRes = await wallet.frostAttemptSignConfig( + config: ref.read(pFrostTxData.state).state!.frostMSConfig!, + ); + + ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; + + await Navigator.of(context).pushNamed( + FrostAttemptSignConfigView.routeName, + arguments: widget.walletId, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Error, + ); + } finally { + _attemptSignLock = false; + } + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Sign config", + 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.symmetric(horizontal: 16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + children: [ + if (!Util.isDesktop) const Spacer(), + SizedBox( + height: MediaQuery.of(context).size.width - 32, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + size: MediaQuery.of(context).size.width - 32, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + DetailItem( + title: "Encoded 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 ? 64 : 16, + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + PrimaryButton( + label: "Attempt sign", + onPressed: () { + _attemptSign(); + }, + ), + const SizedBox( + height: 16, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart new file mode 100644 index 000000000..b390e0b67 --- /dev/null +++ b/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart @@ -0,0 +1,330 @@ +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:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_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/constants.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/models/tx_data.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/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 FrostImportSignConfigView extends ConsumerStatefulWidget { + const FrostImportSignConfigView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/frostImportSignConfigView"; + + final String walletId; + + @override + ConsumerState createState() => + _FrostImportSignConfigViewState(); +} + +class _FrostImportSignConfigViewState + extends ConsumerState { + late final TextEditingController configFieldController; + late final FocusNode configFocusNode; + + bool _configEmpty = true; + + bool _attemptSignLock = false; + + Future _attemptSign() async { + if (_attemptSignLock) { + return; + } + + _attemptSignLock = true; + + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final config = configFieldController.text; + final wallet = + ref.read(pWallets).getWallet(widget.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, + utxos: utxos.toSet(), + ); + + final attemptSignRes = await wallet.frostAttemptSignConfig( + config: ref.read(pFrostTxData.state).state!.frostMSConfig!, + ); + + ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; + + await Navigator.of(context).pushNamed( + FrostAttemptSignConfigView.routeName, + arguments: widget.walletId, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Error, + ); + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Import and attempt sign config failed", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _attemptSignLock = false; + } + } + + @override + void initState() { + configFieldController = TextEditingController(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + configFieldController.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Import FROST sign config", + 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("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start signing", + enabled: !_configEmpty, + onPressed: () { + _attemptSign(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart new file mode 100644 index 000000000..cf64d8e54 --- /dev/null +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -0,0 +1,602 @@ +/* + * 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/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_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/assets.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/fee_slider.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({ + Key? key, + required this.walletId, + required this.coin, + }) : super(key: key); + + static const String routeName = "/frostSendView"; + + final String walletId; + final Coin coin; + + @override + ConsumerState createState() => _FrostSendViewState(); +} + +class _FrostSendViewState extends ConsumerState { + final List recipientWidgetIndexes = [0]; + int _greatestWidgetIndex = 0; + + late final String walletId; + late final Coin coin; + + late TextEditingController noteController; + late TextEditingController onChainNoteController; + + final _noteFocusNode = FocusNode(); + + Set selectedUTXOs = {}; + + bool _createSignLock = false; + + Future _loadingFuture() async { + final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet; + + final recipients = recipientWidgetIndexes + .map((i) => ref.read(pRecipient(i).state).state) + .map((e) => (address: e!.address, amount: e.amount!, isChange: false)) + .toList(growable: false); + + final txData = await wallet.frostCreateSignConfig( + txData: TxData(recipients: recipients), + changeAddress: (await wallet.getCurrentReceivingAddress())!.value, + feePerWeight: customFeeRate, + ); + + return txData; + } + + Future _createSignConfig() async { + if (_createSignLock) { + return; + } + _createSignLock = true; + + try { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + + TxData? txData; + if (mounted) { + txData = await showLoading( + whileFuture: _loadingFuture(), + context: context, + message: "Generating sign config", + isDesktop: Util.isDesktop, + onException: (e) { + throw e; + }, + ); + } + + if (mounted && txData != null) { + ref.read(pFrostTxData.notifier).state = txData; + + await Navigator.of(context).pushNamed( + FrostCreateSignConfigView.routeName, + arguments: widget.walletId, + ); + } + } catch (e) { + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Create sign config failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + ), + ); + } + } finally { + _createSignLock = false; + } + } + + int customFeeRate = 1; + + void _validateRecipientFormStates() { + for (final i in recipientWidgetIndexes) { + final state = ref.read(pRecipient(i).state).state; + if (state?.amount == null || state?.address == null) { + ref.read(previewTxButtonStateProvider.notifier).state = false; + return; + } + } + ref.read(previewTxButtonStateProvider.notifier).state = true; + return; + } + + @override + void initState() { + coin = widget.coin; + walletId = widget.walletId; + + noteController = TextEditingController(); + + super.initState(); + } + + @override + void dispose() { + noteController.dispose(); + + _noteFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final wallet = ref.watch(pWallets).getWallet(walletId); + + final showCoinControl = wallet is CoinControlInterface && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + ); + + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Send ${coin.ticker}", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + semanticsLabel: "Import sign config Button.", + key: const Key("importSignConfigButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.circlePlus, + color: Theme.of(context) + .extension()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + FrostImportSignConfigView.routeName, + arguments: walletId, + ); + }, + ), + ), + ), + ], + ), + 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: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Util.isDesktop) + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch( + coinIconProvider(coin), + ), + ), + width: 22, + height: 22, + ), + const SizedBox( + width: 6, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ref.watch(pWalletName(walletId)), + style: STextStyles.titleBold12(context) + .copyWith(fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + // const SizedBox( + // height: 2, + // ), + Text( + "Available balance", + style: + STextStyles.label(context).copyWith(fontSize: 10), + ), + ], + ), + Util.isDesktop + ? const SizedBox( + height: 24, + ) + : const Spacer(), + GestureDetector( + onTap: () { + // cryptoAmountController.text = ref + // .read(pAmountFormatter(coin)) + // .format( + // _cachedBalance!, + // withUnitName: false, + // ); + }, + 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, + ), + // Text( + // "${(manager.balance.spendable.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, + // ) + ], + ), + ), + ) + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipients", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: "Add", + onTap: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + 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], + coin: coin, + onChanged: () { + _validateRecipientFormStates(); + }, + remove: i == 0 && recipientWidgetIndexes.length == 1 + ? null + : () { + recipientWidgetIndexes.removeAt(i); + setState(() {}); + }, + ), + ), + ], + ), + if (showCoinControl) + const SizedBox( + height: 8, + ), + if (showCoinControl) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Coin control", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + CustomTextButton( + text: selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", + onTap: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + } + + if (mounted) { + // finally spendable = ref + // .read(walletsChangeNotifierProvider) + // .getManager(widget.walletId) + // .balance + // .spendable; + + // TODO: [prio=high] make sure this coincontrol works correctly + + Amount? amount; + + final result = await Navigator.of(context).pushNamed( + CoinControlView.routeName, + arguments: Tuple4( + walletId, + CoinControlViewType.use, + amount, + selectedUTXOs, + ), + ); + + if (result is Set) { + setState(() { + selectedUTXOs = result; + }); + } + } + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + bottom: 12, + top: 16, + ), + child: FeeSlider( + coin: coin, + onSatVByteChanged: (rate) { + customFeeRate = rate; + }, + ), + ), + Util.isDesktop + ? const SizedBox( + height: 12, + ) + : const Spacer(), + const SizedBox( + height: 12, + ), + TextButton( + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? _createSignConfig + : null, + style: ref.watch(previewTxButtonStateProvider.state).state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Create config", + style: STextStyles.button(context), + ), + ), + const SizedBox( + height: 16, + ), + ], + ), + ); + } +} + +final previewTxButtonStateProvider = StateProvider((_) => false); diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart new file mode 100644 index 000000000..e59d3e9ad --- /dev/null +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -0,0 +1,501 @@ +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_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +//TODO: move the following two providers elsewhere +final pClipboard = + Provider((ref) => const ClipboardWrapper()); +final pBarcodeScanner = + Provider((ref) => const BarcodeScannerWrapper()); + +// final _pPrice = Provider.family((ref, coin) { +// return ref.watch( +// priceAnd24hChangeNotifierProvider +// .select((value) => value.getPrice(coin).item1), +// ); +// }); + +final pRecipient = + StateProvider.family<({String address, Amount? amount})?, int>( + (ref, index) => null); + +class Recipient extends ConsumerStatefulWidget { + const Recipient({ + super.key, + required this.index, + required this.coin, + this.remove, + this.onChanged, + }); + + final int index; + final Coin coin; + + final VoidCallback? remove; + final VoidCallback? onChanged; + + @override + ConsumerState createState() => _RecipientState(); +} + +class _RecipientState extends ConsumerState { + late final TextEditingController addressController, amountController; + late final FocusNode addressFocusNode, amountFocusNode; + + bool _addressIsEmpty = true; + bool _cryptoAmountChangeLock = false; + + 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(); + + 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 RoundedWhiteContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: (_) { + setState(() { + _addressIsEmpty = addressController.text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter ${widget.coin.ticker} address", + addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _addressIsEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_addressIsEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + addressController.text = ""; + + setState(() { + _addressIsEmpty = true; + }); + + _updateRecipientData(); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await ref + .read(pClipboard) + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, content.indexOf("\n")); + } + + addressController.text = content.trim(); + + setState(() { + _addressIsEmpty = + addressController.text.isEmpty; + }); + + _updateRecipientData(); + } + }, + child: _addressIsEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_addressIsEmpty) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75, + ), + ); + } + + final qrResult = + await ref.read(pBarcodeScanner).scan(); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); + + /// TODO: deal with address utils + final results = + AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log( + "qrResult parsed: $results", + level: LogLevel.Info, + ); + + if (results.isNotEmpty && + results["scheme"] == + widget.coin.uriScheme) { + // auto fill address + + addressController.text = + (results["address"] ?? "").trim(); + + // autofill amount field + if (results["amount"] != null) { + final Amount amount = + Decimal.parse(results["amount"]!) + .toAmount( + fractionDigits: widget.coin.decimals, + ); + amountController.text = ref + .read(pAmountFormatter(widget.coin)) + .format( + amount, + withUnitName: false, + ); + } + } else { + addressController.text = + qrResult.rawContent.trim(); + } + + setState(() { + _addressIsEmpty = + addressController.text.isEmpty; + }); + + _updateRecipientData(); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while " + "trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 12, + ), + TextField( + autocorrect: false, + enableSuggestions: false, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + key: const Key("amountInputFieldCryptoTextFieldKey"), + controller: amountController, + focusNode: amountFocusNode, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + AmountInputFormatter( + decimals: widget.coin.decimals, + unit: ref.watch(pAmountUnit(widget.coin)), + locale: locale, + ), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref + .watch(pAmountUnit(widget.coin)) + .unitForCoin(widget.coin), + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + // if (ref.watch(prefsChangeNotifierProvider + // .select((value) => value.externalCalls))) + // const SizedBox( + // height: 8, + // ), + // if (ref.watch(prefsChangeNotifierProvider + // .select((value) => value.externalCalls))) + // TextField( + // autocorrect: Util.isDesktop ? false : true, + // enableSuggestions: Util.isDesktop ? false : true, + // style: STextStyles.smallMed14(context).copyWith( + // color: Theme.of(context).extension()!.textDark, + // ), + // key: const Key("amountInputFieldFiatTextFieldKey"), + // controller: baseController, + // focusNode: baseFocusNode, + // keyboardType: Util.isDesktop + // ? null + // : const TextInputType.numberWithOptions( + // signed: false, + // decimal: true, + // ), + // textAlign: TextAlign.right, + // inputFormatters: [ + // AmountInputFormatter( + // decimals: 2, + // locale: locale, + // ), + // ], + // onChanged: (baseAmountString) { + // final baseAmount = Amount.tryParseFiatString( + // baseAmountString, + // locale: locale, + // ); + // Amount? cryptoAmount; + // final int decimals = widget.coin.decimals; + // if (baseAmount != null) { + // final _price = ref.read(_pPrice(widget.coin)); + // + // if (_price == Decimal.zero) { + // cryptoAmount = 0.toAmountAsRaw( + // fractionDigits: decimals, + // ); + // } else { + // cryptoAmount = baseAmount <= Amount.zero + // ? 0.toAmountAsRaw(fractionDigits: decimals) + // : (baseAmount.decimal / _price) + // .toDecimal( + // scaleOnInfinitePrecision: decimals, + // ) + // .toAmount(fractionDigits: decimals); + // } + // if (ref.read(pRecipient(widget.index))?.amount != null && + // ref.read(pRecipient(widget.index))?.amount == + // cryptoAmount) { + // return; + // } + // + // final amountString = + // ref.read(pAmountFormatter(widget.coin)).format( + // cryptoAmount, + // withUnitName: false, + // ); + // + // _cryptoAmountChangeLock = true; + // amountController.text = amountString; + // _cryptoAmountChangeLock = false; + // } else { + // cryptoAmount = 0.toAmountAsRaw( + // fractionDigits: decimals, + // ); + // _cryptoAmountChangeLock = true; + // amountController.text = ""; + // _cryptoAmountChangeLock = false; + // } + // + // _updateRecipientData(); + // }, + // 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(prefsChangeNotifierProvider + // .select((value) => value.currency)), + // style: STextStyles.smallMed14(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .accentColorDark), + // ), + // ), + // ), + // ), + // ), + if (widget.remove != null) + const SizedBox( + height: 6, + ), + if (widget.remove != null) + Row( + children: [ + const Spacer(), + CustomTextButton( + text: "Remove", + onTap: () { + ref.read(pRecipient(widget.index).notifier).state = null; + widget.remove?.call(); + }, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index bbb688f01..3bb5abbd8 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -29,6 +29,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'; @@ -63,6 +64,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'; @@ -973,10 +975,13 @@ class _WalletViewState extends ConsumerState { // break; // } Navigator.of(context).pushNamed( - SendView.routeName, - arguments: Tuple2( - walletId, - coin, + ref.read(pWallets).getWallet(walletId) + is BitcoinFrostWallet + ? FrostSendView.routeName + : SendView.routeName, + arguments: ( + walletId: walletId, + coin: coin, ), ); }, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index a330cb781..01ff6801f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -10,13 +10,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/widgets/custom_tab_view.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class MyWallet extends ConsumerStatefulWidget { @@ -40,11 +44,15 @@ class _MyWalletState extends ConsumerState { ]; late final bool isEth; + late final Coin coin; + late final bool isFrost; @override void initState() { - isEth = ref.read(pWallets).getWallet(widget.walletId).info.coin == - Coin.ethereum; + final wallet = ref.read(pWallets).getWallet(widget.walletId); + coin = wallet.info.coin; + isFrost = wallet is BitcoinFrostWallet; + isEth = coin == Coin.ethereum; if (isEth && widget.contractAddress == null) { titles.add("Transactions"); @@ -64,12 +72,37 @@ class _MyWalletState extends ConsumerState { titles: titles, children: [ widget.contractAddress == null - ? Padding( - padding: const EdgeInsets.all(20), - child: DesktopSend( - walletId: widget.walletId, - ), - ) + ? isFrost + ? Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Import sign config", + onPressed: () { + Navigator.of(context).pushNamed( + FrostImportSignConfigView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + FrostSendView( + walletId: widget.walletId, + coin: coin, + ), + ], + ) + : Padding( + padding: const EdgeInsets.all(20), + child: DesktopSend( + walletId: widget.walletId, + ), + ) : Padding( padding: const EdgeInsets.all(20), child: DesktopTokenSend( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index faf8967e8..87e29d64c 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -756,6 +756,24 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FrostSendView.routeName: + if (args is ({ + String walletId, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostSendView( + walletId: args.walletId, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // case MonkeyLoadedView.routeName: // if (args is Tuple2>) { // return getRoute( diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 4f1c3febf..4058b8b59 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -29,6 +29,12 @@ import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; class BitcoinFrostWallet extends Wallet { + @override + int get isarTransactionVersion => 2; + + @override + bool get supportsMultiRecipient => true; + BitcoinFrostWallet(CryptoCurrencyNetwork network) : super(BitcoinFrost(network) as T); diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 959b7b635..ad18d6d4e 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -55,6 +55,9 @@ abstract class Wallet { // default to Transaction class. For TransactionV2 set to 2 int get isarTransactionVersion => 1; + // whether the wallet currently supports multiple recipients per tx + bool get supportsMultiRecipient => false; + Wallet(this.cryptoCurrency); //============================================================================ From 1e67f3585aa7edc821afe809402108b220437fa8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 25 Jan 2024 02:20:37 -0600 Subject: [PATCH 012/272] some frost clean up --- lib/db/isar/main_db.dart | 2 + .../restore/restore_frost_ms_wallet_view.dart | 6 +-- .../frost_attempt_sign_config_view.dart | 17 +++---- .../frost_ms/frost_complete_sign_view.dart | 4 +- .../frost_continue_sign_config_view.dart | 18 +++---- .../frost_import_sign_config_view.dart | 4 +- .../send_view/frost_ms/frost_send_view.dart | 2 +- lib/route_generator.dart | 1 + .../wallet/impl/bitcoin_frost_wallet.dart | 49 +++++++++++++++---- 9 files changed, 69 insertions(+), 34 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 528c99f98..ac5a544f4 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -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, diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index e99655f06..e316f22c8 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -4,7 +4,7 @@ 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'; +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'; @@ -70,7 +70,7 @@ class _RestoreFrostMsWalletViewState final keys = keysFieldController.text; final config = configFieldController.text; - final myNameIndex = getParticipantIndexFromKeys(serializedKeys: keys); + final myNameIndex = frost.getParticipantIndexFromKeys(serializedKeys: keys); final participants = Frost.getParticipants(multisigConfig: config); final myName = participants[myNameIndex]; @@ -92,7 +92,7 @@ class _RestoreFrostMsWalletViewState knownSalts: [], participants: participants, myName: myName, - threshold: multisigThreshold( + threshold: frost.multisigThreshold( multisigConfig: config, ), ); diff --git a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart index 64b33e9f9..7cdf4a69b 100644 --- a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart +++ b/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart @@ -7,13 +7,13 @@ import 'package:stackwallet/pages/send_view/frost_ms/frost_continue_sign_config_ 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/coins/bitcoin/frost_wallet.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/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'; @@ -72,15 +72,14 @@ class _FrostAttemptSignConfigViewState @override void initState() { - final wallet = ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .wallet as FrostWallet; + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; + final frostInfo = wallet.frostInfo; - myName = wallet.myName; - threshold = wallet.threshold; - participantsWithoutMe = wallet.participants; - myIndex = participantsWithoutMe.indexOf(wallet.myName); + myName = frostInfo.myName; + threshold = frostInfo.threshold; + participantsWithoutMe = frostInfo.participants; + myIndex = participantsWithoutMe.indexOf(frostInfo.myName); myPreprocess = ref.read(pFrostAttemptSignData.state).state!.preprocess; participantsWithoutMe.removeAt(myIndex); diff --git a/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart b/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart index c63dca04d..6478495c0 100644 --- a/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart +++ b/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart @@ -139,8 +139,8 @@ class _FrostCompleteSignViewState extends ConsumerState { Exception? ex; final txData = await showLoading( whileFuture: ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) + .read(pWallets) + .getWallet(widget.walletId) .confirmSend( txData: ref.read(pFrostTxData.state).state!, ), diff --git a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart index 9d6494c62..732fb2f82 100644 --- a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart +++ b/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart @@ -9,13 +9,13 @@ 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/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/coins/bitcoin/frost_wallet.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/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'; @@ -61,17 +61,17 @@ class _FrostContinueSignViewState extends ConsumerState { @override void initState() { - final wallet = ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .wallet as FrostWallet; + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - myName = wallet.myName; - participantsAll = wallet.participants; - myIndex = wallet.participants.indexOf(wallet.myName); + final frostInfo = wallet.frostInfo; + + myName = frostInfo.myName; + participantsAll = frostInfo.participants; + myIndex = frostInfo.participants.indexOf(frostInfo.myName); myShare = ref.read(pFrostContinueSignData.state).state!.share; - participantsWithoutMe = wallet.participants + participantsWithoutMe = frostInfo.participants .toSet() .intersection( ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet()) diff --git a/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart index b390e0b67..c89b6846a 100644 --- a/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart +++ b/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart @@ -92,7 +92,9 @@ class _FrostImportSignConfigViewState // TODO add more data from 'data' and display to user ? ref.read(pFrostTxData.notifier).state = TxData( frostMSConfig: config, - recipients: data.recipients, + recipients: data.recipients + .map((e) => (address: e.address, amount: e.amount, isChange: false)) + .toList(), utxos: utxos.toSet(), ); diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index cf64d8e54..1865556b7 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -84,7 +84,7 @@ class _FrostSendViewState extends ConsumerState { final recipients = recipientWidgetIndexes .map((i) => ref.read(pRecipient(i).state).state) - .map((e) => (address: e!.address, amount: e.amount!, isChange: false)) + .map((e) => (address: e!.address, amount: e!.amount!, isChange: false)) .toList(growable: false); final txData = await wallet.frostCreateSignConfig( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 87e29d64c..26e24653c 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -83,6 +83,7 @@ import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.d import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/send_view/token_send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 4058b8b59..be3b7b821 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -29,12 +29,6 @@ import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; class BitcoinFrostWallet extends Wallet { - @override - int get isarTransactionVersion => 2; - - @override - bool get supportsMultiRecipient => true; - BitcoinFrostWallet(CryptoCurrencyNetwork network) : super(BitcoinFrost(network) as T); @@ -89,7 +83,9 @@ class BitcoinFrostWallet extends Wallet { await _saveMultisigId(multisigId); await _saveMultisigConfig(multisigConfig); - await mainDB.isar.frostWalletInfo.put(frostWalletInfo); + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.put(frostWalletInfo); + }); final keys = frost.deserializeKeys(keys: serializedKeys); @@ -299,6 +295,9 @@ class BitcoinFrostWallet extends Wallet { // ==================== Overrides ============================================ + @override + bool get supportsMultiRecipient => true; + @override int get isarTransactionVersion => 2; @@ -527,8 +526,40 @@ class BitcoinFrostWallet extends Wallet { @override Future checkSaveInitialReceivingAddress() async { - // should not be needed for frost as we explicitly save the address - // on new init and restore + final address = await getCurrentReceivingAddress(); + if (address == null) { + final serializedKeys = await getSerializedKeys(); + if (serializedKeys != null) { + final keys = frost.deserializeKeys(keys: serializedKeys); + + final addressString = frost.addressForKeys( + network: cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet, + keys: keys, + ); + + final publicKey = frost.scriptPubKeyForKeys(keys: keys); + + final address = Address( + walletId: walletId, + value: addressString, + publicKey: publicKey.toUint8ListFromHex, + derivationIndex: 0, + derivationPath: null, + subType: AddressSubType.receiving, + type: AddressType.frostMS, + ); + + await mainDB.updateOrPutAddresses([address]); + } else { + Logging.instance.log( + "$runtimeType.checkSaveInitialReceivingAddress() failed due" + " to missing serialized keys", + level: LogLevel.Fatal, + ); + } + } } @override From b3fa5147340f7ae873b58ceb863c79ed6e9af4e9 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Jan 2024 12:13:35 -0600 Subject: [PATCH 013/272] use github.com instead of www.github.com as frostdart submodule url caused issues for me initializing submodule using git on cli see https://stackoverflow.com/a/64991733 and related --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 98bb17794..925be21c0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,4 +9,4 @@ url = https://github.com/cypherstack/flutter_liblelantus.git [submodule "crypto_plugins/frostdart"] path = crypto_plugins/frostdart - url = https://www.github.com/cypherstack/frostdart + url = https://github.com/cypherstack/frostdart From 2aa3bebf78280f56650c3f0f7be5e7bf7a8964cf Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Jan 2024 19:03:53 -0600 Subject: [PATCH 014/272] wrap send view content in padding will probably need to be adjusted for mobile... --- .../send_view/frost_ms/frost_send_view.dart | 388 +++++++++--------- 1 file changed, 198 insertions(+), 190 deletions(-) diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 1865556b7..95d45cb95 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -391,207 +391,215 @@ class _FrostSendViewState extends ConsumerState { const SizedBox( height: 16, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recipients", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - CustomTextButton( - text: "Add", - onTap: () { - // used for tracking recipient forms - _greatestWidgetIndex++; - recipientWidgetIndexes.add(_greatestWidgetIndex); - setState(() {}); - }, - ), - ], - ), - const SizedBox( - height: 8, - ), - 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], - coin: coin, - onChanged: () { - _validateRecipientFormStates(); - }, - remove: i == 0 && recipientWidgetIndexes.length == 1 - ? null - : () { - recipientWidgetIndexes.removeAt(i); - setState(() {}); - }, - ), - ), - ], - ), - if (showCoinControl) - const SizedBox( - height: 8, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, ), - if (showCoinControl) - RoundedWhiteContainer( - child: Row( + child: Column(children: [ + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Coin control", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), + "Recipients", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), CustomTextButton( - text: selectedUTXOs.isEmpty - ? "Select coins" - : "Selected coins (${selectedUTXOs.length})", - onTap: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); - } - - if (mounted) { - // finally spendable = ref - // .read(walletsChangeNotifierProvider) - // .getManager(widget.walletId) - // .balance - // .spendable; - - // TODO: [prio=high] make sure this coincontrol works correctly - - Amount? amount; - - final result = await Navigator.of(context).pushNamed( - CoinControlView.routeName, - arguments: Tuple4( - walletId, - CoinControlViewType.use, - amount, - selectedUTXOs, - ), - ); - - if (result is Set) { - setState(() { - selectedUTXOs = result; - }); - } - } + text: "Add", + onTap: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); }, ), ], ), - ), - 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: 8, ), - ), - ), - const SizedBox( - height: 12, - ), - Padding( - padding: const EdgeInsets.only( - bottom: 12, - top: 16, - ), - child: FeeSlider( - coin: coin, - onSatVByteChanged: (rate) { - customFeeRate = rate; - }, - ), - ), - Util.isDesktop - ? const SizedBox( - height: 12, - ) - : const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: ref.watch(previewTxButtonStateProvider.state).state - ? _createSignConfig - : null, - style: ref.watch(previewTxButtonStateProvider.state).state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Create config", - style: STextStyles.button(context), - ), - ), - const SizedBox( - height: 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], + coin: coin, + onChanged: () { + _validateRecipientFormStates(); + }, + remove: i == 0 && recipientWidgetIndexes.length == 1 + ? null + : () { + recipientWidgetIndexes.removeAt(i); + setState(() {}); + }, + ), + ), + ], + ), + if (showCoinControl) + const SizedBox( + height: 8, + ), + if (showCoinControl) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Coin control", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + CustomTextButton( + text: selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", + onTap: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + } + + if (mounted) { + // finally spendable = ref + // .read(walletsChangeNotifierProvider) + // .getManager(widget.walletId) + // .balance + // .spendable; + + // TODO: [prio=high] make sure this coincontrol works correctly + + Amount? amount; + + final result = + await Navigator.of(context).pushNamed( + CoinControlView.routeName, + arguments: Tuple4( + walletId, + CoinControlViewType.use, + amount, + selectedUTXOs, + ), + ); + + if (result is Set) { + setState(() { + selectedUTXOs = result; + }); + } + } + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + bottom: 12, + top: 16, + ), + child: FeeSlider( + coin: coin, + onSatVByteChanged: (rate) { + customFeeRate = rate; + }, + ), + ), + Util.isDesktop + ? const SizedBox( + height: 12, + ) + : const Spacer(), + const SizedBox( + height: 12, + ), + TextButton( + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? _createSignConfig + : null, + style: ref.watch(previewTxButtonStateProvider.state).state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Create config", + style: STextStyles.button(context), + ), + ), + const SizedBox( + height: 16, + ), + ]), ), ], ), From 77f1f346d632598e970c44db57704536bcfc13c7 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Jan 2024 19:04:07 -0600 Subject: [PATCH 015/272] override recipient input(s) padding --- lib/pages/send_view/frost_ms/recipient.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index e59d3e9ad..89121d065 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -149,6 +149,7 @@ class _RecipientState extends ConsumerState { ); return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, From a100e6a15c17582e7950500b2471db368ffca4ed Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 29 Jan 2024 17:31:41 -0600 Subject: [PATCH 016/272] only show frost-related config buttons for frost coins --- .../name_your_wallet_view/name_your_wallet_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index ed91d265e..7ddaaba3a 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -399,7 +399,7 @@ class _NameYourWalletViewState extends ConsumerState { ); }, ), - if (widget.addWalletType == AddWalletType.New) + if (widget.coin.isFrost && widget.addWalletType == AddWalletType.New) Column( children: [ PrimaryButton( From cce94676a69b996a6c7a4f2219651caa03b3112a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 29 Jan 2024 23:29:52 -0600 Subject: [PATCH 017/272] fix bitcoin frost wallet restoration --- .../frost_ms/restore/restore_frost_ms_wallet_view.dart | 4 +++- lib/wallets/wallet/impl/bitcoin_frost_wallet.dart | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index e316f22c8..c9c174ab0 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -97,7 +97,9 @@ class _RestoreFrostMsWalletViewState ), ); - await ref.read(mainDBProvider).isar.frostWalletInfo.put(frostInfo); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.frostWalletInfo.put(frostInfo); + }); await (wallet as BitcoinFrostWallet).recover( serializedKeys: keys, diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index be3b7b821..aacad77e4 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -718,8 +718,9 @@ class BitcoinFrostWallet extends Wallet { if (knownSalts.contains(salt)) { throw Exception("Known frost multisig salt found!"); } - knownSalts.add(salt); - await _updateKnownSalts(knownSalts); + List updatedKnownSalts = List.from(knownSalts); + updatedKnownSalts.add(salt); + await _updateKnownSalts(updatedKnownSalts); } else { // clear cache await electrumXCachedClient.clearSharedTransactionCache(coin: coin); @@ -1001,8 +1002,9 @@ class BitcoinFrostWallet extends Wallet { .findFirstSync()!; Future _updateKnownSalts(List knownSalts) async { + final info = frostInfo; + await mainDB.isar.writeTxn(() async { - final info = frostInfo; await mainDB.isar.frostWalletInfo.delete(info.id); await mainDB.isar.frostWalletInfo.put( info.copyWith(knownSalts: knownSalts), From 0f73f762162f9a18627135868013377eba91a9b4 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 30 Jan 2024 11:43:09 -0600 Subject: [PATCH 018/272] refactor _multisigConfig to getMultisigConfig for SWB purposes --- lib/wallets/wallet/impl/bitcoin_frost_wallet.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index aacad77e4..851c41eed 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -930,7 +930,8 @@ class BitcoinFrostWallet extends Wallet { key: "{$walletId}_serializedFROSTKeysPrevGen", ); - Future _multisigConfig() async => await secureStorageInterface.read( + Future getMultisigConfig() async => + await secureStorageInterface.read( key: "{$walletId}_multisigConfig", ); @@ -942,7 +943,7 @@ class BitcoinFrostWallet extends Wallet { Future _saveMultisigConfig( String multisigConfig, ) async { - final current = await _multisigConfig(); + final current = await getMultisigConfig(); if (current == null) { // do nothing From 8ba98d573c463bbbe9e0f99f128ad80272ae5ddb Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 30 Jan 2024 11:43:40 -0600 Subject: [PATCH 019/272] save frost keys and config in otherDataJsonString during SWB creation --- .../helpers/restore_create_backup.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 9891148d7..222a2f948 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -42,6 +42,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.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/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 +303,16 @@ 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(); + // TODO handle case in which either keys or config is null. + + // Format keys and config as a JSON string and set otherDataJsonString. + Map otherData = {}; + otherData["keys"] = keys; + otherData["config"] = config; + backupWallet['otherDataJsonString'] = jsonEncode(otherData); } backupWallet['coinName'] = wallet.info.coin.name; backupWallet['storedChainHeight'] = wallet.info.cachedChainHeight; From 79fedf46e55dd588d4ee3c0792a91182b9033cba Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 30 Jan 2024 11:48:50 -0600 Subject: [PATCH 020/272] throw err if keys or config are null --- .../helpers/restore_create_backup.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 222a2f948..11254181a 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -306,9 +306,15 @@ abstract class SWB { } else if (wallet is BitcoinFrostWallet) { String? keys = await wallet.getSerializedKeys(); String? config = await wallet.getMultisigConfig(); - // TODO handle case in which either keys or config is null. + 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); + } + // TODO [prio=low]: solve case in which either keys or config is null. - // Format keys and config as a JSON string and set otherDataJsonString. + // Format keys & config as a JSON string and set otherDataJsonString. Map otherData = {}; otherData["keys"] = keys; otherData["config"] = config; From a17a551a2b21c76e2ef9c7020c1951c4b3228791 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 30 Jan 2024 12:25:39 -0600 Subject: [PATCH 021/272] add myName to saved frost info --- .../stack_backup_views/helpers/restore_create_backup.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 11254181a..01cc9f65e 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -306,6 +306,7 @@ abstract class SWB { } else if (wallet is BitcoinFrostWallet) { String? keys = await wallet.getSerializedKeys(); String? config = await wallet.getMultisigConfig(); + String myName = wallet.frostInfo.myName; if (keys == null || config == null) { String err = "${wallet.info.coin.name} wallet ${wallet.info.name} " "has null keys or config"; @@ -318,6 +319,7 @@ abstract class SWB { Map otherData = {}; otherData["keys"] = keys; otherData["config"] = config; + otherData["myName"] = myName; backupWallet['otherDataJsonString'] = jsonEncode(otherData); } backupWallet['coinName'] = wallet.info.coin.name; From 8cbca16a3a67bfd34bc5fa54b116d9565f0518e1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 30 Jan 2024 12:41:37 -0600 Subject: [PATCH 022/272] WIP first attempt at Frost wallet restoration from backup --- .../helpers/restore_create_backup.dart | 99 +++++++++++++++++-- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 01cc9f65e..c47630279 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -10,9 +10,11 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; +import 'package:frostdart/frostdart_bindings_generated.dart'; import 'package:isar/isar.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/db/hive/db.dart'; @@ -26,6 +28,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'; @@ -425,16 +428,92 @@ abstract class SWB { ); try { - final wallet = await Wallet.create( - walletInfo: info, - mainDB: MainDB.instance, - secureStorageInterface: secureStorageInterface, - nodeService: nodeService, - prefs: prefs, - mnemonic: mnemonic, - mnemonicPassphrase: mnemonicPassphrase, - privateKey: privateKey, - ); + final Wallet wallet; + + if (info.coin == Coin.bitcoinFrost || + info.coin == Coin.bitcoinFrostTestNet) { + // Decode info.otherDataJsonString for Frost recovery info. + final otherData = jsonDecode(info.otherDataJsonString!); + final String serializedKeys = otherData["keys"] as String; + final String multisigConfig = otherData["config"] as String; + final String myName = otherData["myName"] as String; + + // Start Frost key generation. + final frostStartKeyGenData = Frost.startKeyGeneration( + multisigConfig: multisigConfig, + myName: myName, + ); + + // Generate shares. + final ({ + Pointer secretSharesResPtr, + String share + }) frostSecretSharesData; + try { + frostSecretSharesData = Frost.generateSecretShares( + multisigConfigWithNamePtr: + frostStartKeyGenData.multisigConfigWithNamePtr, + mySeed: frostStartKeyGenData.seed, + secretShareMachineWrapperPtr: + frostStartKeyGenData.secretShareMachineWrapperPtr, + commitments: [frostStartKeyGenData.commitments], + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + throw Error(); + } + + // Get shares. + final shares = [ + frostSecretSharesData.share, + ]; + + // Complete Frost key generation. + final frostCompleteKeyGenData = Frost.completeKeyGeneration( + multisigConfigWithNamePtr: + frostStartKeyGenData.multisigConfigWithNamePtr, + secretSharesResPtr: frostSecretSharesData.secretSharesResPtr, + shares: [frostSecretSharesData.share], // TODO [prio=high]: verify. + ); + + wallet = await Wallet.create( + walletInfo: info, + mainDB: MainDB.instance, + secureStorageInterface: secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + ); + + await (wallet as BitcoinFrostWallet).initializeNewFrost( + mnemonic: frostStartKeyGenData.seed, + multisigConfig: multisigConfig, + recoveryString: frostCompleteKeyGenData.recoveryString, + serializedKeys: serializedKeys, + multisigId: frostCompleteKeyGenData.multisigId, + myName: myName, + participants: Frost.getParticipants( + multisigConfig: multisigConfig, + ), + threshold: Frost.getThreshold( + multisigConfig: multisigConfig, + ), + ); + } else { + wallet = await Wallet.create( + walletInfo: info, + mainDB: MainDB.instance, + secureStorageInterface: secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + mnemonic: mnemonic, + mnemonicPassphrase: mnemonicPassphrase, + privateKey: privateKey, + ); + } await wallet.init(); From 2e6ac40e205724541beb479356b85d9cf7f66e1c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 30 Jan 2024 12:45:39 -0600 Subject: [PATCH 023/272] fix 'cannot cast Null to String' --- .../stack_backup_views/helpers/restore_create_backup.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index c47630279..4dec9dac7 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -406,7 +406,9 @@ abstract class SWB { if (walletbackup['mnemonic'] == null) { // probably private key based - privateKey = walletbackup['privateKey'] as String; + if (walletbackup['privateKey'] != null) { + privateKey = walletbackup['privateKey'] as String; + } } else { if (walletbackup['mnemonic'] is List) { List mnemonicList = (walletbackup['mnemonic'] as List) From 0d3ef1bfc4b3a76573e0afb5efbc1c64c67dfde2 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 30 Jan 2024 18:08:31 -0600 Subject: [PATCH 024/272] frost swb integration fixes --- .../helpers/restore_create_backup.dart | 137 +++++++----------- 1 file changed, 50 insertions(+), 87 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 4dec9dac7..f6491cf1d 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -10,11 +10,10 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; -import 'package:frostdart/frostdart_bindings_generated.dart'; +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'; @@ -44,6 +43,7 @@ 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'; @@ -309,21 +309,21 @@ abstract class SWB { } else if (wallet is BitcoinFrostWallet) { String? keys = await wallet.getSerializedKeys(); String? config = await wallet.getMultisigConfig(); - String myName = wallet.frostInfo.myName; 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 otherData = {}; - otherData["keys"] = keys; - otherData["config"] = config; - otherData["myName"] = myName; - backupWallet['otherDataJsonString'] = jsonEncode(otherData); + Map frostData = {}; + frostData["keys"] = keys; + frostData["config"] = config; + backupWallet['frostWalletData'] = jsonEncode(frostData); } backupWallet['coinName'] = wallet.info.coin.name; backupWallet['storedChainHeight'] = wallet.info.cachedChainHeight; @@ -430,93 +430,48 @@ abstract class SWB { ); try { - final Wallet wallet; - - if (info.coin == Coin.bitcoinFrost || - info.coin == Coin.bitcoinFrostTestNet) { + String? serializedKeys; + String? multisigConfig; + if (info.coin.isFrost) { // Decode info.otherDataJsonString for Frost recovery info. - final otherData = jsonDecode(info.otherDataJsonString!); - final String serializedKeys = otherData["keys"] as String; - final String multisigConfig = otherData["config"] as String; - final String myName = otherData["myName"] as String; + final frostData = jsonDecode(walletbackup["frostWalletData"] as String); + serializedKeys = frostData["keys"] as String; + multisigConfig = frostData["config"] as String; - // Start Frost key generation. - final frostStartKeyGenData = Frost.startKeyGeneration( - multisigConfig: multisigConfig, - myName: myName, - ); - - // Generate shares. - final ({ - Pointer secretSharesResPtr, - String share - }) frostSecretSharesData; - try { - frostSecretSharesData = Frost.generateSecretShares( - multisigConfigWithNamePtr: - frostStartKeyGenData.multisigConfigWithNamePtr, - mySeed: frostStartKeyGenData.seed, - secretShareMachineWrapperPtr: - frostStartKeyGenData.secretShareMachineWrapperPtr, - commitments: [frostStartKeyGenData.commitments], - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - throw Error(); - } - - // Get shares. - final shares = [ - frostSecretSharesData.share, - ]; - - // Complete Frost key generation. - final frostCompleteKeyGenData = Frost.completeKeyGeneration( - multisigConfigWithNamePtr: - frostStartKeyGenData.multisigConfigWithNamePtr, - secretSharesResPtr: frostSecretSharesData.secretSharesResPtr, - shares: [frostSecretSharesData.share], // TODO [prio=high]: verify. - ); - - wallet = await Wallet.create( - walletInfo: info, - mainDB: MainDB.instance, - secureStorageInterface: secureStorageInterface, - nodeService: nodeService, - prefs: prefs, - ); - - await (wallet as BitcoinFrostWallet).initializeNewFrost( - mnemonic: frostStartKeyGenData.seed, - multisigConfig: multisigConfig, - recoveryString: frostCompleteKeyGenData.recoveryString, + final myNameIndex = frost.getParticipantIndexFromKeys( serializedKeys: serializedKeys, - multisigId: frostCompleteKeyGenData.multisigId, + ); + final participants = Frost.getParticipants( + multisigConfig: multisigConfig, + ); + final myName = participants[myNameIndex]; + + final frostInfo = FrostWalletInfo( + walletId: info.walletId, + knownSalts: [], + participants: participants, myName: myName, - participants: Frost.getParticipants( - multisigConfig: multisigConfig, - ), - threshold: Frost.getThreshold( + threshold: frost.multisigThreshold( multisigConfig: multisigConfig, ), ); - } else { - wallet = await Wallet.create( - walletInfo: info, - mainDB: MainDB.instance, - secureStorageInterface: secureStorageInterface, - nodeService: nodeService, - prefs: prefs, - mnemonic: mnemonic, - mnemonicPassphrase: mnemonicPassphrase, - privateKey: privateKey, - ); + + await MainDB.instance.isar.writeTxn(() async { + await MainDB.instance.isar.frostWalletInfo.put(frostInfo); + }); } + final wallet = await Wallet.create( + walletInfo: info, + mainDB: MainDB.instance, + secureStorageInterface: secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + mnemonic: mnemonic, + mnemonicPassphrase: mnemonicPassphrase, + privateKey: privateKey, + ); + await wallet.init(); int restoreHeight = walletbackup['restoreHeight'] as int? ?? 0; @@ -527,7 +482,15 @@ abstract class SWB { Future? restoringFuture; if (!(wallet is CwBasedInterface || wallet is EpiccashWallet)) { - restoringFuture = wallet.recover(isRescan: false); + if (wallet is BitcoinFrostWallet) { + restoringFuture = wallet.recover( + isRescan: false, + multisigConfig: multisigConfig!, + serializedKeys: serializedKeys!, + ); + } else { + restoringFuture = wallet.recover(isRescan: false); + } } uiState?.update( From ccf1e3437776da374a57caad2a78605bc58587f6 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 30 Jan 2024 19:50:55 -0600 Subject: [PATCH 025/272] port of frost backup keys ui from stack frost --- .../wallet_backup_view.dart | 391 ++++++++++++------ .../wallet_settings_view.dart | 105 +++-- .../firo_rescan_recovery_error_dialog.dart | 7 +- .../unlock_wallet_keys_desktop.dart | 43 +- .../wallet_keys_desktop_popup.dart | 216 ++++++---- lib/route_generator.dart | 35 +- .../wallet/impl/bitcoin_frost_wallet.dart | 4 +- 7 files changed, 550 insertions(+), 251 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index 8c2873d0d..5b1548514 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -23,16 +23,23 @@ 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'; +import '../../../wallet_view/transaction_views/transaction_details_view.dart'; + class WalletBackupView extends ConsumerWidget { const WalletBackupView({ Key? key, required this.walletId, required this.mnemonic, + this.frostWalletData, this.clipboardInterface = const ClipboardWrapper(), }) : super(key: key); @@ -40,11 +47,21 @@ class WalletBackupView extends ConsumerWidget { final String walletId; final List mnemonic; + final ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? frostWalletData; final ClipboardInterface clipboardInterface; @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); + + final bool frost = frostWalletData != null; + final prevGen = frostWalletData?.prevGen != null; + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -91,139 +108,261 @@ class WalletBackupView extends ConsumerWidget { ), body: Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - ref.watch(pWalletName(walletId)), - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Recovery Phrase", - textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", - style: STextStyles.label(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: mnemonic, - isDesktop: false, - ), - ), - ), - const SizedBox( - height: 12, - ), - TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - String data = AddressUtils.encodeQRSeedData(mnemonic); - - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (_) { - final width = MediaQuery.of(context).size.width / 2; - return StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Recovery phrase QR code", - style: STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: RepaintBoundary( - // key: _qrKey, - child: SizedBox( - width: width + 20, - height: width + 20, - child: QrImageView( - data: data, - size: width, - backgroundColor: Theme.of(context) - .extension()! - .popupBG, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark), + child: frost + ? LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "Please write down your backup data. Keep it safe and " + "never share it with anyone. " + "Your backup data is the only way you can access your " + "funds if you forget your PIN, lose your phone, etc." + "\n\n" + "Stack Wallet does not keep nor is able to restore " + "your backup data. " + "Only you have access to your wallet.", + style: STextStyles.label(context), ), ), - ), - const SizedBox( - height: 12, - ), - Center( - child: SizedBox( - width: width, - child: TextButton( - onPressed: () async { - // await _capturePng(true); - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), + const SizedBox( + height: 24, + ), + // DetailItem( + // title: "My name", + // detail: frostWalletData!.myName, + // button: Util.isDesktop + // ? IconCopyButton( + // data: frostWalletData!.myName, + // ) + // : SimpleCopyButton( + // data: frostWalletData!.myName, + // ), + // ), + // const SizedBox( + // height: 16, + // ), + DetailItem( + title: "Multisig config", + detail: frostWalletData!.config, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.config, + ) + : SimpleCopyButton( + data: frostWalletData!.config, + ), + ), + const SizedBox( + height: 16, + ), + DetailItem( + title: "Keys", + detail: frostWalletData!.keys, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.keys, + ) + : SimpleCopyButton( + data: frostWalletData!.keys, + ), + ), + if (prevGen) + const SizedBox( + height: 24, + ), + if (prevGen) + RoundedWhiteContainer( child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + "Previous generation info", + style: STextStyles.label(context), ), ), - ), - ), - ], + if (prevGen) + const SizedBox( + height: 12, + ), + if (prevGen) + DetailItem( + title: "Previous multisig config", + detail: frostWalletData!.prevGen!.config, + button: Util.isDesktop + ? IconCopyButton( + data: + frostWalletData!.prevGen!.config, + ) + : SimpleCopyButton( + data: + frostWalletData!.prevGen!.config, + ), + ), + if (prevGen) + const SizedBox( + height: 16, + ), + if (prevGen) + DetailItem( + title: "Previous keys", + detail: frostWalletData!.prevGen!.keys, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.prevGen!.keys, + ) + : SimpleCopyButton( + data: frostWalletData!.prevGen!.keys, + ), + ), + ], + ), ), - ); - }, - ); - }, - child: Text( - "Show QR Code", - style: STextStyles.button(context), + ), + ); + }, + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 4, + ), + Text( + ref.watch(pWalletName(walletId)), + textAlign: TextAlign.center, + style: STextStyles.label(context).copyWith( + fontSize: 12, + ), + ), + const SizedBox( + height: 4, + ), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: + Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", + style: STextStyles.label(context), + ), + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: mnemonic, + isDesktop: false, + ), + ), + ), + const SizedBox( + height: 12, + ), + TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + String data = AddressUtils.encodeQRSeedData(mnemonic); + + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (_) { + final width = MediaQuery.of(context).size.width / 2; + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Recovery phrase QR code", + style: STextStyles.pageTitleH2(context), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: RepaintBoundary( + // key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QrImageView( + data: data, + size: width, + backgroundColor: Theme.of(context) + .extension()! + .popupBG, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: SizedBox( + width: width, + child: TextButton( + onPressed: () async { + // await _capturePng(true); + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context), + child: Text( + "Cancel", + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), + ), + ], ), - ), - ], - ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 0714528e0..e0b870326 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -39,6 +39,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'; @@ -235,39 +236,83 @@ class _WalletSettingsViewState extends ConsumerState { final wallet = ref .read(pWallets) .getWallet(widget.walletId); - // TODO: [prio=frost] take wallets that don't have a mnemonic into account - if (wallet is MnemonicInterface) { - final mnemonic = - await wallet.getMnemonicAsWords(); - if (mounted) { - await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: - Tuple2( - walletId, mnemonic), - showBackButton: true, - routeOnSuccess: - WalletBackupView - .routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery phrase", - biometricsAuthenticationTitle: - "View recovery phrase", - ), - settings: const RouteSettings( - name: - "/viewRecoverPhraseLockscreen"), - ), + // TODO: [prio=med] take wallets that don't have a mnemonic into account + + List? mnemonic; + ({ + String myName, + String config, + String keys, + ({ + String config, + String keys + })? prevGen, + })? frostWalletData; + if (wallet is BitcoinFrostWallet) { + List> futures = []; + + futures.addAll( + [ + wallet.getSerializedKeys(), + wallet.getMultisigConfig(), + wallet.getSerializedKeysPrevGen(), + wallet.getMultisigConfigPrevGen(), + ], + ); + + final results = + await Future.wait(futures); + + if (results.length == 5) { + 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 (mounted) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: ( + walletId: walletId, + mnemonic: mnemonic ?? [], + frostWalletData: + frostWalletData, + ), + showBackButton: true, + routeOnSuccess: + WalletBackupView.routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: + "View recovery phrase", + ), + settings: const RouteSettings( + name: + "/viewRecoverPhraseLockscreen"), + ), + ); } }, ); diff --git a/lib/pages/special/firo_rescan_recovery_error_dialog.dart b/lib/pages/special/firo_rescan_recovery_error_dialog.dart index d062b62d5..40d8e8d6c 100644 --- a/lib/pages/special/firo_rescan_recovery_error_dialog.dart +++ b/lib/pages/special/firo_rescan_recovery_error_dialog.dart @@ -23,7 +23,6 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -import 'package:tuple/tuple.dart'; enum FiroRescanRecoveryErrorViewOption { retry, @@ -269,8 +268,10 @@ class _FiroRescanRecoveryErrorViewState shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => LockscreenView( - routeOnSuccessArguments: - Tuple2(widget.walletId, mnemonic), + routeOnSuccessArguments: ( + walletId: widget.walletId, + mnemonic: mnemonic, + ), showBackButton: true, routeOnSuccess: WalletBackupView.routeName, biometricsCancelButtonString: "CANCEL", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 0a9a5a29e..c621a4030 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -21,6 +21,7 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; @@ -80,19 +81,31 @@ class _UnlockWalletKeysDesktopState Navigator.of(context, rootNavigator: true).pop(); final wallet = ref.read(pWallets).getWallet(widget.walletId); + ({String keys, String config})? frostData; + List? words; - // TODO: [prio=med] handle wallets that don't have a mnemonic + // TODO: [prio=low] handle wallets that don't have a mnemonic // All wallets currently are mnemonic based if (wallet is! MnemonicInterface) { - throw Exception("FIXME ~= see todo in code"); + if (wallet is BitcoinFrostWallet) { + frostData = ( + keys: (await wallet.getMultisigConfig())!, + config: (await wallet.getMultisigConfig())!, + ); + } else { + throw Exception("FIXME ~= see todo in code"); + } + } else { + words = await wallet.getMnemonicAsWords(); } - final words = await wallet.getMnemonicAsWords(); - if (mounted) { await Navigator.of(context).pushReplacementNamed( WalletKeysDesktopPopup.routeName, - arguments: words, + arguments: ( + mnemonic: words ?? [], + frostData: frostData, + ), ); } } else { @@ -301,21 +314,35 @@ class _UnlockWalletKeysDesktopState if (verified) { Navigator.of(context, rootNavigator: true).pop(); + ({String keys, String config})? frostData; + List? words; + final wallet = ref.read(pWallets).getWallet(widget.walletId); // TODO: [prio=low] handle wallets that don't have a mnemonic // All wallets currently are mnemonic based if (wallet is! MnemonicInterface) { - throw Exception("FIXME ~= see todo in code"); + if (wallet is BitcoinFrostWallet) { + frostData = ( + keys: (await wallet.getMultisigConfig())!, + config: (await wallet.getMultisigConfig())!, + ); + } else { + throw Exception("FIXME ~= see todo in code"); + } + } else { + words = await wallet.getMnemonicAsWords(); } - final words = await wallet.getMnemonicAsWords(); if (mounted) { await Navigator.of(context) .pushReplacementNamed( WalletKeysDesktopPopup.routeName, - arguments: words, + arguments: ( + mnemonic: words ?? [], + frostData: frostData, + ), ); } } else { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart index 14574a083..60f0d2436 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart @@ -29,10 +29,12 @@ class WalletKeysDesktopPopup extends StatelessWidget { const WalletKeysDesktopPopup({ Key? key, required this.words, + this.frostData, this.clipboardInterface = const ClipboardWrapper(), }) : super(key: key); final List words; + final ({String keys, String config})? frostData; final ClipboardInterface clipboardInterface; static const String routeName = "walletKeysDesktopPopup"; @@ -66,85 +68,145 @@ class WalletKeysDesktopPopup extends StatelessWidget { const SizedBox( height: 28, ), - Text( - "Recovery phrase", - style: STextStyles.desktopTextMedium(context), - ), - const SizedBox( - height: 8, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Text( - "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", - style: STextStyles.desktopTextExtraExtraSmall(context), - textAlign: TextAlign.center, - ), - ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: MnemonicTable( - words: words, - isDesktop: true, - itemBorderColor: Theme.of(context) - .extension()! - .buttonBackSecondary, - ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Show QR code", - onPressed: () { - final String value = AddressUtils.encodeQRSeedData(words); - Navigator.of(context).pushNamed( - QRCodeDesktopPopupContent.routeName, - arguments: value, - ); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Copy", - onPressed: () async { - await clipboardInterface.setData( - ClipboardData(text: words.join(" ")), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, + frostData != null + ? Column( + children: [ + Text( + "Keys", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, ), - ); - }, - ), + child: SelectableText( + frostData!.keys, + style: + STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Text( + "Config", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: SelectableText( + frostData!.config, + style: + STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + ], + ) + : Column( + children: [ + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: MnemonicTable( + words: words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension()! + .buttonBackSecondary, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show QR code", + onPressed: () { + // TODO: address utils + final String value = + AddressUtils.encodeQRSeedData(words); + Navigator.of(context).pushNamed( + QRCodeDesktopPopupContent.routeName, + arguments: value, + ); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: () async { + await clipboardInterface.setData( + ClipboardData(text: words.join(" ")), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + } + }, + ), + ), + ], + ), + ), + ], ), - ], - ), - ), const SizedBox( height: 32, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 26e24653c..3afd3e5dc 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1403,12 +1403,33 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case WalletBackupView.routeName: - if (args is Tuple2>) { + if (args is ({String walletId, List mnemonic})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => WalletBackupView( - walletId: args.item1, - mnemonic: args.item2, + walletId: args.walletId, + mnemonic: args.mnemonic, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } else if (args is ({ + String walletId, + List mnemonic, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? frostWalletData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + frostWalletData: args.frostWalletData, ), settings: RouteSettings( name: settings.name, @@ -2313,10 +2334,14 @@ class RouteGenerator { settings: RouteSettings(name: settings.name)); case WalletKeysDesktopPopup.routeName: - if (args is List) { + if (args is ({ + List mnemonic, + ({String keys, String config})? frostData + })) { return FadePageRoute( WalletKeysDesktopPopup( - words: args, + words: args.mnemonic, + frostData: args.frostData, ), RouteSettings( name: settings.name, diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 851c41eed..2a50f20b6 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -925,7 +925,7 @@ class BitcoinFrostWallet extends Wallet { ); } - Future _getSerializedKeysPrevGen() async => + Future getSerializedKeysPrevGen() async => await secureStorageInterface.read( key: "{$walletId}_serializedFROSTKeysPrevGen", ); @@ -935,7 +935,7 @@ class BitcoinFrostWallet extends Wallet { key: "{$walletId}_multisigConfig", ); - Future _multisigConfigPrevGen() async => + Future getMultisigConfigPrevGen() async => await secureStorageInterface.read( key: "{$walletId}_multisigConfigPrevGen", ); From 73276ba676d3a6edee43db6924632b78bdc66220 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 23 Feb 2024 17:46:34 -0600 Subject: [PATCH 026/272] update frost wallet for electrum_adapter functionality pulled from electrumx_interface, might consider using those methods instead --- .../wallet/impl/bitcoin_frost_wallet.dart | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 2a50f20b6..097769260 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:ffi'; +import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; +import 'package:electrum_adapter/electrum_adapter.dart'; import 'package:flutter/foundation.dart'; import 'package:frostdart/frostdart.dart' as frost; import 'package:frostdart/frostdart_bindings_generated.dart'; @@ -18,15 +20,19 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stream_channel/stream_channel.dart'; class BitcoinFrostWallet extends Wallet { BitcoinFrostWallet(CryptoCurrencyNetwork network) @@ -38,6 +44,8 @@ class BitcoinFrostWallet extends Wallet { .findFirstSync()!; late ElectrumXClient electrumXClient; + late StreamChannel electrumAdapterChannel; + late ElectrumClient electrumAdapterClient; late CachedElectrumXClient electrumXCachedClient; Future initializeNewFrost({ @@ -1075,6 +1083,7 @@ class BitcoinFrostWallet extends Wallet { ); } + // TODO [prio=low]: Use ElectrumXInterface method. Future _updateElectrumX() async { final failovers = nodeService .failoverNodesFor(coin: cryptoCurrency.coin) @@ -1088,16 +1097,67 @@ class BitcoinFrostWallet extends Wallet { .toList(); final newNode = await _getCurrentElectrumXNode(); + try { + await electrumXClient.electrumAdapterClient?.close(); + } catch (e, s) { + if (e.toString().contains("initialized")) { + // Ignore. This should happen every first time the wallet is opened. + } else { + Logging.instance + .log("Error closing electrumXClient: $e", level: LogLevel.Error); + } + } electrumXClient = ElectrumXClient.from( node: newNode, prefs: prefs, failovers: failovers, + coin: cryptoCurrency.coin, ); + electrumAdapterChannel = await electrum_adapter.connect( + newNode.address, + port: newNode.port, + acceptUnverified: true, + useSSL: newNode.useSSL, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + if (electrumXClient.coin == Coin.firo || + electrumXClient.coin == Coin.firoTestNet) { + electrumAdapterClient = FiroElectrumClient( + electrumAdapterChannel, + newNode.address, + newNode.port, + newNode.useSSL, + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null); + } else { + electrumAdapterClient = ElectrumClient( + electrumAdapterChannel, + newNode.address, + newNode.port, + newNode.useSSL, + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null); + } electrumXCachedClient = CachedElectrumXClient.from( electrumXClient: electrumXClient, + electrumAdapterClient: electrumAdapterClient, + electrumAdapterUpdateCallback: updateClient, ); } + // TODO [prio=low]: Use ElectrumXInterface method. + Future updateClient() async { + Logging.instance.log( + "Updating electrum node and ElectrumAdapterClient from Frost wallet.", + level: LogLevel.Info); + await updateNode(); + return electrumAdapterClient; + } + bool _duplicateTxCheck( List> allTransactions, String txid) { for (int i = 0; i < allTransactions.length; i++) { From bbfb152bd741cd7a16bb24319ad517df9ea43619 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 23 Feb 2024 17:48:45 -0600 Subject: [PATCH 027/272] add bitcoin frost cases to validation switch i'd like to do this more elegantly and just use each wallet impl's validateAddress but this will do for now --- lib/utilities/address_utils.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 0e766d28e..563ca69fd 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -14,10 +14,9 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:crypto/crypto.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart'; -import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; - import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart'; @@ -32,6 +31,7 @@ import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; class AddressUtils { static String condenseAddress(String address) { @@ -72,6 +72,9 @@ class AddressUtils { switch (coin) { case Coin.bitcoin: return Bitcoin(CryptoCurrencyNetwork.main).validateAddress(address); + case Coin.bitcoinFrost: + return BitcoinFrost(CryptoCurrencyNetwork.main) + .validateAddress(address); case Coin.litecoin: return Litecoin(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.bitcoincash: @@ -104,6 +107,9 @@ class AddressUtils { return Tezos(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.bitcoinTestNet: return Bitcoin(CryptoCurrencyNetwork.test).validateAddress(address); + case Coin.bitcoinFrostTestNet: + return BitcoinFrost(CryptoCurrencyNetwork.test) + .validateAddress(address); case Coin.litecoinTestNet: return Litecoin(CryptoCurrencyNetwork.test).validateAddress(address); case Coin.bitcoincashTestnet: From 26b4b4b888e2544f6b99c7f49d2825b136edfdc4 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 4 Mar 2024 16:33:26 -0600 Subject: [PATCH 028/272] flutter_libepiccash: make sure to cd to build dir after building openssl --- crypto_plugins/flutter_libepiccash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index cef5d3aa8..eea3d7674 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit cef5d3aa8c74c8dc9a466f803c964b243ad653a3 +Subproject commit eea3d76740f7b036451850c937c3c013e5724294 From 445fc832a3ab6fda088f0ed7dc69661077b2a305 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 6 Mar 2024 10:16:51 -0600 Subject: [PATCH 029/272] center "import sign config" button --- .../wallet_view/sub_widgets/my_wallet.dart | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 01ff6801f..5b0cac5f7 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -76,19 +76,23 @@ class _MyWalletState extends ConsumerState { ? Column( children: [ Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, children: [ - SecondaryButton( - width: 200, - buttonHeight: ButtonHeight.l, - label: "Import sign config", - onPressed: () { - Navigator.of(context).pushNamed( - FrostImportSignConfigView.routeName, - arguments: widget.walletId, - ); - }, - ), + Padding( + padding: + const EdgeInsets.fromLTRB(0, 20, 0, 0), + child: SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Import sign config", + onPressed: () { + Navigator.of(context).pushNamed( + FrostImportSignConfigView.routeName, + arguments: widget.walletId, + ); + }, + ), + ) ], ), FrostSendView( From 5d1615b72ef363514f99329316cf24a1811d456b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 6 Mar 2024 10:55:15 -0600 Subject: [PATCH 030/272] fix keys popup, add copy buttons, and add basic style and import cleanup --- .../wallet_backup_view.dart | 3 +- .../unlock_wallet_keys_desktop.dart | 6 +- .../wallet_keys_desktop_popup.dart | 62 ++++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index 5b1548514..fee8781ff 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -17,6 +17,7 @@ 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'; @@ -32,8 +33,6 @@ import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -import '../../../wallet_view/transaction_views/transaction_details_view.dart'; - class WalletBackupView extends ConsumerWidget { const WalletBackupView({ Key? key, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index c621a4030..163052ec0 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -89,9 +89,11 @@ class _UnlockWalletKeysDesktopState if (wallet is! MnemonicInterface) { if (wallet is BitcoinFrostWallet) { frostData = ( - keys: (await wallet.getMultisigConfig())!, + keys: (await wallet.getSerializedKeys())!, config: (await wallet.getMultisigConfig())!, ); + print(1111111); + print(frostData); } else { throw Exception("FIXME ~= see todo in code"); } @@ -325,7 +327,7 @@ class _UnlockWalletKeysDesktopState if (wallet is! MnemonicInterface) { if (wallet is BitcoinFrostWallet) { frostData = ( - keys: (await wallet.getMultisigConfig())!, + keys: (await wallet.getSerializedKeys())!, config: (await wallet.getMultisigConfig())!, ); } else { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart index 60f0d2436..606ae21f4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/address_utils.dart'; @@ -24,6 +25,7 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletKeysDesktopPopup extends StatelessWidget { const WalletKeysDesktopPopup({ @@ -83,11 +85,31 @@ class WalletKeysDesktopPopup extends StatelessWidget { padding: const EdgeInsets.symmetric( horizontal: 32, ), - child: SelectableText( - frostData!.keys, - style: - STextStyles.desktopTextExtraExtraSmall(context), - textAlign: TextAlign.center, + child: RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 9), + child: Row( + children: [ + Flexible( + child: SelectableText( + frostData!.keys, + style: STextStyles.desktopTextExtraExtraSmall( + context), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + width: 10, + ), + IconCopyButton( + data: frostData!.keys, + ) + // TODO [prio=low: Add QR code button and dialog. + ], + ), ), ), ), @@ -106,11 +128,31 @@ class WalletKeysDesktopPopup extends StatelessWidget { padding: const EdgeInsets.symmetric( horizontal: 32, ), - child: SelectableText( - frostData!.config, - style: - STextStyles.desktopTextExtraExtraSmall(context), - textAlign: TextAlign.center, + child: RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 9), + child: Row( + children: [ + Flexible( + child: SelectableText( + frostData!.config, + style: STextStyles.desktopTextExtraExtraSmall( + context), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + width: 10, + ), + IconCopyButton( + data: frostData!.config, + ) + // TODO [prio=low: Add QR code button and dialog. + ], + ), ), ), ), From d59be57667067b34ce82c03ab79892229b8de55e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 6 Mar 2024 11:17:19 -0600 Subject: [PATCH 031/272] update flutter_libepiccash build script --- crypto_plugins/flutter_libepiccash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index eea3d7674..72b6ce405 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit eea3d76740f7b036451850c937c3c013e5724294 +Subproject commit 72b6ce405a956f163ffb25d458de28a8458223f4 From f558703253116b4b13e775210bdec3629d23d356 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 6 Mar 2024 18:04:54 -0600 Subject: [PATCH 032/272] DesktopScaffold on desktop --- .../frost_ms/frost_ms_options_view.dart | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index 169a96b64..7fc7236a4 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -13,14 +13,19 @@ import 'package:flutter_riverpod/flutter_riverpod.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/resharing/involved/step_1a/begin_reshare_config_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_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/constants.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 FrostMSWalletOptionsView extends ConsumerWidget { @@ -35,21 +40,40 @@ class FrostMSWalletOptionsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Multisig settings", - style: STextStyles.navBarTitle(context), - ), + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), ), - body: Padding( + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "FROST Multisig options", + style: STextStyles.navBarTitle(context), + ), + ), + body: child), + ), + child: Padding( padding: const EdgeInsets.only( top: 12, left: 16, From 809cbe6195e4d944563745d2f87381c6a98f41b8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 6 Mar 2024 18:09:38 -0600 Subject: [PATCH 033/272] FROST Multisig settings buttons mobile and desktop --- .../wallet_settings_view.dart | 16 +++++ .../sub_widgets/wallet_options_button.dart | 60 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index e0b870326..04de48cb4 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -22,6 +22,7 @@ import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; @@ -194,6 +195,21 @@ class _WalletSettingsViewState extends ConsumerState { padding: const EdgeInsets.all(4), child: Column( children: [ + if (coin == Coin.bitcoinFrost || + coin == Coin.bitcoinFrostTestNet) + if (coin == Coin.bitcoinFrost || + coin == Coin.bitcoinFrostTestNet) + SettingsListButton( + iconAssetName: Assets.svg.addressBook2, + iconSize: 16, + title: "FROST Multisig settings", + onPressed: () { + Navigator.of(context).pushNamed( + FrostMSWalletOptionsView.routeName, + arguments: walletId, + ); + }, + ), SettingsListButton( iconAssetName: Assets.svg.addressBook, iconSize: 16, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index f495475db..c27b7855d 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -14,6 +14,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; @@ -34,7 +35,8 @@ enum _WalletOptions { changeRepresentative, showXpub, lelantusCoins, - sparkCoins; + sparkCoins, + frostOptions; String get prettyName { switch (this) { @@ -50,6 +52,8 @@ enum _WalletOptions { return "Lelantus Coins"; case _WalletOptions.sparkCoins: return "Spark Coins"; + case _WalletOptions.frostOptions: + return "FROST settings"; } } } @@ -96,6 +100,9 @@ class WalletOptionsButton extends StatelessWidget { onFiroShowSparkCoins: () async { Navigator.of(context).pop(_WalletOptions.sparkCoins); }, + onFrostMSWalletOptionsPressed: () async { + Navigator.of(context).pop(_WalletOptions.frostOptions); + }, walletId: walletId, ); }, @@ -207,6 +214,15 @@ class WalletOptionsButton extends StatelessWidget { ), ); break; + + case _WalletOptions.frostOptions: + unawaited( + Navigator.of(context).pushNamed( + FrostMSWalletOptionsView.routeName, + arguments: walletId, + ), + ); + break; } } }, @@ -241,6 +257,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { required this.onChangeRepPressed, required this.onFiroShowLelantusCoins, required this.onFiroShowSparkCoins, + required this.onFrostMSWalletOptionsPressed, required this.walletId, }) : super(key: key); @@ -250,6 +267,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { final VoidCallback onChangeRepPressed; final VoidCallback onFiroShowLelantusCoins; final VoidCallback onFiroShowSparkCoins; + final VoidCallback onFrostMSWalletOptionsPressed; final String walletId; @override @@ -265,6 +283,9 @@ class WalletOptionsPopupMenu extends ConsumerWidget { final bool canChangeRep = coin == Coin.nano || coin == Coin.banano; + final bool isFrost = + coin == Coin.bitcoinFrost || coin == Coin.bitcoinFrostTestNet; + return Stack( children: [ Positioned( @@ -429,6 +450,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget { ), ), ), + if (isFrost) + const SizedBox( + height: 8, + ), + if (isFrost) + TransparentButton( + onPressed: onFrostMSWalletOptionsPressed, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.addressBookDesktop, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + const SizedBox(width: 14), + Expanded( + child: Text( + _WalletOptions.frostOptions.prettyName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], + ), + ), + ), if (xpubEnabled) const SizedBox( height: 8, From bfdcfcec1aeb734d26f5396d22c89f764978186a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 6 Mar 2024 18:13:39 -0600 Subject: [PATCH 034/272] resolve "can't add to fixed length list" exception --- .../involved/step_1a/complete_reshare_config_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart index 0e2e1e111..74cfaee17 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart @@ -93,14 +93,14 @@ class _CompleteReshareConfigViewState ), ); } else { - final salts = frostInfo.knownSalts; - salts.add(salt); + final salts = frostInfo.knownSalts; // Fixed length list. + final newSalts = List.from(salts)..add(salt); final mainDB = ref.read(mainDBProvider); await mainDB.isar.writeTxn(() async { final info = frostInfo; await mainDB.isar.frostWalletInfo.delete(info.id); await mainDB.isar.frostWalletInfo.put( - info.copyWith(knownSalts: salts), + info.copyWith(knownSalts: newSalts), ); }); } From d2841121202128d4300054d28a175e104675d1cb Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Mar 2024 21:20:59 -0600 Subject: [PATCH 035/272] Windows (and Rust in general) docs updates --- docs/building.md | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/building.md b/docs/building.md index e7128df2e..cf02f5219 100644 --- a/docs/building.md +++ b/docs/building.md @@ -9,7 +9,7 @@ Here you will find instructions on how to install the necessary tools for buildi - 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 [Windows](#Windows-host) section. ### 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: @@ -29,6 +29,8 @@ Then in `File > Settings > Plugins`, install the **Flutter** and **Dart** plugin Make a Pixel 4 (API 30) x86_64 emulator with 2GB of storage space for emulation +### 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 @@ -43,19 +45,22 @@ 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.72.0 # For tor. +rustup install 1.67.1 # For flutter_libepiccash. 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,9 +72,11 @@ 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 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 @@ -134,12 +141,7 @@ flutter run linux 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. ### 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` +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 MXE on the WSL2 Ubuntu 20.04 host by running `stack_wallet/scripts/windows/deps.sh` ```sh ./stack_wallet/scripts/windows/deps.sh ``` @@ -158,10 +160,23 @@ Copy the resulting `dll`s to their respective positions on the Windows host: --> -### Install Flutter on Windows host +### Flutter 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. -### Dependencies +### Rust +Install [Rust](https://www.rust-lang.org/tools/install) via `rustup`. Make sure it works and install the following versions: +``` +rustup install 1.72.0 # For tor. +rustup install 1.67.1 # For flutter_libepiccash. +rustup default 1.67.1 +``` + +Also install `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, From 05080ea80ac551cab7e35b6c96ca19a60e1f6a60 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Mar 2024 21:38:17 -0600 Subject: [PATCH 036/272] add coinlib, VS, gtk, and flutter 3.16.0 notes --- docs/building.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/building.md b/docs/building.md index cf02f5219..7685abdbf 100644 --- a/docs/building.md +++ b/docs/building.md @@ -119,6 +119,12 @@ cd scripts/windows ./build_all.sh ``` +### 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` + + ### Running #### Android Plug in your android device or use the emulator available via Android Studio and then run the following commands: @@ -138,13 +144,20 @@ 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. +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++" and "Linux development with C++" workloads. You may also need the Windows 10, 11, and/or Universal SDK workloads depending on your Windows version. ### 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 MXE on the WSL2 Ubuntu 20.04 host by running `stack_wallet/scripts/windows/deps.sh` - ```sh - ./stack_wallet/scripts/windows/deps.sh - ``` +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. + +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: @@ -161,10 +174,10 @@ Copy the resulting `dll`s to their respective positions on the Windows host: ### Flutter -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. +Install Flutter 3.16.0 on the 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` (you may need to open a new terminal). Run `flutter doctor` in PowerShell to confirm its installation. ### Rust -Install [Rust](https://www.rust-lang.org/tools/install) via `rustup`. Make sure it works and install the following versions: +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 tor. rustup install 1.67.1 # For flutter_libepiccash. @@ -192,6 +205,11 @@ winget install Microsoft.Windows.CppWinRT -Version 2.0.210806.1 or [download the package](https://www.nuget.org/packages/Microsoft.Windows.CppWinRT/2.0.210806.1) and [manually install it](https://github.com/Baseflow/flutter-permission-handler/issues/1025#issuecomment-1518576722) by placing it in `flutter/bin` with [nuget.exe](https://dist.nuget.org/win-x86-commandline/latest/nuget.exe) and installing by running `nuget install Microsoft.Windows.CppWinRT -Version 2.0.210806.1` in the root `stack_wallet` folder. +### Build coinlib +Coinlib's native secp256k1 library must be built prior to running Stack Wallet. It can be run from within the root `stack_wallet` folder on a... + - Windows host: `dart run coinlib:build_windows` + - WSL2 host: `dart run coinlib:build_wsl` + ### 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 @@ -201,7 +219,7 @@ cd scripts // when finished go back to the root directory cd .. ``` -or manually by creating the files referenced in that script with the specified content. +or manually by creating the files referenced in that script with the specified content. ### Running From 0b4d5db02165e1f9f4bdebc2627252431365d1b6 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 7 Mar 2024 22:31:32 -0600 Subject: [PATCH 037/272] add coinlib section and general rearrangement --- docs/building.md | 71 ++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/docs/building.md b/docs/building.md index 7685abdbf..0af3882ae 100644 --- a/docs/building.md +++ b/docs/building.md @@ -9,7 +9,7 @@ Here you will find instructions on how to install the necessary tools for buildi - 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. This entire section (except for the Android Studio section) needs to be completed in WSL if building on a Windows host. ### 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: @@ -27,7 +27,12 @@ Use `Tools > SDK Manager` to install: 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 +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 @@ -36,11 +41,6 @@ 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 ``` -The following *may* be needed for Android studio: -``` -sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-1.0:i386 -``` - Install [Rust](https://www.rust-lang.org/tools/install) with command: ``` curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -74,18 +74,24 @@ cd stack_wallet git submodule update --init --recursive ``` -### 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. +Build the secure storage dependencies in order to target Linux (not needed for Windows or other platforms): +``` +cd scripts/linux +./build_secure_storage_deps.sh +// when finished go back to the root directory +cd ../.. +``` + +### 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` + + +For Windows targets, you can use a `secp256k1.dll` produced by any of the three bottom options if the first attempts doesn't succeed! -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.*' -``` - ### Run prebuild script @@ -112,19 +118,25 @@ cd scripts/linux ./build_all.sh ``` +##### Remove system packages (may be needed for building flutter_libmonero) +[`flutter_libmonero`](https://github.com/cypherstack/flutter_libmonero) may have issues building due to conflicts with system packages: if so, follow this section. + +Remove pre-installed system libraries for the following packages built by cryptography plugins in the crypto_plugins folder: `boost iconv libjson-dev libsecret openssl sodium unbound zmq`. You can use +``` +sudo apt list --installed | grep boost +``` +for example to find which pre-installed packages you may need to remove with `sudo apt remove`. Be careful, as some packages (especially boost) are linked to GNOME (GUI) packages: when in doubt, remove `-dev` packages first like with +``` +sudo apt-get remove '^libboost.*-dev.*' +``` + + #### Building plugins for Windows ``` cd scripts/windows ./deps.sh ./build_all.sh ``` - -### 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` - - ### Running #### Android Plug in your android device or use the emulator available via Android Studio and then run the following commands: @@ -144,7 +156,7 @@ 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++" and "Linux development with C++" workloads. You may also need the Windows 10, 11, and/or Universal SDK workloads depending on your Windows version. +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. ### 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. @@ -205,14 +217,9 @@ winget install Microsoft.Windows.CppWinRT -Version 2.0.210806.1 or [download the package](https://www.nuget.org/packages/Microsoft.Windows.CppWinRT/2.0.210806.1) and [manually install it](https://github.com/Baseflow/flutter-permission-handler/issues/1025#issuecomment-1518576722) by placing it in `flutter/bin` with [nuget.exe](https://dist.nuget.org/win-x86-commandline/latest/nuget.exe) and installing by running `nuget install Microsoft.Windows.CppWinRT -Version 2.0.210806.1` in the root `stack_wallet` folder. -### Build coinlib -Coinlib's native secp256k1 library must be built prior to running Stack Wallet. It can be run from within the root `stack_wallet` folder on a... - - Windows host: `dart run coinlib:build_windows` - - WSL2 host: `dart run coinlib:build_wsl` - ### 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 as in ``` cd scripts ./prebuild.ps1 From ed42dba9cc6eda3745898cd969b9fc74a032c75e Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 8 Mar 2024 19:31:34 +0200 Subject: [PATCH 038/272] Update address to match epicbox config --- lib/wallets/wallet/impl/epiccash_wallet.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 8228ab7c8..015faa00c 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -317,6 +317,7 @@ class EpiccashWallet extends Bip39Wallet { Future

_generateAndStoreReceivingAddressForIndex( int index, ) async { + Address? address = await getCurrentReceivingAddress(); EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); @@ -325,6 +326,7 @@ class EpiccashWallet extends Bip39Wallet { //Check if the address is the same as the current epicbox domain //Since we're only using one epicbpox now this doesn't apply but will be // useful in the future + final encodedConfig = jsonEncode(epicboxConfig); if (splitted[1] != epicboxConfig.host) { //Update the address address = await thisWalletAddress(index, epicboxConfig); @@ -332,6 +334,13 @@ class EpiccashWallet extends Bip39Wallet { } else { address = await thisWalletAddress(index, epicboxConfig); } + + if (info.cachedReceivingAddress != address.value) { + await info.updateReceivingAddress( + newAddress: address.value, + isar: mainDB.isar, + ); + } return address; } @@ -360,7 +369,6 @@ class EpiccashWallet extends Bip39Wallet { subType: AddressSubType.receiving, publicKey: [], // ?? ); - await mainDB.updateOrPutAddresses([address]); return address; } From e6fc739192942cf86e37d7f85474640efd0e902e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 8 Mar 2024 14:25:49 -0600 Subject: [PATCH 039/272] add docker/podman note for coinlib --- docs/building.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index 0af3882ae..e3d0cebaf 100644 --- a/docs/building.md +++ b/docs/building.md @@ -90,7 +90,9 @@ Coinlib's native secp256k1 library must be built prior to running Stack Wallet. - WSL2 host: `dart run coinlib:build_wsl` -For Windows targets, you can use a `secp256k1.dll` produced by any of the three bottom options if the first attempts doesn't succeed! +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 bottom options if the first attempt doesn't succeed! ### Run prebuild script From 6f98abddf78108aa6f976936753fc0410637bfd1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 9 Mar 2024 06:12:10 -0800 Subject: [PATCH 040/272] nit that android studio is not necessarily needed in wsl --- docs/building.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index e3d0cebaf..9f220f759 100644 --- a/docs/building.md +++ b/docs/building.md @@ -161,7 +161,7 @@ flutter run linux 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. ### 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. +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: ``` From 114ec4cd1011fe58081b0d36bd632be66a503181 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 9 Mar 2024 15:43:50 -0800 Subject: [PATCH 041/272] expand upon flutter installation method --- docs/building.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index 9f220f759..b4385220a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -34,6 +34,15 @@ The following *may* be needed for Android studio: sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-1.0:i386 ``` +### Flutter + +Flutter and the Dart SDK should have been set up by Android studio, but if running `flutter` doesn't work (try `flutter doctor`, too), follow the [guide to install Flutter on any of their supported platforms](https://docs.flutter.dev/get-started/install) or: + - `git clone https://github.com/flutter/flutter` somewhere it can live (`/var`, `/opt`, `~`) + - `git checkout 3.16.0` after navigating into the `flutter` directory, and + - add `flutter/bin` to your PATH (on Ubuntu, add `PATH=$PATH:path/to/flutter/bin` to `~/.profile`). + +Run `flutter doctor` to install any missing dependencies and review and agree to any license agreements. + ### Build dependencies Install basic dependencies @@ -188,7 +197,11 @@ Copy the resulting `dll`s to their respective positions on the Windows host: ### Flutter -Install Flutter 3.16.0 on the 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` (you may need to open a new terminal). Run `flutter doctor` in PowerShell to confirm its installation. +Install Flutter 3.16.0 on the Windows host (not in WSL2) by following [Flutter's Windows install guide](https://docs.flutter.dev/get-started/install/windows), by running `scripts/windows/deps.ps1`, or by + - `git clone https://github.com/flutter/flutter` somewhere it can live (`C:`, **avoid** anywhere in `C:/Users/`), + - `git checkout 3.16.0` (after navigating into the `flutter` folder), + - and adding `flutter/bin` to your PATH environmen variable (search "environment variables" in the Start menu) +You may still have to add `C:\development\flutter\bin` to PATH before proceeding, even if you ran `deps.ps1` (you may need to open a new terminal). 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: From 557d3fd07d44a4e129fde9bc7e98bb0924087f32 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 9 Mar 2024 17:55:27 -0600 Subject: [PATCH 042/272] android nit --- docs/building.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/building.md b/docs/building.md index b4385220a..5bd2016e7 100644 --- a/docs/building.md +++ b/docs/building.md @@ -20,10 +20,11 @@ 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. From 88afa95b52801d05a7b568f680ee1f61928d8844 Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 11 Mar 2024 14:32:40 +0200 Subject: [PATCH 043/272] Update to latest epic --- crypto_plugins/flutter_libepiccash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index cef5d3aa8..aab6a4676 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit cef5d3aa8c74c8dc9a466f803c964b243ad653a3 +Subproject commit aab6a4676188901fbe158d8f1feeb1fc0ea247f8 From 09bbdb536847011a7cf0ef7f041adb60808e8e31 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 11 Mar 2024 11:20:41 -0500 Subject: [PATCH 044/272] add missing frost routes --- lib/route_generator.dart | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 3afd3e5dc..6f298a7c0 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -83,6 +83,8 @@ import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.d import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/send_view/token_send_view.dart'; @@ -775,6 +777,34 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FrostCreateSignConfigView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostCreateSignConfigView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FrostAttemptSignConfigView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostAttemptSignConfigView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // case MonkeyLoadedView.routeName: // if (args is Tuple2>) { // return getRoute( From 10233550b187c7a3a879f4c09d2358551c4c8ce1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 11 Mar 2024 11:21:19 -0500 Subject: [PATCH 045/272] fix issue with modifying fixed-length list --- .../send_view/frost_ms/frost_attempt_sign_config_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart index 7cdf4a69b..149eb61d3 100644 --- a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart +++ b/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart @@ -78,7 +78,7 @@ class _FrostAttemptSignConfigViewState myName = frostInfo.myName; threshold = frostInfo.threshold; - participantsWithoutMe = frostInfo.participants; + participantsWithoutMe = List.from(frostInfo.participants); // Copy so it isn't fixed-length. myIndex = participantsWithoutMe.indexOf(frostInfo.myName); myPreprocess = ref.read(pFrostAttemptSignData.state).state!.preprocess; From 181ec5e5398d94f60b32397d53f0a899171e4799 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 11 Mar 2024 23:05:55 -0500 Subject: [PATCH 046/272] Revert "wrap send view content in padding" This reverts commit 2aa3bebf78280f56650c3f0f7be5e7bf7a8964cf. --- .../send_view/frost_ms/frost_send_view.dart | 384 +++++++++--------- 1 file changed, 188 insertions(+), 196 deletions(-) diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 95d45cb95..1865556b7 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -391,215 +391,207 @@ class _FrostSendViewState extends ConsumerState { const SizedBox( height: 16, ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipients", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: "Add", + onTap: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + 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], + coin: coin, + onChanged: () { + _validateRecipientFormStates(); + }, + remove: i == 0 && recipientWidgetIndexes.length == 1 + ? null + : () { + recipientWidgetIndexes.removeAt(i); + setState(() {}); + }, + ), + ), + ], + ), + if (showCoinControl) + const SizedBox( + height: 8, ), - child: Column(children: [ - Row( + if (showCoinControl) + RoundedWhiteContainer( + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Recipients", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, + "Coin control", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), ), CustomTextButton( - text: "Add", - onTap: () { - // used for tracking recipient forms - _greatestWidgetIndex++; - recipientWidgetIndexes.add(_greatestWidgetIndex); - setState(() {}); + text: selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", + onTap: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + } + + if (mounted) { + // finally spendable = ref + // .read(walletsChangeNotifierProvider) + // .getManager(widget.walletId) + // .balance + // .spendable; + + // TODO: [prio=high] make sure this coincontrol works correctly + + Amount? amount; + + final result = await Navigator.of(context).pushNamed( + CoinControlView.routeName, + arguments: Tuple4( + walletId, + CoinControlViewType.use, + amount, + selectedUTXOs, + ), + ); + + if (result is Set) { + setState(() { + selectedUTXOs = result; + }); + } + } }, ), ], ), - const SizedBox( - height: 8, - ), - 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], - coin: coin, - onChanged: () { - _validateRecipientFormStates(); - }, - remove: i == 0 && recipientWidgetIndexes.length == 1 - ? null - : () { - recipientWidgetIndexes.removeAt(i); - setState(() {}); - }, - ), - ), - ], - ), - if (showCoinControl) - const SizedBox( - height: 8, - ), - if (showCoinControl) - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Coin control", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - CustomTextButton( - text: selectedUTXOs.isEmpty - ? "Select coins" - : "Selected coins (${selectedUTXOs.length})", - onTap: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); - } - - if (mounted) { - // finally spendable = ref - // .read(walletsChangeNotifierProvider) - // .getManager(widget.walletId) - // .balance - // .spendable; - - // 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, + ), + 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 = ""; + }); + }, ), - ); - - if (result is Set) { - setState(() { - selectedUTXOs = result; - }); - } - } - }, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Note (optional)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: noteController, - focusNode: _noteFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type something...", - _noteFocusNode, - context, - ).copyWith( - suffixIcon: noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - noteController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 12, - ), - Padding( - padding: const EdgeInsets.only( - bottom: 12, - top: 16, - ), - child: FeeSlider( - coin: coin, - onSatVByteChanged: (rate) { - customFeeRate = rate; - }, - ), - ), - Util.isDesktop - ? const SizedBox( - height: 12, - ) - : const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: ref.watch(previewTxButtonStateProvider.state).state - ? _createSignConfig + ], + ), + ), + ) : null, - style: ref.watch(previewTxButtonStateProvider.state).state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Create config", - style: STextStyles.button(context), - ), ), - const SizedBox( - height: 16, - ), - ]), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + bottom: 12, + top: 16, + ), + child: FeeSlider( + coin: coin, + onSatVByteChanged: (rate) { + customFeeRate = rate; + }, + ), + ), + Util.isDesktop + ? const SizedBox( + height: 12, + ) + : const Spacer(), + const SizedBox( + height: 12, + ), + TextButton( + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? _createSignConfig + : null, + style: ref.watch(previewTxButtonStateProvider.state).state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Create config", + style: STextStyles.button(context), + ), + ), + const SizedBox( + height: 16, ), ], ), From 0fe16638b04a8047f14caac94745bb565e6b5869 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 12 Mar 2024 06:45:26 -0500 Subject: [PATCH 047/272] pad desktop send view with ConditionalParent --- .../send_view/frost_ms/frost_send_view.dart | 595 +++++++++--------- 1 file changed, 303 insertions(+), 292 deletions(-) diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 1865556b7..5f04bb346 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -287,313 +287,324 @@ class _FrostSendViewState extends ConsumerState { ), ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!Util.isDesktop) - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - children: [ - SvgPicture.file( - File( - ref.watch( - coinIconProvider(coin), - ), - ), - width: 22, - height: 22, - ), - const SizedBox( - width: 6, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ref.watch(pWalletName(walletId)), - style: STextStyles.titleBold12(context) - .copyWith(fontSize: 14), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - // const SizedBox( - // height: 2, - // ), - Text( - "Available balance", - style: - STextStyles.label(context).copyWith(fontSize: 10), - ), - ], - ), - Util.isDesktop - ? const SizedBox( - height: 24, - ) - : const Spacer(), - GestureDetector( - onTap: () { - // cryptoAmountController.text = ref - // .read(pAmountFormatter(coin)) - // .format( - // _cachedBalance!, - // withUnitName: false, - // ); - }, - 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, - ), - // Text( - // "${(manager.balance.spendable.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, - // ) - ], - ), - ), - ) - ], - ), - ), - ), - const SizedBox( - height: 16, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recipients", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - CustomTextButton( - text: "Add", - onTap: () { - // used for tracking recipient forms - _greatestWidgetIndex++; - recipientWidgetIndexes.add(_greatestWidgetIndex); - setState(() {}); - }, - ), - ], - ), - const SizedBox( - height: 8, - ), - 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], - coin: coin, - onChanged: () { - _validateRecipientFormStates(); - }, - remove: i == 0 && recipientWidgetIndexes.length == 1 - ? null - : () { - recipientWidgetIndexes.removeAt(i); - setState(() {}); - }, + child: child, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Util.isDesktop) + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), ), - ], - ), - if (showCoinControl) - const SizedBox( - height: 8, - ), - if (showCoinControl) - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Coin control", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - CustomTextButton( - text: selectedUTXOs.isEmpty - ? "Select coins" - : "Selected coins (${selectedUTXOs.length})", - onTap: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); - } - - if (mounted) { - // finally spendable = ref - // .read(walletsChangeNotifierProvider) - // .getManager(widget.walletId) - // .balance - // .spendable; - - // 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, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch( + coinIconProvider(coin), ), - ); - - if (result is Set) { - setState(() { - selectedUTXOs = result; - }); - } - } - }, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - Text( - "Note (optional)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: noteController, - focusNode: _noteFocusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Type something...", - _noteFocusNode, - context, - ).copyWith( - suffixIcon: noteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( + ), + 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: () { + // cryptoAmountController.text = ref + // .read(pAmountFormatter(coin)) + // .format( + // _cachedBalance!, + // withUnitName: false, + // ); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - noteController.text = ""; - }); - }, + Text( + ref.watch(pAmountFormatter(coin)).format(ref + .watch(pWalletBalance(walletId)) + .spendable), + style: + STextStyles.titleBold12(context).copyWith( + fontSize: 10, + ), + textAlign: TextAlign.right, ), + // Text( + // "${(manager.balance.spendable.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, + // ) ], ), ), ) - : null, + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipients", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: "Add", + onTap: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + 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], + coin: coin, + onChanged: () { + _validateRecipientFormStates(); + }, + remove: i == 0 && recipientWidgetIndexes.length == 1 + ? null + : () { + recipientWidgetIndexes.removeAt(i); + setState(() {}); + }, + ), + ), + ], + ), + if (showCoinControl) + const SizedBox( + height: 8, + ), + if (showCoinControl) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Coin control", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + CustomTextButton( + text: selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", + onTap: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + } + + if (mounted) { + // finally spendable = ref + // .read(walletsChangeNotifierProvider) + // .getManager(widget.walletId) + // .balance + // .spendable; + + // TODO: [prio=high] make sure this coincontrol works correctly + + Amount? amount; + + final result = await Navigator.of(context).pushNamed( + CoinControlView.routeName, + arguments: Tuple4( + walletId, + CoinControlViewType.use, + amount, + selectedUTXOs, + ), + ); + + if (result is Set) { + setState(() { + selectedUTXOs = result; + }); + } + } + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), ), ), - ), - const SizedBox( - height: 12, - ), - Padding( - padding: const EdgeInsets.only( - bottom: 12, - top: 16, + const SizedBox( + height: 12, ), - child: FeeSlider( - coin: coin, - onSatVByteChanged: (rate) { - customFeeRate = rate; - }, + Padding( + padding: const EdgeInsets.only( + bottom: 12, + top: 16, + ), + child: FeeSlider( + coin: coin, + onSatVByteChanged: (rate) { + customFeeRate = rate; + }, + ), ), - ), - Util.isDesktop - ? const SizedBox( - height: 12, - ) - : const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: ref.watch(previewTxButtonStateProvider.state).state - ? _createSignConfig - : null, - style: ref.watch(previewTxButtonStateProvider.state).state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Create config", - style: STextStyles.button(context), + Util.isDesktop + ? const SizedBox( + height: 12, + ) + : const Spacer(), + const SizedBox( + height: 12, ), - ), - const SizedBox( - height: 16, - ), - ], + TextButton( + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? _createSignConfig + : null, + style: ref.watch(previewTxButtonStateProvider.state).state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Create config", + style: STextStyles.button(context), + ), + ), + const SizedBox( + height: 16, + ), + ], + ), ), ); } From 778795139a8fbd3a813fbcc1d7074f229661c004 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 12 Mar 2024 06:48:01 -0500 Subject: [PATCH 048/272] add import sign config route --- lib/route_generator.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 6f298a7c0..4eb906b8a 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -85,6 +85,7 @@ import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/send_view/token_send_view.dart'; @@ -777,6 +778,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FrostImportSignConfigView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostImportSignConfigView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FrostCreateSignConfigView.routeName: if (args is String) { return getRoute( From 64b0f23910807fe7134188028b1263fee84ff887 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 12 Mar 2024 07:05:05 -0500 Subject: [PATCH 049/272] desktop create sign tweaks making things wider and scrollable but the qr code not overflowingly wide --- .../frost_create_sign_config_view.dart | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart index 104160a76..bb2c129ca 100644 --- a/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart +++ b/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart @@ -71,6 +71,8 @@ class _FrostCreateSignConfigViewState @override Widget build(BuildContext context) { + double qrImageSize = + Util.isDesktop ? 360 : MediaQuery.of(context).size.width - 32; return ConditionalParent( condition: Util.isDesktop, builder: (child) => DesktopScaffold( @@ -79,9 +81,11 @@ class _FrostCreateSignConfigViewState isCompactHeight: false, leading: AppBarBackButton(), ), - body: SizedBox( - width: 480, - child: child, + body: SingleChildScrollView( + child: SizedBox( + width: 600, // Was 480, may look better but overflows the bottom. + child: child, + ), ), ), child: ConditionalParent( @@ -126,13 +130,13 @@ class _FrostCreateSignConfigViewState children: [ if (!Util.isDesktop) const Spacer(), SizedBox( - height: MediaQuery.of(context).size.width - 32, + height: qrImageSize, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ QrImageView( data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, - size: MediaQuery.of(context).size.width - 32, + size: qrImageSize, backgroundColor: Theme.of(context).extension()!.background, foregroundColor: Theme.of(context) @@ -142,9 +146,10 @@ class _FrostCreateSignConfigViewState ], ), ), - const SizedBox( - height: 32, - ), + if (!Util.isDesktop) + const SizedBox( + height: 32, + ), DetailItem( title: "Encoded config", detail: ref.watch(pFrostTxData.state).state!.frostMSConfig!, @@ -157,7 +162,7 @@ class _FrostCreateSignConfigViewState ), ), SizedBox( - height: Util.isDesktop ? 64 : 16, + height: Util.isDesktop ? 20 : 16, ), if (!Util.isDesktop) const Spacer( From 95bb47aaf8c3bf2553eb4421b1e8e0d907fdb236 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 12 Mar 2024 07:45:49 -0500 Subject: [PATCH 050/272] fix rescans --- .../wallet/impl/bitcoin_frost_wallet.dart | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 097769260..fdefac4e9 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -698,10 +698,14 @@ class BitcoinFrostWallet extends Wallet { String? multisigConfig, }) async { if (serializedKeys == null || multisigConfig == null) { - throw Exception( - "Failed to recover $runtimeType: " - "Missing serializedKeys and/or multisigConfig.", - ); + serializedKeys = await getSerializedKeys(); + multisigConfig = await getMultisigConfig(); + } + if (serializedKeys == null || multisigConfig == null) { + String err = "${info.coinName} wallet ${info.walletId} had null keys/cfg"; + Logging.instance.log(err, level: LogLevel.Fatal); + throw Exception(err); + // TODO [prio=low]: handle null keys or config. This should not happen. } final coin = info.coin; @@ -719,7 +723,7 @@ class BitcoinFrostWallet extends Wallet { if (!isRescan) { final salt = frost .multisigSalt( - multisigConfig: multisigConfig, + multisigConfig: multisigConfig!, ) .toHex; final knownSalts = _getKnownSalts(); @@ -735,9 +739,9 @@ class BitcoinFrostWallet extends Wallet { await mainDB.deleteWalletBlockchainData(walletId); } - final keys = frost.deserializeKeys(keys: serializedKeys); - await _saveSerializedKeys(serializedKeys); - await _saveMultisigConfig(multisigConfig); + final keys = frost.deserializeKeys(keys: serializedKeys!); + await _saveSerializedKeys(serializedKeys!); + await _saveMultisigConfig(multisigConfig!); final addressString = frost.addressForKeys( network: cryptoCurrency.network == CryptoCurrencyNetwork.main From f68c0f4839f8867ba302cf0cef218bc4cf0ef3a0 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Tue, 12 Mar 2024 16:55:01 -0600 Subject: [PATCH 051/272] Update version (v1.10.3, build 215) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 58b5c7008..fd19a216e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.10.2+214 +version: 1.10.3+215 environment: sdk: ">=3.0.2 <4.0.0" From ec51f954496bd8ae1610cfcacc7c6415c5bfdf9c Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 14 Mar 2024 20:23:41 +0200 Subject: [PATCH 052/272] WIP: Add frost mascot --- assets/images/mascot.png | Bin 0 -> 222983 bytes lib/pages/FrostMascot.dart | 49 ++++++++++++++++++ ...irm_new_frost_ms_wallet_creation_view.dart | 14 +---- .../new/create_new_frost_ms_wallet_view.dart | 4 +- .../new/frost_share_commitments_view.dart | 14 +---- .../frost_ms/new/frost_share_shares_view.dart | 13 +---- .../new/share_new_multisig_config_view.dart | 4 +- lib/utilities/assets.dart | 1 + 8 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 assets/images/mascot.png create mode 100644 lib/pages/FrostMascot.dart diff --git a/assets/images/mascot.png b/assets/images/mascot.png new file mode 100644 index 0000000000000000000000000000000000000000..9c05490a43350fd0e0e17b5b456f38b466e94ce3 GIT binary patch literal 222983 zcmeEui9ghP^!Fh9S`kI0?8*{CwxZ2m_G}|U_I=+=xv8iTiXyUv7_#p}$(5v(eH(jO zN4Bv)XJ&MN_xF09|KPb^_ufW#=ChpjectDM=l)fl%ZzkK=nx15qxzK#dI$t3P>7XF}hzjECRfuKJ?_?N6>Fmwz4lGFR5iMPI+owwf&Pg{hVt+Q8v*D-x> zpPLSD#|+%uoEx%7f)R*g2=xo+4gC9N2R;7E@C#{PY*+I=Af@g*Gx*B|Cl<+gp)IiU zdx2-st2vRu+h4;>i#j^et?wr2J>cJYqEW(n-c()4ASc4q)a8j0-;YBN_%lsj%kXvi zH*hUFOBKo6l+C%K2FHEBXNM^k2l=gtRccQ9PE~IPHb@i~w-$R1_u6CShqgks`^(7S z$i$ylDVGq0yAgk`#vvH~ynZD_#_{L%xohMSe_n^D@3Z>z8gY>p^Uv$(1L6Oy%F2(R z`13!6H2$^0)BiTulgoc1`ePvfS+zm)lpPyClM|A^xM-;{ZdR{Og-YP^H*pt?Yu`IGT?BVLKjsrA4WU zaf-?{N|IYcAIIXn5Vv$^U>RwUzgAPiIICaYxV#|T*C{EO80r|&b zTW96$#KVMJjA$*`o$i$Nj60IZI|6F$Td6v$t&tpTenV!^TMkBW2YR~|!C!zaU+NJYIBpALQ9 zYQ}?FOYVm5^`5^P=XE_#w)mkGlUy?;%r27?HhD|p2o&A4ziaVTp0&XFXrtb%4?PpZ zpS`8p-g?ab(v9K6p|ww4`>%*b5-wG!P9zC1luk*L@A$d}11_S<`JU@~zHBjPouX^x zcIx--^oRE$d}T=*n#y-b(u^}t7Q=6p@}bYfpOU@eDc#Z?r}%ihjxUyPohR=OVcl6} z5_6HF|6N)e6^fD-T)&FyPX5%x!ME;-3c4q|_qOAEZ!`QZoygalJie{2eCxQ)oO9kC zr5|N;@4gYXp4B3~=}V47(tW4LDz86#SMfMr9^_1l||^QjK((>x{-b zXP=E9R6nGZQF2f{X6iG?Y_$;;ff;$DOL|M}t)whb)RWzfNFdn47BW*#mdbPhy^CLmM5|;nG&rYl*8&%UME!S2a7kn1v zKOne|aBvPDlB`gC1PfS?_wH9|?4@ad|AYAWkaQaH5UEx%*sog=MwpN>bA zxui-EZWTVqOC*qvsdq@XnZ*SkZ}DNXbgLh3|M200fS{mkz4G?Xu8MMUe0)4#OJAR9 zS6A1&y1JA&L0vl^9~t#`RaI5%QZG7{ei=Q4>uvUzg?zk1*t;x&w70bOcfJ7QkItLT zq4lXPEiG-E@So+t+9`(WOD--hr&?QEubY{*e)^vsCNw%J>3ubY*Xby`NI^l*@2A$;=i-BaS?_O`aRviEMN zTq&0s7eobDy7cqgAA(gdO@gF*Lexqg&*EF?&mHbvSd&sxdI<;8($!78Un}-ubad2? zp{%6jX;V|vAZ}{;4qsUOIKBG3YyT>(Jpl+|noj&#hD*cK3ssJMJN#{C0Q)qirUp+56{n{uRD`o*c_*rG_C@Xu!T8J5p^@irQdMjxQ)N% zQKF1vr=QhYiqq56-&3#_)OpeKupzpD$xenql!osoa-kAK>;)=Eg0uyCQ^%9|sN=Y< zc)rIiHc0Md2aC6;2(E8>FTQ{6(k~Veb@;W{Tpyn-f55O%tb&S4YG}P1hP~zEwAG!4 zV_=J&mOCFEt&j(a-lrHwRMy8C)fEGrk1Z-uAmBP7`14&py_43xj`ywVblVYVu_-%m zqxGd$Nj>yLOVs6nf$o|~8F_!)D>AnT@R~M`w_lB(3}5-(!WY(GHurkyBH`HdL~FF# z6xT_feCOTJ-KoyRU#Se~uZpjG4jZ zPmDp%ZIrhlz&!tyV^}xzY+M@>v+%)BN7X~6T3$C&mn|wPJsp2=c;rmpA5{rZ-D#+B z+rMsAMKh~orb-BBFL;O{lzkz+_(5HPjwc~iS-u?4ey3o(ewJtV+4!KkjtT8Um$UY0 z?cdM&WMh6;EG;elTHB;jymZEhl9PdI4F8*3vs`4e3GlB>GDE zr*7AwzQy&56D?@LfD8Z}5AU2(4nrn^)7L9~8vD}L2ENu8-u(5M8*@Mq3T%9Gi3JwI z4iNA8aFV>(VKvKaZl*bI%lraT{6oIFvg4*y*@$ADVvEA}yRs*w_7uTwBPuEyfFFsH z;$ZlAYTu?K3Np$m(G$e0z9c%tRM@rN{8b+dRHEQ|3!h5zxS@d5frTr-n+Wljt_=6W z6;&hClK4NWa3`}la$D)yc&++QsY}0LttOldVNW!2_YwBEK}RH7%hRT+v(66HGwf0_ zum1aiOFy?1T^W8W)CDEZCoh+4v%0zcxxJm!m6y{@jfv`xM}YbrzWTUvojlUXhyVby zZgDQfbZ*AyS33H{;Ty*quZJ9KnFAL&Oljs{fJ`2j`bFOU^R@*Mr)9AI6sUE7*5(Kxa ztgO`1H4`e#&CJXk2w58(D?BwGhywJ`TIRyD{DrU}G0MyJH(7s|{$;dw_UU+bT+OML z6@lVYLr=zI%3O>e-9B}8S6uh^JYSeJ=<&-JJ3G6N;Sazn@Qjo zRwKHCPW_dVwlTSlV!nECJ&aIKdV7Q>>a*Z_hfr(~78Ajx9NZJ`7cSX*$Wy7sTC+Nj*f%~>-w|v1@g|0l zM^seYDir?acuQ55B8IAMGpdATFs=fGWYN>cemA=jEz7sw$%j52U$nf+m+IEPdib@~ z17%Lj=i^?lceG6Q1by@-gAv6+Vz{p+hI=bnONJBxaI!5cS*N>(QpfedhaFg*zuA9i zMGc_Fm0l~(KRI#vo!hW7#ZW|9fQ-a`LY^wjBle4Yzh8WX%ga@{;_1|1HY`h5_Op02 znvdrSd&Hy9v$M`1KnDM)zR6C^1uMdN3y8S0hmk$35k0N^avVY?!L)u#jiJ2FVncnO zHY>Sf&oS{RJ7W|EAOd+U|NJ(G?>tV3#8$>cBb^<8P-np%x)N9mNnT4w$IjaN_;#oY z{G;9hVv>}6Vu>zaU#WbXpKlB4p8y~NKYDTYzjPTuqtW&O0RcbyUv2xKVrh=_jJC(h z3aneBa;_aM9$v5y4IOg#BZSlepKgo(BK`~-0$tbRL)4YRqP1J%T_h*q@*f6b^;^G} zH@9JXWv3rLk|9gU!(1zeZXvIDYJ&16(aEQ_xB_fZ?fe0qLb26_g@uPL-xcTQ=TH3n z_8Jx*zqD^vQdE%O#?=u7H?|VA7@xb1n_2F0^7K3%v^GCj_I1X5pVjQ+_P9j;b^ogT zW0xoYesZF>+%1)C;dpb*(N+p(#lp^dUob0m<=(QnJ3|D!vJyN>mO1Gr( zxqV@xuRn7e4RN^br_+Ej^MUXpHD%+1E59T7${+r&C@wC3{`|Ql)?)I*{ezWjLqWxL zb#?KCWiO+)0^lz57O*T{Wp!SzZoay1VSA?hLWXkslb?$>zg=|CXr1+W*X(gWxo3Ig z%B}ttG4|Ib4{!9f>7=Vh_$G9GWBt~7TCG>;@s&IDPNI&siA~j&wLObn`}^>HEq9y* zOB+5pdkI=R-Zvece|_Tb3+czE7Hh+}`n9icX?896d52Ebq{K$?=ve>S+KLv4$%Pft(Za(33bh@ELZjL^S3D=vHO z?as=r7h^-d{VvV}72hZeR3YMXsf4~*UtibL38KYI;7n*n#B1mYEAr(WIE;vc7a1ko zKhuA%oKtWY|N3OI`}e2_5-?N_eekv%g;nmWkjZ=u4~37%qwB`T&0YCs76T$ODk|;w z4+>WJFIo#{*Y4XAahHgc7_)dxSXDj#BjOq1MdsWw_KWCDj;%h*8a9)-kKNbw_1`DS zIQehSAa&0qYlr-tX<`V^xxJd4O2$pm)hs_>=F%@Rt%QEpBZ!_NZ^A6^9#ws|#B_yl z*bEJ8gcjjNhiCYM^lk)axK6Z7uU_~3qDCg#XQnIjUFn!LNP%)rA&lPt=kJt*$@ymG zdd9|6Hk@LV&R7 zgy%U?qwOj-{6*|GragXB#=iJ^Qc@DwrZj%#I2Fd=Mvy)eUXjJg+G(aM|3_zMB_{Cx zfhdd^qMnM2fp_a`XClE~)E#U|D*iO$e(0hsnF_^#X3=r=>&0zjc~Xchy8hJA19(O& z-4|n%O98OHxmB&!eKjk4A)^5#1Kw6S%=2azoSp_ zAax~|J1O3ZH_y}@*m87rEo&P)PO!Oo!!RCvoLqC8U9ofTyC%vmnRvNPk}Syb_9qT+<3pgKP6S=z?2Ll?^tVeI~oPdl&Q!a1#j2Fcg*xp3 zn9M#+S7~sND^S!hbWP`lTtlVTl!jzybA)arONw77NjOLLgrmn*m;Un+ zW)LG7k=#0x#1P8gjCdyE&io*}o_S0C#h6|3^)lz)QCR8Fp+j=MvmET=mRfwWX2J7k+utZ$9FQ7lmS>Qjw~-$Awz%xJLv3?fYHAsd5}qa(@6FD9p^ zPT?2F0OkoQbsogH-DA7PH3#kD|$D1p)<=BUEQOwIf3Q-K^mnR!1~?5+4>Z$&;PvuoZ`xMIwm zbIlrrfS6fY8edg))=h_&gRrKP3S}^Bg~}HXS(m;GHa4&6-||0CO0vFrlkbu8ob&!P>DdiSdQ}~Buqv^Abj*hor*~-dZTq7O9wFZZeV|ftB`(6VdKVCZH(&znt zSOqdh(*ySBsj2q9z6qL%Vjhd57eR}bl0SV;z*%KvWCntl+TNO$Mr#SE3B=v#%ryid zn^<^I9#!yt`26_?1?j#&7na$(a5q_=RwJ_4IU!hCS0HXN>n#i6rXln8;wrJ)kgp)$ zKuS5ZQVP}X=g*%dq@>=@&dzqeGM2A6K$Dl3X9JrI1kU8&d#_=2<3>bwYUqaJ-SWs6 z)XMuHF4bUjNfE`&HM-Mw}v1KCOk7HL9Ti3=g$he zTPuxs>0hXXrS$gp>KO!T+;pp3=`uqeV1Nya&)Ph+v$qe}_>~=(GAZ%(psL(l^#9%@ zA*fK~4ldEK{?h~SB3x)UVwu9iBc|HnyTB1n&Is#^*-+GrTlUp?q+v!l}C;6K(?w z#v7V7>%jrXy3E`{)P=Mj_>F#`{qj;Fah%l#Q;*p3!igJ_eD$y-DZI)CzKTva3s<^d&Nc2V{kLl|Q*`3O28At8^=%OI4Ho7U>f5!B%V#;;4j z-YP((4v=41d>NsBXn-cPTdv@hQws35N{r&$Zv}A%3<)Z+e#isab_4X;}aoZk`9iJ=|x3Fqe!R5{A2AD_2gSqSmZkm!fF|8 zH%SL8@N4 z=sqiP@f&A&7_E1_b)A;L`^mOs(SqL^GT?wBeS*W}TSp38JKre!`1nZ6${Jr#7=8Zy zFr6_m~pW+(tQ z(OA30h|$3&hWiv;nb34%advio`0d-bjC=pcc>kV^o1!v=K9QwTm+>nlj-WpQ<}<7c ztE259+p_soeV|9)iVWY{y1R4mT-yY)oZM%qtx402#Tml0U}|uO2(cF8%Sqou45HK- z)||z$WJ>Doh7j$+Mj68lh2T7+AejdNh|m+2 zfg-`|mOIy={%a@SJ=B9*4u;&GCGeox;53ERfeY7`54~c<2VkedQ)3){3S;GPGT52K z-EHzFY5$@T^D%-bidmbQmsfT`v%{xDYiobP?L5PCTVMf!*2!(>@% zG%8y7jbhA9L_KxQK{cc%xLw6uS^uY1FYyKuD}78YZ=_%d9nrk?gh@&?7>r663SB#TlaB496`jws zu#YN2Tm4ICPtV_3+R5h2B9MZr=6B4fx3r^O*o+rD5Iu@GC9I;vWmT7>$dM#hvNKoT zuCA!)zcsh3AL4+d@q0b^dg)!QLb@IfcO=b7-Vh0ZK9wf9T<+RA){E$Dle81xx?-Pe z-6IsF`ubz(#YM$&FE9Kh*$|s5$4U$dbo^^|p*}5IQx(^~lKX(!*n=rgojfT) z!fciPi^o+qzQy8q*6PDn>cx#MERMV|loYL?g}aPtR;9_VeckbH zE$j}`HlwKM3G}zv7&Qb78nkn7mBu|4^E0SQz_UpGAw^d zVIgLA_9S5Yn~?A7w_58T@{b!*-HQl2s>-n>rYT#q#M z7dFvkXAmwNTOv-Py=UZ!#u~%YxI1<|3fkJ3e5V_l)T@(d1i^Xd9EKtlq!wNvcuy;wZr8U z)!|6@iyij-jX%c5#$G4|ow=XJL!o>?m1FJ8|FyP|Hk%R-*EYAbG3{5KS2kUp*PaDtZkbz{m|$B3q0yDuL+_(awsx0g z=ZAs_$&;*ziCxc!D>VHL6W#(FqTcqT<>t?yKi!KGM*sjrsGCPLeUf}Fc6T{$29e`_q`jeZWIi;5=AW znvtI?(iFcMqpzt2_|HL)~4)a0!l-3gV#`3aTbL#S~A#+j$%(~tBtHj?WqE?zjG zrb4w9bHkE&+Nb+S4k@yWJ$TV6LYzDqm1MKMu>y&=y5|AG-hh7r9Qpb4Gp9f{8_XA$ zMkzRV^ya2hgfI3E=8SR1Z<4#GN6gqX$M7%qN>gqH(u{a4q!W!J8h9PTPVpf>@2A(3 z)$rgs3eVn`f7g4`tjyV?h@BvjhmRf|-Y~5RqX<@x#2rmk4Qv?-J=hc-!-E)NeCuPF zdl`zJxxP|q85#c5JD)!@0Pm@3{gXNzl%81gfMnh}dw#Q;CK!DNfjpKrjVueTsT1w2 z6A%z^UaXL|k60sXz7ys5t7&uN6ebW0gjc&DxW&YqqJ z!l6o~yGuU2IkbjfEw9&3yJ~gwW~6G`Rkn6jAVo;!fxv1}{nmuI*Dt+U`TXYa*hrR- zA%djvA-5NtDydT-fV=bHDl;YvSH?=O&pGv0ML;wd_7Tp=+YCI~WE5MkEx9j2q|!VF zYBe)6PPgDO&X%bqaeBX|k*4#7uP?{`9`I^6be1?7*HRd;G}&{f$NtNU8^CwZx4tBw zr{~?uS+FB2lT~m}nQNGX7v0E|byYj-0p%S%dQ@!r!AXc(khg(kUxzDZKJ3siFDWg(hD6qawkrlFc!UU zc)Qf8`}|6Mn|!P zj(h&Styo2iuTC6hw;lXl1Py+;v%S5b0(kNwrhDb#!-u#Z`nr;eik|^sOsB>}#5FZD zv)Oc34#rCWW9xyOw82oT(PU+iT*GAzL@BH03SF;=gvMCOT@kKLCzZ%W!y7#0bnC~5-=8g_)Cz%DG!L}$# zQMm;>J|m5kKvy|0aiHI0n!~}V>%Ha2Ex9|%Rj9X=aao?#b!BB`KMXcZbDuof51aZ= z=Va?~QayOhfAhA>F@*@VhWcmmL>Wu&McLm>XPc&xsnlJ9lA+3}**Q5mBHg}@j?pKp=ywEr{OL1cf0SBIDE9@u#mgj4xllY?%KG z!U@DzEiEnRYhcw5p^Gs*SKsc)98$$U(LBDk_>%xKQ}?-wMLFd{!v%U7-hO_@=B4PS zBi!7RPcCzhKocaVY98=^U#XLuTt&#_=fmKuqjhf~ahc3rBkkPu&(0UVQDLtg$ldAu z_U)QXJ`ZCkzas%tN*;#;xZ9hs+SjDQ_? za+*Pzs&W%Ko}ThwbC@s!m#a^-22?`po?zq#E_@qCfXgwM99_f5TW-&QxkJ`VfJ%iY zt0Re=`+c-k?)-N3$wnmjHB67hnu|?oUrz^1r=q4l%)xP`O-5Em=38H%hPHN_PrdIS zJ5K{9J`zWfNDk33&YVfOV0-XXTL9J! z!ybVqAC_i)CDL1Mthg25I?+?^#sm8gJ+@7H@gk$PR!OE4bEyA-r=8tB$Qw;cAw0Ns zmc|m@7~=g*#r7h~nESzt3zXdCT_2QI!A^kOx44+!y|twX!HBBC2Eo7n5?Y^;+Gcz+b3q!Rzl|wTjU&K$I#CuoTfx%OjUFrB~OfQ@+B*(;`mhzZL2@L?ZRv&xDSa|{v;iVx097!K9drjMY5 zmD}q=HbtsR7c}xp9Ic|5d19)ye;bE0N3rN^mXKOA6Qqh)c$+#|C{O`M2~DV6;EtMs zHy+y|D?|D5)A+K20V^(`geHPkqwm$f-7qv&Qc_}IV8|FX1;lDn?F(eZDe=wXgMvB) zC?%Jza7hy3`#Jjp(LqinlJe^>nf`mL*L@{v%{$jFVrz(9xyITb^|s3@m~wM^|km$HYz`IlGH zer&la4RVq8e~YS$S>{^Fet)i8(Lu8?8}*}{n)B$<(m|UEc`^ospc;*hSd?A~a#y;^ zs5Uj@!w9;p><}|D&&IB^f|1v-98v`^7_m7-y?%oXVXCQ z$ycc$d9Mw4F;g>hYqiNBbEpD=R0WcK|J2uA#-ETgd6W+m<>ydI3f~$F5dzUeh*|2g z4@$nKd0~Kp&GR=hbKO&{Winr5{&L|p%nDi&c}3Af{T2wMm6a7PId@D#tnC{#I+hSf zr<00%od@!t=95T3lr@PO)g#pEZ6?NfdzRsv+ zTc$fqKET94;U>~q1KK%pIp%TeL=CO2qm@;7N=k~OolE(qXQ;5;AbY1*)NG5CJ%F# zk;um>50Y=WVaoK{0AT3ci;A+?Vm*BLa9V9`?GZIAB-xGw%}5xY^?!TC%c$o|lzl##9S8=V{T98HVOwrI_=rNhCVXKW3NEJ^NQWFhLY7b>BAHA9W zh8BPHmFRrY(z+sHC~kX${m>z3D%o0FH%6WBmfE+afYqWIi4u#_D7kDOzJ{p3EcpW^ z_a6Dn*p|0Yg_@f@l>7~~rPq4>@V*}idR7V~LWP_TgCr|X4BJ(fpa^EJ@L%sn-eu8> zB;rAX%snd`J`Yw#H%o1;?U5;2bG`BKM)}1tkC#AOx17N_jx5C4w?@qbsgoeZ{C#E|bO)-YK%hGr;@iM^; zGYDiCywIy+l5ARyO(h>Xy4C}@_2%j4?}7&b?&kC*#DW;SeSJN&2b!?sQ{nfapEoZQ9T4V<*kQxf>3z1x=XpMQ`0(q{kSmSzdY0Jk zSsh7MUpjok4Ps$}5{cc5pl3hu{n(LAZhL3jn(!{D7SQGk#Q+raDsrpZA#)L?L16y` zAHy?UrpU;>&y=54(v9Zxp?T4m+@?t7nP#6Y~ z9RX(ZlWrLD&kY6AR$~dy)@kQXf*8?ti4g4O=Usj02cB;@A|J?rKO>Pmv5LR1$Xj&9 zv-GsEG`^^Q=xT)wJ%vNy`qkS@gVtODCIcl^0yUUtzk=vDRvbT8#bQTGf`-^~*NY}0 zyQeYlHH(^hsKeUpBDK!1_4zHV!9AZ{t?mw?vUPXtUw3{%%J0QP|V zO$==YI}G8$hILGTW&sE;rqMjY4;*!Unn}LU8Q{8NVq)0BLjJL?PbVvG;ASR1ousj& z(UN^9UQdZn#3;nQ5KL_KnEsKIn{()m)53F3S=WrKLKPgDi#}E~aty(uk+|EM#|g{p zt?yxsqfv=MeKcD^Rv|CO64W-E0sqDDsC?Pl3T_&dy@TvFzLd|)7~vosD%ke4sLFKyc#q6BoJKhj*yuDi9LYn&1ISXQyKgHoW+YV*P$Ayg^bZX zGlVn-ePMt|20ykxO?W~)E`q?%GjvfT?$|S4iBoneb+mY7xC)|+Prf@xKifz#V^d>t za#Aj2L;Cyo?@%`2wwBsqPKyk)yu936SC^I+0b}2ePEHQcFoj zKUn>1e!k42pAkwAJHUgYy@xtjo)we}qA7P|aP;b5mT>QxzYh*t96|@Uy2jY21l*Id z|65{(kwu`Z}KbDPEt(w(nyoMNSZ>fZyau=6)Hm(S` z)pAip7uZf17{oi`@(jArNZreqqiz%$OUlc)e0;DNmNrfY9MZJf%BAjNQQ zl8=e(-qB>OW$R&Mo2zrLi;KTU$=E^KC@C*T8^{#W zW2(bd01{!U(^@w@IGeD|?E*5F#S*LgbUgT9D2F6%Z>}XuFZ-PG8}Oippga&T^-?)l z#m?XVS%`ZfurP)b#dI{fwARM_ z$dMz27C>~_DY46b!y&Q|fW3K*z5tPfiHT&%vS1<#!zhLn&L=`{-A^~g+F-UlVv^hJ zdf(Y_B_B}BcR7*Baq~Ec6sRBp$8|TUC#56ADGdU z10n+Oy5>e7)6ugZcD6U4CyVa7W>Ym-=3W&l^cedXpNo2vPJd1Xm&wxTXTEoWd;dB? zJ*6Vmhor7gQ%gW!#n1W}=;1|E+r3~Y6lMJL8q5F=`VF@M*O8O0Oa;SY0(f@X&MI~^ zWgU&PhGUFHwJ%ss9jZ3#d$z8WoSaDlTYW+Eu^uD=y@3qM-8$lZXz|9F*O>CH7ew%u zv%F^v3u>CIF8jCvYB;$JR>Ay46{_Z*23^?PP;GKV1R2j~wldXIynDy0H#Kwc(wT9aU{p6;dxb_qBU z9Fsw(<<=qW5+78SFy9C)1^(+;?AE&BH^2VsWJ zphn(W3&{7szGoli;<_0aST(v}tfoc*KpH0O3?`PU zKo;=x>35%JB^SUF5?%y+Httz@F{JJg9VKi5VL}nW{fo0c(NHrLI3s6l1;xXRg4=u_ z6UD~gPGS#!D-~Y&){a1ufsy!ldqcKIlzgT_CC?HkHD$=BsAJ06N}sa?<3xVj;vz zfNI!=J4D#RL8HL1jIh5QMth2w#M1YSc9?(F-xyMmY^UNpmVdf9b;qT&f0l` z+V4;^Cj<|fi#00@uj>HK2|-9yPu|PZ69ok8Y49wa8_+NbJFLp#WlSX8gyr{A%hU~i zm>miwBa9^iK@aT`{o~!{!XxM?p3r9iUU(W=O+iR7w{@4r&SE>d|IWeu9Uwg9JjZD& z&Ba!WrijKg_4&CJ48tu{j4@SJSLeW3T~fE=KSBNe7kwlWn9!+C5v00eYpX?6U7a!z zk&7R?t@kT45?Xb?=7|jkz0-T5YJac6y3LB4IfsNnva+&{j*bJHD@Gc4r{TJg*}l?N zYKbNdF7jk@<$b~Yk+>7a_35kF3wdFT%*@3w*3#4bG9@LezCQK-eOk9sTNrg*UR`}> z*OrtU&<2CYk7}bQ;xWd`sZ%c(g6(@L>k1|`3=5&oO?an`i>=-l%;Enzm*n{T@Aqs; z@hvb>^{lfk8pfQ&Lt1cAnk4N42rJnJ2Ih91toZOQVTAfSP%8Tnk`YP@znB z+d?F1XlRJ1yKR98rN_teM#>Mz6HUBn55N2-9IG7>OExpcwRUvD|MJ^zc^MhQPrgC- zp>&^uXv4Pn-etQKrY=KyfEftxjz|BsE&FX6dGO%DB4gdtTRtJ3hsezw*$6E%(NjnJ z`}#yYEecBR1&=?uY-GLRM!rUYWP;9>g)q^Sga4%;7qT6C7PHC%=QFJdr_8LYKZ4{v zMnByDfnEVj1OozZRMRk!)-?_1CX}%c%5H0I4<+APLGQ(Vljax6$u>Zp-m5=Ez(frV z=K&z)S?^X`vK0cUGPE@*i*%VYK_Zbd?TzE(d~Nn3q3emKLw0iDs}jIy;rT7IX@@YLpNzbp@i9*c zsX>@~&y|MH6I{s`<^k_JxVXS08n(TJ(L%2+ya)7b^=rQnY8JQ#pxxk34LQjowwqOJ zLxz}U;j&u~1_7!k{6M)$f=L9d_S)OjcL87oP+)lgl>}l63`~NjKYk3)l-JI=5V+OTDxWZVb>x}h2_kVH{bVm;`CYxAo=QGXseE{FY>D7A{eYpcyZWn=<32SPpb*Q zJa2FB!qDx&GtND@+|D8xnIaVF&+Svx`GSo^6ntl`pmn8;Cy%~S34wzqiY>;vC~ z8&xPJ-u%EA^PW?GRPg@EqNb+S$~_gMFmDBD&$*{?-)KEBP#xdG@+c58cYq$h{@pX~5Xr0e^4{b+ma z7-niKEi`v^u%3~D0(fDC#8yd3$^HHQfb_k6PA!Z9w{vH$W=aV#v_P5D*aA#pBU)0*ssZ=*6h3vMmkG z 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μ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

O*Mn*=`wd*$fqIslnFgbCjyKM9##RyZ^ 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 WE7!}zGao8c;sF<8%{yx)AwFrR8Qj47Q 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 + context: context, + builder: (context) => + const RecoveryPhraseExplanationDialog(), + ); + }, + child: Image( + image: AssetImage( + Assets.png.mascot, + ), + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart index 08018b43c..c1797cd6e 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart @@ -4,10 +4,10 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/FrostMascot.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_desktop_specific/desktop_home_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/node_service_provider.dart'; @@ -103,17 +103,7 @@ class _ConfirmNewFrostMSWalletCreationViewState ); }, ), - trailing: ExitToMyStackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), + trailing: const FrostMascot(), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index b408b61ef..d1799bc5e 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/FrostMascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -121,7 +121,7 @@ class _NewFrostMsWalletViewState appBar: const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), + trailing: FrostMascot(), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart index bf3649a37..282f1716c 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/FrostMascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.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_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -121,17 +121,7 @@ class _FrostShareCommitmentsViewState ); }, ), - trailing: ExitToMyStackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), + trailing: const FrostMascot(), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart index 0f5e70ee7..33fdcefcf 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/FrostMascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; @@ -120,17 +121,7 @@ class _FrostShareSharesViewState extends ConsumerState { ); }, ), - trailing: ExitToMyStackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), + trailing: const FrostMascot(), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart index 4afb4c0c5..aa5a3d7bf 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/FrostMascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -47,7 +47,7 @@ class _ShareNewMultisigConfigViewState appBar: const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), + trailing: FrostMascot(), ), body: SizedBox( width: 480, diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index ecd170f6c..ff385a44d 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -257,6 +257,7 @@ class _PNG { String get glasses => "assets/images/glasses.png"; String get glassesHidden => "assets/images/glasses-hidden.png"; + String get mascot => "assets/images/mascot.png"; } class _ANIMATIONS { From d8e5b8e3052790300eee5c2468fd44669c0fe776 Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 15 Mar 2024 13:26:50 +0200 Subject: [PATCH 053/272] Add frost mascot and dialog --- .../frost_step_explanation_dialog.dart | 65 +++++++++++++++++++ ...irm_new_frost_ms_wallet_creation_view.dart | 9 ++- .../new/create_new_frost_ms_wallet_view.dart | 9 ++- .../new/frost_share_commitments_view.dart | 7 +- .../frost_ms/new/frost_share_shares_view.dart | 8 ++- .../new/import_new_frost_ms_wallet_view.dart | 10 ++- .../new/share_new_multisig_config_view.dart | 9 ++- .../restore/restore_frost_ms_wallet_view.dart | 9 ++- .../{FrostMascot.dart => frost_mascot.dart} | 18 +++-- .../resharing/finish_resharing_view.dart | 10 ++- .../new/new_continue_sharing_view.dart | 16 ++--- .../new/new_import_resharer_config_view.dart | 9 ++- .../new/new_start_resharing_view.dart | 16 ++--- 13 files changed, 139 insertions(+), 56 deletions(-) create mode 100644 lib/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart rename lib/pages/{FrostMascot.dart => frost_mascot.dart} (66%) diff --git a/lib/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart b/lib/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart new file mode 100644 index 000000000..102bab1e5 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart @@ -0,0 +1,65 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostStepExplanationDialog extends StatelessWidget { + final String title; + final String body; + const FrostStepExplanationDialog({super.key, required this.title, required this.body}); + + @override + Widget build(BuildContext context) { + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 12, + ), + Text( + body, + style: STextStyles.baseXS(context), + ), + ], + ), + ), + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: SecondaryButton( + label: "Close", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart index c1797cd6e..ffd1127b6 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/FrostMascot.dart'; +import 'package:stackwallet/pages/frost_mascot.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_desktop_specific/desktop_home_view.dart'; @@ -34,7 +34,7 @@ import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; -import '../../../../wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget { const ConfirmNewFrostMSWalletCreationView({ @@ -103,7 +103,10 @@ class _ConfirmNewFrostMSWalletCreationViewState ); }, ), - trailing: const FrostMascot(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index d1799bc5e..9f42a3fa6 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/FrostMascot.dart'; +import 'package:stackwallet/pages/frost_mascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; @@ -118,10 +118,13 @@ class _NewFrostMsWalletViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( + appBar: DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), - trailing: FrostMascot(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart index 282f1716c..1234dbf8a 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/FrostMascot.dart'; +import 'package:stackwallet/pages/frost_mascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; @@ -121,7 +121,10 @@ class _FrostShareCommitmentsViewState ); }, ), - trailing: const FrostMascot(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart index 33fdcefcf..20ac39c03 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart @@ -3,12 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/FrostMascot.dart'; +import 'package:stackwallet/pages/frost_mascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.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_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -121,7 +120,10 @@ class _FrostShareSharesViewState extends ConsumerState { ); }, ), - trailing: const FrostMascot(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart index 1b08b045d..4eeb3a045 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -25,6 +24,8 @@ 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:stackwallet/pages/frost_mascot.dart'; + class ImportNewFrostMsWalletView extends ConsumerStatefulWidget { const ImportNewFrostMsWalletView({ super.key, @@ -73,10 +74,13 @@ class _ImportNewFrostMsWalletViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( + appBar: DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart index aa5a3d7bf..7d463c4ca 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/FrostMascot.dart'; +import 'package:stackwallet/pages/frost_mascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; @@ -44,10 +44,13 @@ class _ShareNewMultisigConfigViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( + appBar: DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), - trailing: FrostMascot(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index c9c174ab0..08f36ebde 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -40,6 +40,8 @@ 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:stackwallet/pages/frost_mascot.dart'; + class RestoreFrostMsWalletView extends ConsumerStatefulWidget { const RestoreFrostMsWalletView({ super.key, @@ -212,10 +214,13 @@ class _RestoreFrostMsWalletViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( + appBar: DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ) ), body: SizedBox( width: 480, diff --git a/lib/pages/FrostMascot.dart b/lib/pages/frost_mascot.dart similarity index 66% rename from lib/pages/FrostMascot.dart rename to lib/pages/frost_mascot.dart index 92058d2b9..3f6c0562d 100644 --- a/lib/pages/FrostMascot.dart +++ b/lib/pages/frost_mascot.dart @@ -9,18 +9,16 @@ */ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; - -import 'add_wallet_views/new_wallet_recovery_phrase_warning_view/recovery_phrase_explanation_dialog.dart'; class FrostMascot extends StatelessWidget { - const FrostMascot({ - Key? key, - this.onPressed, - }) : super(key: key); + final String title; + final String body; + FrostMascot({ + super.key, + this.onPressed, required this.title, required this.body, + }); final VoidCallback? onPressed; @@ -35,7 +33,7 @@ class FrostMascot extends StatelessWidget { await showDialog( context: context, builder: (context) => - const RecoveryPhraseExplanationDialog(), + FrostStepExplanationDialog(title: title, body: body), ); }, child: Image( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart index 8d23e9ed4..5ff5c815d 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart @@ -8,7 +8,6 @@ import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_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/my_stack_view/exit_to_my_stack_button.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'; @@ -34,6 +33,8 @@ 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:stackwallet/pages/frost_mascot.dart'; + class FinishResharingView extends ConsumerStatefulWidget { const FinishResharingView({ super.key, @@ -180,10 +181,13 @@ class _FinishResharingViewState extends ConsumerState { condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( + appBar: DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart index 86ff2ebe0..5e4ed4762 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart @@ -5,7 +5,6 @@ import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.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'; @@ -20,6 +19,8 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/pages/frost_mascot.dart'; + class NewContinueSharingView extends ConsumerStatefulWidget { const NewContinueSharingView({ super.key, @@ -68,16 +69,9 @@ class _NewContinueSharingViewState ); }, ), - trailing: ExitToMyStackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart index f3ef1ec0b..698363923 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart @@ -27,6 +27,8 @@ 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:stackwallet/pages/frost_mascot.dart'; + class NewImportResharerConfigView extends ConsumerStatefulWidget { const NewImportResharerConfigView({ super.key, @@ -89,10 +91,13 @@ class _NewImportResharerConfigViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( + appBar: DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), ), body: SizedBox( width: 480, diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart index fb6107c2c..7173eff3d 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -27,6 +26,8 @@ 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:stackwallet/pages/frost_mascot.dart'; + class NewStartResharingView extends ConsumerStatefulWidget { const NewStartResharingView({ super.key, @@ -146,16 +147,9 @@ class _NewStartResharingViewState extends ConsumerState { ); }, ), - trailing: ExitToMyStackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, + trailing: FrostMascot( + title: 'Lorem ipsum', + body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( From f6d50756e8b4db841fd922474d1a9cea32271950 Mon Sep 17 00:00:00 2001 From: rehrar Date: Fri, 15 Mar 2024 11:55:39 -0600 Subject: [PATCH 054/272] Updated XCode podfile --- macos/Podfile.lock | 8 +++- macos/Runner.xcodeproj/project.pbxproj | 45 ++++++++++++++++++- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index f76e35a57..ba853d0f9 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -62,6 +62,8 @@ PODS: - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - frostdart (0.0.1): + - FlutterMacOS - isar_flutter_libs (1.0.0): - FlutterMacOS - lelantus (0.0.1): @@ -98,6 +100,7 @@ DEPENDENCIES: - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - frostdart (from `Flutter/ephemeral/.symlinks/plugins/frostdart/macos`) - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) - lelantus (from `Flutter/ephemeral/.symlinks/plugins/lelantus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) @@ -140,6 +143,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral + frostdart: + :path: Flutter/ephemeral/.symlinks/plugins/frostdart/macos isar_flutter_libs: :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos lelantus: @@ -171,10 +176,11 @@ SPEC CHECKSUMS: device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_libepiccash: be1560a04150c5cc85bcf08d236ec2b3d1f5d8da - flutter_libsparkmobile: 8ae86b0ccc7e52c9db6b53e258ee2977deb184ab + flutter_libsparkmobile: df2d36af1691379c81249e7be7b68be3c81d388b flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + frostdart: e6bf3119527ccfbcec1b8767da6ede5bb4c4f716 isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a lelantus: 308e42c5a648598936a07a234471dd8cf8e687a0 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index c2c2e62ad..f20cb25e7 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -31,6 +31,9 @@ B98151822A67402A009D013C /* mobileliblelantus.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = B98151802A674022009D013C /* mobileliblelantus.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B98151842A674143009D013C /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B98151832A674143009D013C /* libsqlite3.0.tbd */; }; BFD0376C00E1FFD46376BB9D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9206484E84CB0AD93E3E68CA /* Pods_RunnerTests.framework */; }; + F1FA2C4E2BA4B49F00BDA1BB /* frostdart.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F1FA2C4D2BA4B49F00BDA1BB /* frostdart.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; + F1FA2C502BA4B4CA00BDA1BB /* frostdart.dylib in Resources */ = {isa = PBXBuildFile; fileRef = F1FA2C4F2BA4B4CA00BDA1BB /* frostdart.dylib */; }; + F1FA2C512BA4B51E00BDA1BB /* frostdart.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = F1FA2C4D2BA4B49F00BDA1BB /* frostdart.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F653CA022D33E8B60E11A9F3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -58,6 +61,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + F1FA2C512BA4B51E00BDA1BB /* frostdart.dylib in Bundle Framework */, B98151822A67402A009D013C /* mobileliblelantus.framework in Bundle Framework */, ); name = "Bundle Framework"; @@ -94,6 +98,8 @@ B98151832A674143009D013C /* libsqlite3.0.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.0.tbd; path = usr/lib/libsqlite3.0.tbd; sourceTree = SDKROOT; }; BF5E76865ACB46314AC27D8F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F1FA2C4D2BA4B49F00BDA1BB /* frostdart.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = frostdart.dylib; path = ../crypto_plugins/frostdart/macos/frostdart.dylib; sourceTree = ""; }; + F1FA2C4F2BA4B4CA00BDA1BB /* frostdart.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = frostdart.dylib; path = ../crypto_plugins/frostdart/macos/frostdart.dylib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -111,6 +117,7 @@ files = ( B98151842A674143009D013C /* libsqlite3.0.tbd in Frameworks */, B98151812A674022009D013C /* mobileliblelantus.framework in Frameworks */, + F1FA2C4E2BA4B49F00BDA1BB /* frostdart.dylib in Frameworks */, F653CA022D33E8B60E11A9F3 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -140,6 +147,7 @@ 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( + F1FA2C4F2BA4B4CA00BDA1BB /* frostdart.dylib */, 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, @@ -196,6 +204,7 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + F1FA2C4D2BA4B49F00BDA1BB /* frostdart.dylib */, B98151832A674143009D013C /* libsqlite3.0.tbd */, B98151802A674022009D013C /* mobileliblelantus.framework */, E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */, @@ -268,7 +277,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -325,6 +334,7 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + F1FA2C502BA4B4CA00BDA1BB /* frostdart.dylib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -610,6 +620,17 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_monero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_wownero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos/libs\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos\"", + /usr/lib/swift, + "$(PATH)/crypto_plugins/frostdart/macos\n", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -763,6 +784,17 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_monero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_wownero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos/libs\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos\"", + /usr/lib/swift, + "$(PATH)/crypto_plugins/frostdart/macos\n", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -807,6 +839,17 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_monero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_wownero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos/libs\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos\"", + /usr/lib/swift, + "$(PATH)/crypto_plugins/frostdart/macos\n", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a9d38bc3b..5b6f6cbd1 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Fri, 15 Mar 2024 20:02:14 +0200 Subject: [PATCH 055/272] Change coin ordering --- lib/utilities/enums/coin_enum.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 0356c1e7c..abb6985ea 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -13,10 +13,10 @@ import 'package:stackwallet/utilities/constants.dart'; enum Coin { bitcoin, - bitcoinFrost, monero, banano, bitcoincash, + bitcoinFrost, dogecoin, eCash, epicCash, @@ -36,8 +36,8 @@ enum Coin { /// bitcoinTestNet, - bitcoinFrostTestNet, bitcoincashTestnet, + bitcoinFrostTestNet, dogecoinTestNet, firoTestNet, litecoinTestNet, From 1a5e31d046986a508130bb6af1463a26b15ffc81 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 15 Mar 2024 18:57:30 -0500 Subject: [PATCH 056/272] add missing send view args option --- lib/route_generator.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 4eb906b8a..e4d188cdf 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1861,7 +1861,19 @@ class RouteGenerator { name: settings.name, ), ); + } else if (args is ({Coin coin, String walletId})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SendView( + walletId: args.walletId, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); } + return _routeError("${settings.name} invalid args: ${args.toString()}"); case TokenSendView.routeName: From c1b9ba085fef6e7a8d343cdc11aee6d55b7b157f Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 18 Mar 2024 17:49:11 +0200 Subject: [PATCH 057/272] In the tx list only process txs that are not anon --- lib/wallets/wallet/impl/particl_wallet.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 89faa8950..36ab9c83b 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -191,10 +191,9 @@ class ParticlWallet extends Bip39HDWallet final List addresses = []; String valueStringSats = "0"; OutpointV2? outpoint; - final coinbase = map["coinbase"] as String?; - - if (coinbase == null) { + final txType = map['type'] as String?; + if (coinbase == null && txType == null) { // Not a coinbase (ie a typical input). final txid = map["txid"] as String; final vout = map["vout"] as int; From 11136d3d140d6f74c64d892dcca16a5cdd1e70bf Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 18 Mar 2024 17:50:04 +0200 Subject: [PATCH 058/272] Revert "In the tx list only process txs that are not anon" This reverts commit c1b9ba085fef6e7a8d343cdc11aee6d55b7b157f. --- lib/wallets/wallet/impl/particl_wallet.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 36ab9c83b..89faa8950 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -191,9 +191,10 @@ class ParticlWallet extends Bip39HDWallet final List addresses = []; String valueStringSats = "0"; OutpointV2? outpoint; + final coinbase = map["coinbase"] as String?; - final txType = map['type'] as String?; - if (coinbase == null && txType == null) { + + if (coinbase == null) { // Not a coinbase (ie a typical input). final txid = map["txid"] as String; final vout = map["vout"] as int; From ad4974e0725ad960a81bed330722d12ffa713682 Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 18 Mar 2024 17:52:40 +0200 Subject: [PATCH 059/272] Ignore anon type txs when syncing wallet --- lib/wallets/wallet/impl/particl_wallet.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 89faa8950..e0d5bc9c2 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -193,8 +193,8 @@ class ParticlWallet extends Bip39HDWallet OutpointV2? outpoint; final coinbase = map["coinbase"] as String?; - - if (coinbase == null) { + final txType = map['type'] as String?; + if (coinbase == null && txType == null) { // Not a coinbase (ie a typical input). final txid = map["txid"] as String; final vout = map["vout"] as int; From 85cdb0cc096cb9c9437bc27d06227ccdd5cbafeb Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 20 Mar 2024 15:33:25 -0500 Subject: [PATCH 060/272] Default to using `double.parse` in `electrum_adapter` `catch` with the(ir custom) original `_parseDouble` --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7b9adb7c9..56a538c7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -528,8 +528,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2897c6448e131241d4d91fe23fdab83305134225" - resolved-ref: "2897c6448e131241d4d91fe23fdab83305134225" + ref: "9e9441fc1e9ace8907256fff05fe2c607b0933b6" + resolved-ref: "9e9441fc1e9ace8907256fff05fe2c607b0933b6" url: "https://github.com/cypherstack/electrum_adapter.git" source: git version: "3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index fd19a216e..940682650 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -179,7 +179,7 @@ dependencies: electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git - ref: 2897c6448e131241d4d91fe23fdab83305134225 + ref: 9e9441fc1e9ace8907256fff05fe2c607b0933b6 stream_channel: ^2.1.0 dev_dependencies: From 5e7c9ad65bd4d6b544e8a6ec5e7ba58282ba353c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 20 Mar 2024 16:04:28 -0500 Subject: [PATCH 061/272] add FROST enabled pref a bool toggle from hidden settings menu --- .../global_settings_view/hidden_settings.dart | 32 ++++++++++++++++++- lib/utilities/prefs.dart | 22 +++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 6cc47a0d4..ec96afde5 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -215,7 +216,6 @@ class HiddenSettings extends StatelessWidget { ), ); }), - const SizedBox( height: 12, ), @@ -252,6 +252,36 @@ class HiddenSettings extends StatelessWidget { } }, ), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + ref + .read(prefsChangeNotifierProvider) + .frostEnabled = + !(ref + .read(prefsChangeNotifierProvider) + .frostEnabled); + if (kDebugMode) { + print( + "FROST enabled: ${ref.read(prefsChangeNotifierProvider).frostEnabled}"); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Toggle FROST multisig", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }, + ), + const SizedBox( + height: 12, + ), Consumer( builder: (_, ref, __) { return GestureDetector( diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 07726bdf1..8fbbbf069 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -68,6 +68,7 @@ class Prefs extends ChangeNotifier { await _setMaxDecimals(); _useTor = await _getUseTor(); _fusionServerInfo = await _getFusionServerInfo(); + _frostEnabled = await _getFrostEnabled(); _initialized = true; } @@ -1008,4 +1009,25 @@ class Prefs extends ChangeNotifier { return actualMap; } + + // FROST multisig + + bool _frostEnabled = false; + + bool get frostEnabled => _frostEnabled; + + set frostEnabled(bool frostEnabled) { + if (_frostEnabled != frostEnabled) { + DB.instance.put( + boxName: DB.boxNamePrefs, key: "frostEnabled", value: frostEnabled); + _frostEnabled = frostEnabled; + notifyListeners(); + } + } + + Future _getFrostEnabled() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, key: "frostEnabled") as bool? ?? + false; + } } From 0f4fb8378fbbbeeabb5ae963528792acbd31300f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 20 Mar 2024 16:08:33 -0500 Subject: [PATCH 062/272] use pref to cull frost things if not enabled --- .../add_wallet_view/add_wallet_view.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 32e1b618c..3d6c4b8df 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -134,9 +134,18 @@ class _AddWalletViewState extends ConsumerState { _coins.remove(Coin.wownero); } + // Remove FROST from the list of coins based on our frostEnabled preference. + if (!ref.read(prefsChangeNotifierProvider).frostEnabled) { + _coins.remove(Coin.bitcoinFrost); + } + coinEntities.addAll(_coins.map((e) => CoinEntity(e))); if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) { + if (!ref.read(prefsChangeNotifierProvider).frostEnabled) { + _coinsTestnet.remove(Coin.bitcoinFrostTestNet); + } + coinEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e))); } From 96c1976d7874aea35e48a06110b9b412afced255 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 20 Mar 2024 16:16:44 -0500 Subject: [PATCH 063/272] use same back button widget on mobile as on desktop --- .../global_settings_view/hidden_settings.dart | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index ec96afde5..b8b9a9982 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -23,7 +23,6 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/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'; @@ -40,27 +39,25 @@ class HiddenSettings extends StatelessWidget { child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( - leading: Util.isDesktop - ? Padding( - padding: const EdgeInsets.all(8.0), - child: AppBarIconButton( - size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: 18, - height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: Navigator.of(context).pop, - ), - ) - : Container(), + leading: Padding( + padding: const EdgeInsets.all(8.0), + child: AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + ), title: Text( "Dev options", style: STextStyles.navBarTitle(context), From d9163a2bbd72d1638386737a713b586d49e3f050 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 20 Mar 2024 16:48:55 -0500 Subject: [PATCH 064/272] hide tor stacy buttons --- .../global_settings_view/hidden_settings.dart | 87 +++++++++---------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index b8b9a9982..915d48552 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -14,8 +14,6 @@ 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: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'; @@ -25,7 +23,6 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.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/rounded_white_container.dart'; class HiddenSettings extends StatelessWidget { @@ -174,48 +171,48 @@ class HiddenSettings extends StatelessWidget { const SizedBox( height: 12, ), - Consumer(builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - await showOneTimeTorHasBeenAddedDialogIfRequired( - context, - ); - }, - child: RoundedWhiteContainer( - child: Text( - "Test tor stacy popup", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }), - const SizedBox( - height: 12, - ), - Consumer(builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - final box = await Hive.openBox( - DB.boxNameOneTimeDialogsShown); - await box.clear(); - }, - child: RoundedWhiteContainer( - child: Text( - "Reset tor stacy popup", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }), - const SizedBox( - height: 12, - ), + // Consumer(builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // await showOneTimeTorHasBeenAddedDialogIfRequired( + // context, + // ); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Test tor stacy popup", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .accentColorDark), + // ), + // ), + // ); + // }), + // const SizedBox( + // height: 12, + // ), + // Consumer(builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // final box = await Hive.openBox( + // DB.boxNameOneTimeDialogsShown); + // await box.clear(); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Reset tor stacy popup", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .accentColorDark), + // ), + // ), + // ); + // }), + // const SizedBox( + // height: 12, + // ), Consumer( builder: (_, ref, __) { if (ref.watch(prefsChangeNotifierProvider From e597045db49626f96f233cb7eff27ce8344e803a Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Mon, 25 Mar 2024 17:33:53 -0600 Subject: [PATCH 065/272] Update version (v1.10.4, build 216) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 940682650..0ddcae808 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.10.3+215 +version: 1.10.4+216 environment: sdk: ">=3.0.2 <4.0.0" From 22504c42a9acb58dd4327e8bec1b6759c68aea62 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 28 Mar 2024 15:54:46 -0500 Subject: [PATCH 066/272] do not change rust version in parallel --- scripts/android/build_all.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index f42a6c5d3..28e56acd4 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -12,7 +12,7 @@ mkdir -p build (cd ../../crypto_plugins/flutter_liblelantus/scripts/android && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/android && ./install_ndk.sh && ./build_opensll.sh && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/android/ && ./build_all.sh ) & +(cd ../../crypto_plugins/flutter_libmonero/scripts/android/ && ./build_all.sh ) && set_rust_to_1720 & (cd ../../crypto_plugins/frostdart/scripts/android && ./build_all.sh ) & From 98c8b4dc6ace8141e670c5ce54b706de41d412e1 Mon Sep 17 00:00:00 2001 From: Kronk <49569383+Kronkmeister@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:29:22 +0200 Subject: [PATCH 067/272] Update block_explorers.dart (change eCash default explorer) Changed the eCash block explorer from bitcoinabc.org to the default explorer.e.cash. --- lib/utilities/block_explorers.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 9f3e92c5d..941f7c599 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -31,7 +31,7 @@ Uri getDefaultBlockExplorerUrlFor({ case Coin.dogecoin: return Uri.parse("https://chain.so/tx/DOGE/$txid"); case Coin.eCash: - return Uri.parse("https://explorer.bitcoinabc.org/tx/$txid"); + return Uri.parse("https://explorer.e.cash/tx/$txid"); case Coin.dogecoinTestNet: return Uri.parse("https://chain.so/tx/DOGETEST/$txid"); case Coin.epicCash: From 9eb6aef23621d37797f68e02b2bc85c055ed70d2 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Tue, 2 Apr 2024 17:47:54 -0600 Subject: [PATCH 068/272] Update version (v1.10.4, build 217) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0ddcae808..0daa6d065 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.10.4+216 +version: 1.10.4+217 environment: sdk: ">=3.0.2 <4.0.0" From 35019bdab3841969ccd05090004334c4498c7fe8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 3 Apr 2024 12:07:16 -0500 Subject: [PATCH 069/272] finish mac host section --- docs/building.md | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/building.md b/docs/building.md index c6473127e..2caed60d3 100644 --- a/docs/building.md +++ b/docs/building.md @@ -185,6 +185,7 @@ After installing Homebrew, install the following packages: ``` brew install cocoapods git cmake autoconf fontconfig libpng lz4 pkg-config automake freetype libssh2 lzo procs berkeley-db gdbm libtool m4 rtmpdump brotli gettext libunistring make rustup-init ca-certificates git-gui libx11 openldap tcl-tk cairo glib libxau openssl@1.1 unbound cbindgen gmp libxcb openssl@3 unzip cmake libevent libxdmcp pcre2 xorgproto coreutils libidn2 libxext perl xz curl libnghttp2 libxrender pixman zstd ``` + Download and install [Rust](https://www.rust-lang.org/tools/install). [Rustup](https://rustup.rs/) is recommended for Rust setup. Use `rustc` to confirm successful installation. Install toolchains 1.67.1 and 1.72.0 and `cbindgen` and `cargo-lipo` too. You will also have to add the platform target(s) `aarch64-apple-ios` and/or `aarch64-apple-darwin`. You can use the command(s): ``` @@ -201,17 +202,42 @@ Download and install [Flutter](https://docs.flutter.dev/get-started/install). V Download [Android Studio](https://developer.android.com/studio). VS Code may work as an alternative, but this is not recommended. -### Building libraries -TODO +### 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. -### Install Flutter on Mac host -Install Flutter 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 +``` + +### 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. -### Building libraries in WSL2 +### 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: From 4cefed89d28f4049cbfae522a926ea098cb0b54d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 3 Apr 2024 12:17:17 -0500 Subject: [PATCH 070/272] mention prebuild.sh for apple, too --- docs/building.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index 2caed60d3..b3effad4c 100644 --- a/docs/building.md +++ b/docs/building.md @@ -106,7 +106,6 @@ To build coinlib on Linux, you will need `docker` (see [installation instruction For Windows targets, you can use a `secp256k1.dll` produced by any of the three bottom options if the first attempt doesn't succeed! - ### 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 @@ -218,6 +217,17 @@ 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: From 115ae7cb73d2da604830ea3150a5ea3df867b7cb Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 3 Apr 2024 12:25:01 -0500 Subject: [PATCH 071/272] macos/ios nit --- docs/building.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index b3effad4c..c4301bfae 100644 --- a/docs/building.md +++ b/docs/building.md @@ -4,7 +4,7 @@ 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 From 32c253674d699ca45a370b5fec876b51900af437 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 4 Apr 2024 13:02:43 -0500 Subject: [PATCH 072/272] remove cargo-ndk mention for windows over-agressive copying and pasting with insufficient review --- docs/building.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/building.md b/docs/building.md index c4301bfae..b6dbec820 100644 --- a/docs/building.md +++ b/docs/building.md @@ -285,15 +285,16 @@ You may still have to add `C:\development\flutter\bin` to PATH before proceeding 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.73.0 # For cargo-ndk. rustup install 1.67.1 # For flutter_libepiccash. rustup default 1.67.1 ``` - -Also install `cargo-ndk`: + ### 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`). From 9826dbb0b19a6a38dc74589a1ededf601b7db15a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 4 Apr 2024 13:20:28 -0500 Subject: [PATCH 073/272] clarification of deps script role in flutter\bin's path for PATH --- docs/building.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/building.md b/docs/building.md index b6dbec820..f6e529b37 100644 --- a/docs/building.md +++ b/docs/building.md @@ -277,9 +277,9 @@ Copy the resulting `dll`s to their respective positions on the Windows host: ### Flutter Install Flutter 3.16.0 on the Windows host (not in WSL2) by following [Flutter's Windows install guide](https://docs.flutter.dev/get-started/install/windows), by running `scripts/windows/deps.ps1`, or by - `git clone https://github.com/flutter/flutter` somewhere it can live (`C:`, **avoid** anywhere in `C:/Users/`), - - `git checkout 3.16.0` (after navigating into the `flutter` folder), - - and adding `flutter/bin` to your PATH environmen variable (search "environment variables" in the Start menu) -You may still have to add `C:\development\flutter\bin` to PATH before proceeding, even if you ran `deps.ps1` (you may need to open a new terminal). Run `flutter doctor` in PowerShell to confirm its installation. + - `git checkout 3.16.9` (after navigating into the `flutter` folder), + - adding `flutter\bin`'s full absolute path to your PATH environment variable (search "environment variables" in the Start menu. If you ran `deps.ps1`, use `C:\development\flutter\bin`. You may also need to open a new terminal), + - and running `flutter doctor` in PowerShell to confirm its installation. You may need to resolve any issues which `flutter doctor` might raise. ### 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: From 70afa6fbb3807321c8ee08722e50933021fa7628 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 4 Apr 2024 14:08:28 -0500 Subject: [PATCH 074/272] make kvm performance note --- docs/building.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index f6e529b37..46e57b1cf 100644 --- a/docs/building.md +++ b/docs/building.md @@ -159,7 +159,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: From a13b1b6da3128bc6792b4040bf9d6ce1e4d52aa0 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 4 Apr 2024 17:42:40 -0500 Subject: [PATCH 075/272] flutter pub add flutter_local_notifications:^17.0.0 --- pubspec.lock | 94 ++++++++++++++++++++++++++++++---------------------- pubspec.yaml | 2 +- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 56a538c7d..f4f331580 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -324,10 +324,10 @@ packages: dependency: transitive description: name: coverage - sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.7.2" cross_file: dependency: transitive description: @@ -593,10 +593,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -691,26 +691,26 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "57d0012730780fe137260dd180e072c18a73fbeeb924cdc029c18aaa0f338d64" + sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1 url: "https://pub.dev" source: hosted - version: "9.9.1" + version: "17.0.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: b472bfc173791b59ede323661eae20f7fff0b6908fea33dd720a6ef5d576bae8 + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "21bceee103a66a53b30ea9daf677f990e5b9e89b62f222e60dd241cd08d63d3a" + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "7.0.0+1" flutter_mobx: dependency: transitive description: @@ -1043,6 +1043,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lelantus: dependency: "direct main" description: @@ -1086,18 +1110,18 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" memoize: dependency: transitive description: @@ -1110,10 +1134,10 @@ packages: dependency: "direct main" description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -1214,10 +1238,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1334,10 +1358,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -1374,10 +1398,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" protobuf: dependency: transitive description: @@ -1702,10 +1726,10 @@ packages: dependency: transitive description: name: timezone - sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e" + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.2" timing: dependency: transitive description: @@ -1879,10 +1903,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" wakelock: dependency: "direct main" description: @@ -1948,14 +1972,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.dev" - source: hosted - version: "0.3.0" web3dart: dependency: "direct main" description: @@ -1976,10 +1992,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: @@ -2025,10 +2041,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" xml: dependency: transitive description: @@ -2062,5 +2078,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0daa6d065..26edda588 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,7 +79,7 @@ dependencies: http: ^0.13.0 local_auth: ^1.1.10 permission_handler: ^11.0.0 - flutter_local_notifications: ^9.4.0 + flutter_local_notifications: ^17.0.0 rxdart: ^0.27.3 zxcvbn: ^1.0.0 dart_numerics: ^0.0.6 From f26e6e8b0166ba8949575151ea85201d6cc99c0f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 4 Apr 2024 18:18:24 -0500 Subject: [PATCH 076/272] flutter_local_notifications updates Ios->Darwin, etc. --- lib/services/notifications_api.dart | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/services/notifications_api.dart b/lib/services/notifications_api.dart index 3ef97462a..fa2f5f545 100644 --- a/lib/services/notifications_api.dart +++ b/lib/services/notifications_api.dart @@ -1,4 +1,4 @@ -/* +/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack @@ -25,17 +25,17 @@ class NotificationApi { // importance: Importance.max, priority: Priority.high, ticker: 'ticker'), - iOS: IOSNotificationDetails(), - macOS: MacOSNotificationDetails(), + iOS: DarwinNotificationDetails(), + macOS: DarwinNotificationDetails(), ); } static Future init({bool initScheduled = false}) async { const android = AndroidInitializationSettings('app_icon_alpha'); - const iOS = IOSInitializationSettings(); + const iOS = DarwinInitializationSettings(); const linux = LinuxInitializationSettings( defaultActionName: "temporary_stack_wallet"); - const macOS = MacOSInitializationSettings(); + const macOS = DarwinInitializationSettings(); const settings = InitializationSettings( android: android, iOS: iOS, @@ -44,8 +44,11 @@ class NotificationApi { ); await _notifications.initialize( settings, - onSelectNotification: (payload) async { - onNotifications.add(payload); + onDidReceiveNotificationResponse: (payload) async { + onNotifications.add(payload.payload); + }, + onDidReceiveBackgroundNotificationResponse: (payload) async { + onNotifications.add(payload.payload); }, ); } From d574bb5b45bcf1fc52c765c7be6518e74956d37b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 7 Apr 2024 20:11:45 -0500 Subject: [PATCH 077/272] Remove duplicate instruction and clatify that Android Studio is not required on macOS --- docs/building.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/building.md b/docs/building.md index 46e57b1cf..af128caf4 100644 --- a/docs/building.md +++ b/docs/building.md @@ -197,9 +197,7 @@ cargo install cbindgen cargo-lipo rustup target add aarch64-apple-ios aarch64-apple-darwin ``` -Download and install [Flutter](https://docs.flutter.dev/get-started/install). Versions 3.16.8 and 3.10.6 should both work. Use `flutter doctor` to confirm successful installation. - -Download [Android Studio](https://developer.android.com/studio). VS Code may work as an alternative, but this is not recommended. +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. From 8083a809e3aa20a453c7d29dbd3af66eeaf22adb Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 7 Apr 2024 20:55:28 -0500 Subject: [PATCH 078/272] autogenerated macos changes by flutter 3.19.0-0.1.pre upgrade --- ios/Podfile.lock | 44 +++++++++++++------ ios/Runner.xcodeproj/project.pbxproj | 6 +++ macos/Podfile.lock | 2 +- macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b2235ac27..3bcf54939 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,6 +3,9 @@ PODS: - Flutter - MTBBarcodeScanner - SwiftProtobuf + - coinlib_flutter (0.3.2): + - Flutter + - FlutterMacOS - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -106,12 +109,16 @@ 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): + - Flutter - integration_test (0.0.1): - Flutter - isar_flutter_libs (1.0.0): @@ -126,7 +133,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 +141,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 +154,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 +165,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 +178,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 +196,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 +218,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 +244,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 +255,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 + coinlib_flutter: 6abec900d67762a6e7ccfd567a3cd3ae00bbee35 connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a cw_monero: 9816991daff0e3ad0a8be140e31933b5526babd4 cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 @@ -249,30 +265,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: ed3dc4e5dce431a1a8791dd7ddba472a05ea626d 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.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fa0e9728d..3043c5ba8 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -305,6 +305,7 @@ "${BUILT_PRODUCTS_DIR}/SwiftProtobuf/SwiftProtobuf.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/barcode_scan2/barcode_scan2.framework", + "${BUILT_PRODUCTS_DIR}/coinlib_flutter/secp256k1.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/cw_monero/cw_monero.framework", "${BUILT_PRODUCTS_DIR}/cw_shared_external/cw_shared_external.framework", @@ -313,9 +314,11 @@ "${BUILT_PRODUCTS_DIR}/devicelocale/devicelocale.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_libmonero/flutter_libmonero.framework", + "${PODS_ROOT}/../.symlinks/plugins/flutter_libsparkmobile/ios/flutter_libsparkmobile.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_native_splash/flutter_native_splash.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", + "${BUILT_PRODUCTS_DIR}/frostdart/frostdart.framework", "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", "${BUILT_PRODUCTS_DIR}/isar_flutter_libs/isar_flutter_libs.framework", "${BUILT_PRODUCTS_DIR}/lelantus/lelantus.framework", @@ -338,6 +341,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftProtobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/barcode_scan2.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/secp256k1.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_monero.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_shared_external.framework", @@ -346,9 +350,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/devicelocale.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_libmonero.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_libsparkmobile.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_splash.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/frostdart.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/isar_flutter_libs.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/lelantus.framework", diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ba853d0f9..35c7171cb 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -195,4 +195,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index f20cb25e7..eccafe6ba 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -277,7 +277,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5b6f6cbd1..73f65e0e3 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Mon, 8 Apr 2024 00:47:36 -0500 Subject: [PATCH 079/272] remove windows flutter installation helper just git clone github:flutter/flutter and checkout your tag. --- scripts/windows/deps.ps1 | 75 ---------------------------------------- 1 file changed, 75 deletions(-) delete mode 100644 scripts/windows/deps.ps1 diff --git a/scripts/windows/deps.ps1 b/scripts/windows/deps.ps1 deleted file mode 100644 index 2d90ed7fe..000000000 --- a/scripts/windows/deps.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -# Create C:\development -New-Item -Path 'C:\development' -ItemType Directory -ErrorAction Ignore - -# $wc = [System.Net.WebClient]::new() -# $publishedHash = '8E28E54D601F0751922DE24632C1E716B4684876255CF82304A9B19E89A9CCAC' -# $FileHash = Get-FileHash -InputStream ($wc.OpenRead("C:\development\flutter_windows_3.7.12-stable.zip")) - -# if (-Not [System.IO.File]::Exists("C:\development\flutter_windows_3.7.12-stable.zip") or -Not ($FileHash.Hash -eq $publishedHash)) { -# } else { -# Download flutter_windows_3.7.12-stable.zip -# Write-Output "Downloading flutter_windows_3.7.12-stable.zip" -# $ProgressPreference = 'SilentlyContinue' # Speed up download process, see https://stackoverflow.com/questions/28682642/powershell-why-is-using-invoke-webrequest-much-slower-than-a-browser-download -# Invoke-WebRequest "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.7.12-stable.zip" -OutFile "C:\development\flutter_windows_3.7.12-stable.zip" -# } - -# Extract Flutter SDK -Write-Output "Extracting flutter_windows_3.7.12-stable.zip" -$progressPreference = 'SilentlyContinue' # Speed up extraction process, see https://github.com/PowerShell/Microsoft.PowerShell.Archive/issues/32#issuecomment-642582179 -# Add-MpPreference -ExclusionPath C:\development -# Expand-Archive "C:\development\flutter_windows_3.7.12-stable.zip" -DestinationPath "C:\development" -Add-Type -Assembly "System.IO.Compression.Filesystem" -[System.IO.Compression.ZipFile]::ExtractToDirectory("C:\development\flutter_windows_3.7.12-stable.zip", "C:\development") - -# See https://stackoverflow.com/a/69239861 -function Add-Path { - - param( - [Parameter(Mandatory, Position=0)] - [string] $LiteralPath, - [ValidateSet('User', 'CurrentUser', 'Machine', 'LocalMachine')] - [string] $Scope - ) - - Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop' - - $isMachineLevel = $Scope -in 'Machine', 'LocalMachine' - if ($isMachineLevel -and -not $($ErrorActionPreference = 'Continue'; net session 2>$null)) { throw "You must run AS ADMIN to update the machine-level Path environment variable." } - - $regPath = 'registry::' + ('HKEY_CURRENT_USER\Environment', 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment')[$isMachineLevel] - - # Note the use of the .GetValue() method to ensure that the *unexpanded* value is returned. - $currDirs = (Get-Item -LiteralPath $regPath).GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split ';' -ne '' - - if ($LiteralPath -in $currDirs) { - Write-Verbose "Already present in the persistent $(('user', 'machine')[$isMachineLevel])-level Path: $LiteralPath" - return - } - - $newValue = ($currDirs + $LiteralPath) -join ';' - - # Update the registry. - Set-ItemProperty -Type ExpandString -LiteralPath $regPath Path $newValue - - # Broadcast WM_SETTINGCHANGE to get the Windows shell to reload the - # updated environment, via a dummy [Environment]::SetEnvironmentVariable() operation. - $dummyName = [guid]::NewGuid().ToString() - [Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User') - [Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User') - - # Finally, also update the current session's `$env:Path` definition. - # Note: For simplicity, we always append to the in-process *composite* value, - # even though for a -Scope Machine update this isn't strictly the same. - $env:Path = ($env:Path -replace ';$') + ';' + $LiteralPath - - Write-Verbose "`"$LiteralPath`" successfully appended to the persistent $(('user', 'machine')[$isMachineLevel])-level Path and also the current-process value." - -} - -# Add Flutter SDK to PATH if it's not there already -if ($Env:Path -split ";" -contains 'C:\development\flutter\bin') { - Write-Output "Flutter SDK in PATH, done" -} else { - Write-Output "Attempting to add Flutter SDK to PATH" - Add-Path("C:\development\flutter\bin") -} From 5e0911dcf7f21c371b669b9d2d009cf86dce4eab Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 8 Apr 2024 00:48:16 -0500 Subject: [PATCH 080/272] explicitly mention Flutter installation for linux host and update win dows' noted flutter version --- docs/building.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/building.md b/docs/building.md index e7128df2e..2417de523 100644 --- a/docs/building.md +++ b/docs/building.md @@ -11,6 +11,9 @@ Here you will find instructions on how to install the necessary tools for buildi ## Linux host The following instructions are for building and running on a Linux host. Alternatively, see the [Windows](#Windows host) section. +### Flutter +Install Flutter 3.19 beta (3.19.0-0.1.pre) by following these instructions: 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.0-0.1.pre` tag, and add its `flutter/bin` folder to your PATH. 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: ``` @@ -159,7 +162,7 @@ Copy the resulting `dll`s to their respective positions on the Windows host: ### Install Flutter on Windows host -Install Flutter 3.10.3 on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows or by running `scripts/windows/deps.ps1`. You may still have to add `C:\development\flutter\bin` to PATH before proceeding, even if you ran `deps.ps1`. Run `flutter doctor` in PowerShell to confirm its installation. +Install Flutter 3.19 beta (3.19.0-0.1.pre) on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk. You can also clone https://github.com/flutter/flutter, check out the `3.19.0-0.1.pre` tag, and add its `flutter/bin` folder to your PATH. Run `flutter doctor` in PowerShell to confirm its installation. ### Dependencies 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`). From 90d1015b37b743b80522acdd66eae402cb4fcff6 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 8 Apr 2024 03:51:06 -0500 Subject: [PATCH 081/272] autogenerated windows changes from flutter 3.19.0-0.1.pre upgrade --- windows/flutter/CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d2071a..903f4899d 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS From 34cf1f971583fb1e86b492843274f3c3cd150657 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 10 Apr 2024 23:53:20 -0500 Subject: [PATCH 082/272] Document required brew formulae: boost, libsodium, zmq/zeromq --- docs/building.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/building.md b/docs/building.md index af128caf4..52d663841 100644 --- a/docs/building.md +++ b/docs/building.md @@ -182,9 +182,14 @@ Download and install [Homebrew](https://brew.sh/). The following command can in After installing Homebrew, install the following packages: ``` -brew install cocoapods git cmake autoconf fontconfig libpng lz4 pkg-config automake freetype libssh2 lzo procs berkeley-db gdbm libtool m4 rtmpdump brotli gettext libunistring make rustup-init ca-certificates git-gui libx11 openldap tcl-tk cairo glib libxau openssl@1.1 unbound cbindgen gmp libxcb openssl@3 unzip cmake libevent libxdmcp pcre2 xorgproto coreutils libidn2 libxext perl xz curl libnghttp2 libxrender pixman zstd +brew install autoconf automake boost berkeley-db ca-certificates cbindgen cmake cmake cocoapods curl git libssh2 make openssl@1.1 openssl@3 perl pkg-config rustup-init sodium unbound unzip xz zmq ``` - + +The following brew formula *may* be needed: +``` +brew install brotli cairo coreutils gdbm gettext glib gmp libevent libidn2 libnghttp2 libtool libunistring libx11 libxau libxcb libxdmcp libxext libxrender lzo m4 openldap pcre2 pixman procs rtmpdump tcl-tk xorgproto zstd +``` + Download and install [Rust](https://www.rust-lang.org/tools/install). [Rustup](https://rustup.rs/) is recommended for Rust setup. Use `rustc` to confirm successful installation. Install toolchains 1.67.1 and 1.72.0 and `cbindgen` and `cargo-lipo` too. You will also have to add the platform target(s) `aarch64-apple-ios` and/or `aarch64-apple-darwin`. You can use the command(s): ``` From b412973a6b9006b3e5413faae82c7bf7cba31a9d Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 11 Apr 2024 11:21:20 -0600 Subject: [PATCH 083/272] disable troublesome unused callbacks --- lib/services/notifications_api.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/services/notifications_api.dart b/lib/services/notifications_api.dart index fa2f5f545..010e3c220 100644 --- a/lib/services/notifications_api.dart +++ b/lib/services/notifications_api.dart @@ -9,14 +9,13 @@ */ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:stackwallet/models/notification_model.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/utilities/prefs.dart'; class NotificationApi { static final _notifications = FlutterLocalNotificationsPlugin(); - static final onNotifications = BehaviorSubject(); + // static final onNotifications = BehaviorSubject(); static Future _notificationDetails() async { return const NotificationDetails( @@ -44,12 +43,12 @@ class NotificationApi { ); await _notifications.initialize( settings, - onDidReceiveNotificationResponse: (payload) async { - onNotifications.add(payload.payload); - }, - onDidReceiveBackgroundNotificationResponse: (payload) async { - onNotifications.add(payload.payload); - }, + // onDidReceiveNotificationResponse: (payload) async { + // onNotifications.add(payload.payload); + // }, + // onDidReceiveBackgroundNotificationResponse: (payload) async { + // onNotifications.add(payload.payload); + // }, ); } From 9b8dc85c8a6519177628ed04429c8e6308dc8542 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 11 Apr 2024 11:21:44 -0600 Subject: [PATCH 084/272] update android compileSdkVersion --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ec8747bff..ae9570217 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 // ndkVersion = "21.1.6352462" // ndkVersion = "25.2.9519653" From b230a123a5eaa86fab508ce87a4a6b0ea20efc51 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 11 Apr 2024 11:34:12 -0600 Subject: [PATCH 085/272] material3 surface tint on scroll "bug" fix --- lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/main.dart b/lib/main.dart index 54ccf3866..011b8a27a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -687,6 +687,7 @@ class _MaterialAppWithThemeState extends ConsumerState appBarTheme: AppBarTheme( centerTitle: false, color: colorScheme.background, + surfaceTintColor: colorScheme.background, elevation: 0, ), inputDecorationTheme: InputDecorationTheme( From 02a53391469146d460a66f9625b59e8f42cf6cd8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 11 Apr 2024 12:18:20 -0600 Subject: [PATCH 086/272] linter warning fix --- .../settings_views/global_settings_view/hidden_settings.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 915d48552..46f1831f9 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -26,7 +26,7 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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"; From 163bce4a9815453d78a4597249c00aae80452a95 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 12 Apr 2024 11:32:37 -0600 Subject: [PATCH 087/272] script tweaks. Requires https://github.com/cypherstack/flutter_libepiccash/commit/a10f90532b65f0040f651ab23577260a0e59cb10 --- scripts/android/build_all.sh | 12 +++++++----- scripts/android/install_ndk.sh | 20 +++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index 28e56acd4..5da88e0e3 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -10,11 +10,13 @@ mkdir -p build . ./config.sh ./install_ndk.sh -(cd ../../crypto_plugins/flutter_liblelantus/scripts/android && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libepiccash/scripts/android && ./install_ndk.sh && ./build_opensll.sh && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/android/ && ./build_all.sh ) && -set_rust_to_1720 & -(cd ../../crypto_plugins/frostdart/scripts/android && ./build_all.sh ) & +PLUGINS_DIR=../../crypto_plugins + +(cd "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android && ./build_all.sh ) & +(cd "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android && ./build_all.sh ) & +(cd "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/ && ./build_all.sh ) && +set_rust_to_1720 && +(cd "${PLUGINS_DIR}"/frostdart/scripts/android && ./build_all.sh ) & wait echo "Done building" diff --git a/scripts/android/install_ndk.sh b/scripts/android/install_ndk.sh index c36651516..0864a2bde 100755 --- a/scripts/android/install_ndk.sh +++ b/scripts/android/install_ndk.sh @@ -2,18 +2,20 @@ mkdir -p build . ./config.sh -TOOLCHAIN_DIR=${WORKDIR}/toolchain ANDROID_NDK_SHA256="8381c440fe61fcbb01e209211ac01b519cd6adf51ab1c2281d5daad6ca4c8c8c" if [ ! -e "$ANDROID_NDK_ZIP" ]; then - curl https://dl.google.com/android/repository/android-ndk-r20b-linux-x86_64.zip -o ${ANDROID_NDK_ZIP} + curl https://dl.google.com/android/repository/android-ndk-r20b-linux-x86_64.zip -o "${ANDROID_NDK_ZIP}" fi -echo $ANDROID_NDK_SHA256 $ANDROID_NDK_ZIP | sha256sum -c || exit 1 +echo "${ANDROID_NDK_SHA256}" "${ANDROID_NDK_ZIP}" | sha256sum -c || exit 1 -mkdir ../../crypto_plugins/flutter_libmonero/scripts/android/build -mkdir ../../crypto_plugins/flutter_liblelantus/scripts/android/build -mkdir ../../crypto_plugins/flutter_libepiccash/scripts/android/build -cp ${ANDROID_NDK_ZIP} ../../crypto_plugins/flutter_libmonero/scripts/android/build/ -cp ${ANDROID_NDK_ZIP} ../../crypto_plugins/flutter_liblelantus/scripts/android/build/ -cp ${ANDROID_NDK_ZIP} ../../crypto_plugins/flutter_libepiccash/scripts/android/build/ +PLUGINS_DIR=../../crypto_plugins + +mkdir -p "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/build +mkdir -p "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android/build +mkdir -p "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android/build + +cp "${ANDROID_NDK_ZIP}" "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/build/ +cp "${ANDROID_NDK_ZIP}" "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android/build/ +cp "${ANDROID_NDK_ZIP}" "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android/build/ From 2f8f4b0e7f27652dff8a245c8e0ce7a7e69094a8 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 12 Apr 2024 13:06:43 -0600 Subject: [PATCH 088/272] update libepiccash submodule --- crypto_plugins/flutter_libepiccash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index aab6a4676..19c76409e 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit aab6a4676188901fbe158d8f1feeb1fc0ea247f8 +Subproject commit 19c76409e55f1bfed58855eb767574604376edb6 From 5f6eabb1dc52a3d2f43ea6b7d6757b45bfdf8be8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 22 Mar 2024 16:19:48 -0500 Subject: [PATCH 089/272] NetworkParams->Network TODO update network params --- lib/wallets/crypto_currency/coins/bitcoin.dart | 12 +++++++++--- lib/wallets/crypto_currency/coins/bitcoincash.dart | 12 +++++++++--- lib/wallets/crypto_currency/coins/dogecoin.dart | 12 +++++++++--- lib/wallets/crypto_currency/coins/ecash.dart | 12 +++++++++--- lib/wallets/crypto_currency/coins/firo.dart | 12 +++++++++--- lib/wallets/crypto_currency/coins/litecoin.dart | 12 +++++++++--- lib/wallets/crypto_currency/coins/namecoin.dart | 7 +++++-- lib/wallets/crypto_currency/coins/particl.dart | 7 +++++-- .../intermediate/bip39_hd_currency.dart | 2 +- 9 files changed, 65 insertions(+), 23 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index d441961a7..01ce800e8 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -51,10 +51,10 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x80, p2pkhPrefix: 0x00, p2shPrefix: 0x05, @@ -62,9 +62,12 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(546), // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xef, p2pkhPrefix: 0x6f, p2shPrefix: 0xc4, @@ -72,6 +75,9 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { pubHDPrefix: 0x043587cf, bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(546), // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); diff --git a/lib/wallets/crypto_currency/coins/bitcoincash.dart b/lib/wallets/crypto_currency/coins/bitcoincash.dart index eaf40d4d0..3ae131346 100644 --- a/lib/wallets/crypto_currency/coins/bitcoincash.dart +++ b/lib/wallets/crypto_currency/coins/bitcoincash.dart @@ -59,10 +59,10 @@ class Bitcoincash extends Bip39HDCurrency { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x80, p2pkhPrefix: 0x00, p2shPrefix: 0x05, @@ -70,9 +70,12 @@ class Bitcoincash extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(546), // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xef, p2pkhPrefix: 0x6f, p2shPrefix: 0xc4, @@ -80,6 +83,9 @@ class Bitcoincash extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(546), // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); diff --git a/lib/wallets/crypto_currency/coins/dogecoin.dart b/lib/wallets/crypto_currency/coins/dogecoin.dart index 2051839e4..18670cecc 100644 --- a/lib/wallets/crypto_currency/coins/dogecoin.dart +++ b/lib/wallets/crypto_currency/coins/dogecoin.dart @@ -102,10 +102,10 @@ class Dogecoin extends Bip39HDCurrency { int get minConfirms => 1; @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x9e, p2pkhPrefix: 0x1e, p2shPrefix: 0x16, @@ -113,9 +113,12 @@ class Dogecoin extends Bip39HDCurrency { pubHDPrefix: 0x02facafd, bech32Hrp: "doge", messagePrefix: '\x18Dogecoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xf1, p2pkhPrefix: 0x71, p2shPrefix: 0xc4, @@ -123,6 +126,9 @@ class Dogecoin extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tdge", messagePrefix: "\x18Dogecoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); diff --git a/lib/wallets/crypto_currency/coins/ecash.dart b/lib/wallets/crypto_currency/coins/ecash.dart index 07e164c6e..36d24ac82 100644 --- a/lib/wallets/crypto_currency/coins/ecash.dart +++ b/lib/wallets/crypto_currency/coins/ecash.dart @@ -57,10 +57,10 @@ class Ecash extends Bip39HDCurrency { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x80, p2pkhPrefix: 0x00, p2shPrefix: 0x05, @@ -68,9 +68,12 @@ class Ecash extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xef, p2pkhPrefix: 0x6f, p2shPrefix: 0xc4, @@ -78,6 +81,9 @@ class Ecash extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index 82eb8ab39..ab008ec6e 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -48,10 +48,10 @@ class Firo extends Bip39HDCurrency { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xd2, p2pkhPrefix: 0x52, p2shPrefix: 0x07, @@ -59,9 +59,12 @@ class Firo extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", messagePrefix: '\x18Zcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xb9, p2pkhPrefix: 0x41, p2shPrefix: 0xb2, @@ -69,6 +72,9 @@ class Firo extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tb", messagePrefix: "\x18Zcoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index 5c964db5a..c8ccf9281 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -55,10 +55,10 @@ class Litecoin extends Bip39HDCurrency { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xb0, p2pkhPrefix: 0x30, p2shPrefix: 0x32, @@ -66,9 +66,12 @@ class Litecoin extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "ltc", messagePrefix: '\x19Litecoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xef, p2pkhPrefix: 0x6f, p2shPrefix: 0x3a, @@ -76,6 +79,9 @@ class Litecoin extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tltc", messagePrefix: "\x19Litecoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); diff --git a/lib/wallets/crypto_currency/coins/namecoin.dart b/lib/wallets/crypto_currency/coins/namecoin.dart index ce5906ee3..4353d70db 100644 --- a/lib/wallets/crypto_currency/coins/namecoin.dart +++ b/lib/wallets/crypto_currency/coins/namecoin.dart @@ -143,10 +143,10 @@ class Namecoin extends Bip39HDCurrency { @override // See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L3474 - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xb4, // From 180. p2pkhPrefix: 0x34, // From 52. p2shPrefix: 0x0d, // From 13. @@ -154,6 +154,9 @@ class Namecoin extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "nc", messagePrefix: '\x18Namecoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); // case CryptoCurrencyNetwork.test: // TODO: [prio=low] Add testnet support. diff --git a/lib/wallets/crypto_currency/coins/particl.dart b/lib/wallets/crypto_currency/coins/particl.dart index 32da6e1f3..57e0b97cf 100644 --- a/lib/wallets/crypto_currency/coins/particl.dart +++ b/lib/wallets/crypto_currency/coins/particl.dart @@ -125,10 +125,10 @@ class Particl extends Bip39HDCurrency { @override // See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L3532 - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x6c, p2pkhPrefix: 0x38, p2shPrefix: 0x3c, @@ -136,6 +136,9 @@ class Particl extends Bip39HDCurrency { pubHDPrefix: 0x696e82d1, bech32Hrp: "pw", messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: BigInt.from(1), // TODO. + feePerKb: BigInt.from(1), // TODO. ); // case CryptoCurrencyNetwork.test: // TODO: [prio=low] Add testnet. diff --git a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart index a2899d149..fa5d5c796 100644 --- a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart @@ -11,7 +11,7 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency. abstract class Bip39HDCurrency extends Bip39Currency { Bip39HDCurrency(super.network); - coinlib.NetworkParams get networkParams; + coinlib.Network get networkParams; Amount get dustLimit; From 3296e590e72844882da9c199fd661c1608c6f253 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 12 Apr 2024 14:01:45 -0600 Subject: [PATCH 090/272] coinlib 2 --- pubspec.lock | 26 ++++++++++++-------------- pubspec.yaml | 17 +---------------- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f4f331580..82aed929e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -271,23 +271,21 @@ packages: source: hosted version: "4.6.0" coinlib: - dependency: "direct overridden" + dependency: transitive description: - path: coinlib - ref: "376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7" - resolved-ref: "376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7" - url: "https://github.com/cypherstack/coinlib.git" - source: git - version: "1.1.0" + name: coinlib + sha256: "44aa3f7b07d3b03d58353e7657f43cdaf76a70ad2cce5bdac9306208099d8df5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" coinlib_flutter: dependency: "direct main" description: - path: coinlib_flutter - ref: "376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7" - resolved-ref: "376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7" - url: "https://github.com/cypherstack/coinlib.git" - source: git - version: "1.1.0" + name: coinlib_flutter + sha256: b352378773158dbaec37bd542c297682f3812f9881acb676971f0f4c5893631f + url: "https://pub.dev" + source: hosted + version: "2.0.0" collection: dependency: transitive description: @@ -2078,5 +2076,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.2.0-0 <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 26edda588..4c35df705 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -171,11 +171,7 @@ dependencies: convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1 - coinlib_flutter: - git: - url: https://github.com/cypherstack/coinlib.git - path: coinlib_flutter - ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7 + coinlib_flutter: ^2.0.0 electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git @@ -227,17 +223,6 @@ dependency_overrides: url: https://github.com/cypherstack/bip47.git ref: a6e7941b98a43a613708b1a12564bc17e712cfc7 - coinlib_flutter: - git: - url: https://github.com/cypherstack/coinlib.git - path: coinlib_flutter - ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7 - coinlib: - git: - url: https://github.com/cypherstack/coinlib.git - path: coinlib - ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7 - # required for dart 3, at least until a fix is merged upstream wakelock_windows: git: From 6fdfa166f2e77b1cb86e435430cc7ba73ba567b9 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 22 Mar 2024 16:46:45 -0500 Subject: [PATCH 091/272] update fusiondart to coinlib 2.0 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 82aed929e..e5026de53 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -839,8 +839,8 @@ packages: dependency: "direct main" description: path: "." - ref: df8f7c627cfc77eaa3e364c0d166f3d04169ae05 - resolved-ref: df8f7c627cfc77eaa3e364c0d166f3d04169ae05 + ref: "7dd8ff0dc9cb0caaac795fa44841a26437edfec3" + resolved-ref: "7dd8ff0dc9cb0caaac795fa44841a26437edfec3" url: "https://github.com/cypherstack/fusiondart.git" source: git version: "1.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4c35df705..242f2fc62 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: fusiondart: git: url: https://github.com/cypherstack/fusiondart.git - ref: df8f7c627cfc77eaa3e364c0d166f3d04169ae05 + ref: 7dd8ff0dc9cb0caaac795fa44841a26437edfec3 # Utility plugins http: ^0.13.0 From f2effa357557483fe37da54dec4d53f96313431b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 25 Mar 2024 23:03:00 -0500 Subject: [PATCH 092/272] add most basic bip86 address derivation stub to be tested according to https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#test-vectors --- .../isar/models/blockchain_data/address.dart | 4 +++- lib/utilities/enums/derive_path_type_enum.dart | 1 + lib/wallets/crypto_currency/coins/bitcoin.dart | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 8adaa4ce5..16516edf4 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -164,7 +164,7 @@ enum AddressType { stellar, tezos, frostMS, - ; + p2tr; String get readableName { switch (this) { @@ -196,6 +196,8 @@ enum AddressType { return "Tezos"; case AddressType.frostMS: return "FrostMS"; + case AddressType.p2tr: + return "Taproot"; // Why not use P2TR, P2PKH, etc.? } } } diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 6d2371735..eb86aa6d5 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -15,6 +15,7 @@ enum DerivePathType { bch44, bip49, bip84, + bip86, eth, eCash44, } diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index 01ce800e8..7008d8b3b 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -30,6 +30,7 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { DerivePathType.bip44, DerivePathType.bip49, DerivePathType.bip84, + DerivePathType.bip86, // P2TR. ]; @override @@ -115,6 +116,9 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { case DerivePathType.bip84: purpose = 84; break; + case DerivePathType.bip86: + purpose = 86; + break; default: throw Exception("DerivePathType $derivePathType not supported"); } @@ -157,6 +161,16 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { return (address: addr, addressType: AddressType.p2wpkh); + case DerivePathType.bip86: + final taproot = coinlib.Taproot(internalKey: publicKey); + + final addr = coinlib.P2TRAddress.fromTaproot( + taproot, + hrp: coinlib.Network.mainnet.bech32Hrp, + ); + + return (address: addr, addressType: AddressType.p2tr); + default: throw Exception("DerivePathType $derivePathType not supported"); } From f630c3f56785379ef2c439f1d05290fd7f639022 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 26 Mar 2024 13:15:37 -0500 Subject: [PATCH 093/272] use HRP for appropriate network fixes testnet --- lib/wallets/crypto_currency/coins/bitcoin.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index 7008d8b3b..46eff4b35 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -166,7 +166,7 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { final addr = coinlib.P2TRAddress.fromTaproot( taproot, - hrp: coinlib.Network.mainnet.bech32Hrp, + hrp: networkParams.bech32Hrp, ); return (address: addr, addressType: AddressType.p2tr); From b50985aec7e4df44147c8294f40d28deb6a01d93 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 26 Mar 2024 16:05:16 -0500 Subject: [PATCH 094/272] detect p2tr outputs --- .../intermediate/bip39_hd_currency.dart | 20 ++++++++++++++----- .../electrumx_interface.dart | 14 +++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart index fa5d5c796..5e6cd60e5 100644 --- a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart @@ -80,12 +80,22 @@ abstract class Bip39HDCurrency extends Bip39Currency { } catch (err) { // Bech32 decode fail } - if (networkParams.bech32Hrp != decodeBech32!.hrp) { - throw ArgumentError('Invalid prefix or Network mismatch'); - } - if (decodeBech32.version != 0) { - throw ArgumentError('Invalid address version'); + if (decodeBech32?.hrp != null) { + // P2TR Bech32m addresses have null hrp. + if (networkParams.bech32Hrp != decodeBech32!.hrp) { + throw ArgumentError('Invalid prefix or Network mismatch'); + } + if (decodeBech32.version != 0) { + throw ArgumentError('Invalid address version'); + } + } else { + // P2TR. + if (address.startsWith('bc1p') || address.startsWith('tb1p')) { + // P2TR (Taproot). + return DerivePathType.bip86; + } } + // P2WPKH return DerivePathType.bip84; } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index aa738c6ec..78847397e 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -682,6 +682,20 @@ mixin ElectrumXInterface on Bip39HDWallet { // TODO: use coinlib + // Check if any txData.recipients are taproot/P2TR outputs. + bool hasTaprootOutput = false; + for (final recipient in txData.recipients!) { + if (cryptoCurrency.addressType(address: recipient.address) == + DerivePathType.bip86) { + hasTaprootOutput = true; + } + } + + if (hasTaprootOutput) { + // Use Coinlib to construct taproot transaction. + // TODO [prio=high]: Implement taproot transaction construction. + } + final txb = bitcoindart.TransactionBuilder( network: bitcoindart.NetworkType( messagePrefix: cryptoCurrency.networkParams.messagePrefix, From 3b0fb693391f79d37035aaeea6d78a0d37403bdc Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 22 Mar 2024 16:19:57 -0500 Subject: [PATCH 095/272] fromScript->fromRedeemScript --- lib/wallets/crypto_currency/coins/bitcoin.dart | 2 +- lib/wallets/crypto_currency/coins/litecoin.dart | 2 +- lib/wallets/crypto_currency/coins/namecoin.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index 46eff4b35..3da33c78f 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -146,7 +146,7 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { hrp: networkParams.bech32Hrp, ).program.script; - final addr = coinlib.P2SHAddress.fromScript( + final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, version: networkParams.p2shPrefix, ); diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index c8ccf9281..67236104d 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -146,7 +146,7 @@ class Litecoin extends Bip39HDCurrency { hrp: networkParams.bech32Hrp, ).program.script; - final addr = coinlib.P2SHAddress.fromScript( + final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, version: networkParams.p2shPrefix, ); diff --git a/lib/wallets/crypto_currency/coins/namecoin.dart b/lib/wallets/crypto_currency/coins/namecoin.dart index 4353d70db..a15078a19 100644 --- a/lib/wallets/crypto_currency/coins/namecoin.dart +++ b/lib/wallets/crypto_currency/coins/namecoin.dart @@ -121,7 +121,7 @@ class Namecoin extends Bip39HDCurrency { hrp: networkParams.bech32Hrp, ).program.script; - final addr = coinlib.P2SHAddress.fromScript( + final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, version: networkParams.p2shPrefix, ); From cf7f8c050a344b3ce26ddc2a44ab35c97c26168d Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 12 Apr 2024 14:55:28 -0600 Subject: [PATCH 096/272] windows gitignore --- windows/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/.gitignore b/windows/.gitignore index d492d0d98..57538caed 100644 --- a/windows/.gitignore +++ b/windows/.gitignore @@ -1,4 +1,4 @@ -flutter/ephemeral/ +flutter/ephemeral # Visual Studio user-specific files. *.suo From e15e8e3530b8e01ebabc3b6a9694419ca5b5102a Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 12 Apr 2024 14:56:10 -0600 Subject: [PATCH 097/272] move bip86 down to preserve index values of other values just in case --- lib/utilities/enums/derive_path_type_enum.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index eb86aa6d5..95b5d9abb 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -15,9 +15,9 @@ enum DerivePathType { bch44, bip49, bip84, - bip86, eth, eCash44, + bip86, } extension DerivePathTypeExt on DerivePathType { From 98410ea8f02290cbe8bf63fc4b8cbaea6e4c4843 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 12 Apr 2024 14:57:41 -0600 Subject: [PATCH 098/272] use coinlib address parsing to check address type and not rely specifically on btc address prefixes to validate taproot addresses --- .../intermediate/bip39_hd_currency.dart | 52 ++++--------------- 1 file changed, 11 insertions(+), 41 deletions(-) diff --git a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart index 5e6cd60e5..62be12f20 100644 --- a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart @@ -1,5 +1,3 @@ -import 'package:bech32/bech32.dart'; -import 'package:bs58check/bs58check.dart' as bs58check; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; @@ -57,47 +55,19 @@ abstract class Bip39HDCurrency extends Bip39Currency { } DerivePathType addressType({required String address}) { - Uint8List? decodeBase58; - Segwit? decodeBech32; - try { - decodeBase58 = bs58check.decode(address); - } catch (err) { - // Base58check decode fail - } - if (decodeBase58 != null) { - if (decodeBase58[0] == networkParams.p2pkhPrefix) { - // P2PKH - return DerivePathType.bip44; - } - if (decodeBase58[0] == networkParams.p2shPrefix) { - // P2SH - return DerivePathType.bip49; - } - throw ArgumentError('Invalid version or Network mismatch'); - } else { - try { - decodeBech32 = segwit.decode(address, networkParams.bech32Hrp); - } catch (err) { - // Bech32 decode fail - } - if (decodeBech32?.hrp != null) { - // P2TR Bech32m addresses have null hrp. - if (networkParams.bech32Hrp != decodeBech32!.hrp) { - throw ArgumentError('Invalid prefix or Network mismatch'); - } - if (decodeBech32.version != 0) { - throw ArgumentError('Invalid address version'); - } - } else { - // P2TR. - if (address.startsWith('bc1p') || address.startsWith('tb1p')) { - // P2TR (Taproot). - return DerivePathType.bip86; - } - } + final address2 = coinlib.Address.fromString(address, networkParams); - // P2WPKH + if (address2 is coinlib.P2PKHAddress) { + return DerivePathType.bip44; + } else if (address2 is coinlib.P2SHAddress) { + return DerivePathType.bip49; + } else if (address2 is coinlib.P2WPKHAddress) { return DerivePathType.bip84; + } else if (address2 is coinlib.P2TRAddress) { + return DerivePathType.bip86; + } else { + // TODO: [prio=med] better error handling + throw ArgumentError('Invalid address'); } } } From 2a030bffba7d464d4b065a58bb761fa8e830fa2e Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 10:31:57 -0600 Subject: [PATCH 099/272] linter warning clean up --- .../wallet/wallet_mixin_interfaces/electrumx_interface.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 78847397e..07e48b7bc 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -34,7 +34,7 @@ import 'package:stream_channel/stream_channel.dart'; mixin ElectrumXInterface on Bip39HDWallet { late ElectrumXClient electrumXClient; - late StreamChannel electrumAdapterChannel; + late StreamChannel electrumAdapterChannel; late ElectrumClient electrumAdapterClient; late CachedElectrumXClient electrumXCachedClient; // late SubscribableElectrumXClient subscribableElectrumXClient; @@ -906,7 +906,7 @@ mixin ElectrumXInterface on Bip39HDWallet { final newNode = await _getCurrentElectrumXNode(); try { await electrumXClient.electrumAdapterClient?.close(); - } catch (e, s) { + } catch (e) { if (e.toString().contains("initialized")) { // Ignore. This should happen every first time the wallet is opened. } else { @@ -1189,8 +1189,6 @@ mixin ElectrumXInterface on Bip39HDWallet { coin: cryptoCurrency.coin, ); - print("txn: $txn"); - final vout = jsonUTXO["tx_pos"] as int; final outputs = txn["vout"] as List; From 400f08c8bb4242f71875cc84f852ecce9fb98faa Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 13:22:30 -0600 Subject: [PATCH 100/272] minOutput = dust limit ? --- lib/db/isar/main_db.dart | 2 +- lib/wallets/crypto_currency/coins/bitcoin.dart | 5 +++-- lib/wallets/crypto_currency/coins/bitcoincash.dart | 4 ++-- lib/wallets/crypto_currency/coins/dogecoin.dart | 4 ++-- lib/wallets/crypto_currency/coins/ecash.dart | 4 ++-- lib/wallets/crypto_currency/coins/firo.dart | 4 ++-- lib/wallets/crypto_currency/coins/litecoin.dart | 4 ++-- lib/wallets/crypto_currency/coins/namecoin.dart | 2 +- lib/wallets/crypto_currency/coins/particl.dart | 2 +- 9 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index ac5a544f4..7d10c8720 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -72,7 +72,7 @@ class MainDB { ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, - inspector: false, + inspector: true, name: "wallet_data", maxSizeMiB: 512, ); diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index 3da33c78f..a8a1a7aa7 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -64,7 +64,7 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(546), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: @@ -77,7 +77,7 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(546), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); default: @@ -140,6 +140,7 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { return (address: addr, addressType: AddressType.p2pkh); + // TODO: [prio=high] verify this works similarly to bitcoindart's p2sh or something(!!) case DerivePathType.bip49: final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( publicKey, diff --git a/lib/wallets/crypto_currency/coins/bitcoincash.dart b/lib/wallets/crypto_currency/coins/bitcoincash.dart index 3ae131346..7bda07ad4 100644 --- a/lib/wallets/crypto_currency/coins/bitcoincash.dart +++ b/lib/wallets/crypto_currency/coins/bitcoincash.dart @@ -71,7 +71,7 @@ class Bitcoincash extends Bip39HDCurrency { bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(546), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: @@ -84,7 +84,7 @@ class Bitcoincash extends Bip39HDCurrency { bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(546), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); default: diff --git a/lib/wallets/crypto_currency/coins/dogecoin.dart b/lib/wallets/crypto_currency/coins/dogecoin.dart index 18670cecc..f79dfc2cf 100644 --- a/lib/wallets/crypto_currency/coins/dogecoin.dart +++ b/lib/wallets/crypto_currency/coins/dogecoin.dart @@ -114,7 +114,7 @@ class Dogecoin extends Bip39HDCurrency { bech32Hrp: "doge", messagePrefix: '\x18Dogecoin Signed Message:\n', minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: @@ -127,7 +127,7 @@ class Dogecoin extends Bip39HDCurrency { bech32Hrp: "tdge", messagePrefix: "\x18Dogecoin Signed Message:\n", minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); default: diff --git a/lib/wallets/crypto_currency/coins/ecash.dart b/lib/wallets/crypto_currency/coins/ecash.dart index 36d24ac82..35306f065 100644 --- a/lib/wallets/crypto_currency/coins/ecash.dart +++ b/lib/wallets/crypto_currency/coins/ecash.dart @@ -69,7 +69,7 @@ class Ecash extends Bip39HDCurrency { bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: @@ -82,7 +82,7 @@ class Ecash extends Bip39HDCurrency { bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); default: diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index ab008ec6e..bcc2411ee 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -60,7 +60,7 @@ class Firo extends Bip39HDCurrency { bech32Hrp: "bc", messagePrefix: '\x18Zcoin Signed Message:\n', minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: @@ -73,7 +73,7 @@ class Firo extends Bip39HDCurrency { bech32Hrp: "tb", messagePrefix: "\x18Zcoin Signed Message:\n", minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); default: diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index 67236104d..0088df184 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -67,7 +67,7 @@ class Litecoin extends Bip39HDCurrency { bech32Hrp: "ltc", messagePrefix: '\x19Litecoin Signed Message:\n', minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: @@ -80,7 +80,7 @@ class Litecoin extends Bip39HDCurrency { bech32Hrp: "tltc", messagePrefix: "\x19Litecoin Signed Message:\n", minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); default: diff --git a/lib/wallets/crypto_currency/coins/namecoin.dart b/lib/wallets/crypto_currency/coins/namecoin.dart index a15078a19..6f5de4068 100644 --- a/lib/wallets/crypto_currency/coins/namecoin.dart +++ b/lib/wallets/crypto_currency/coins/namecoin.dart @@ -155,7 +155,7 @@ class Namecoin extends Bip39HDCurrency { bech32Hrp: "nc", messagePrefix: '\x18Namecoin Signed Message:\n', minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); // case CryptoCurrencyNetwork.test: diff --git a/lib/wallets/crypto_currency/coins/particl.dart b/lib/wallets/crypto_currency/coins/particl.dart index 57e0b97cf..d9e99988e 100644 --- a/lib/wallets/crypto_currency/coins/particl.dart +++ b/lib/wallets/crypto_currency/coins/particl.dart @@ -137,7 +137,7 @@ class Particl extends Bip39HDCurrency { bech32Hrp: "pw", messagePrefix: '\x18Bitcoin Signed Message:\n', minFee: BigInt.from(1), // TODO [prio=high]. - minOutput: BigInt.from(1), // TODO. + minOutput: dustLimit.raw, // TODO. feePerKb: BigInt.from(1), // TODO. ); // case CryptoCurrencyNetwork.test: From 15b66131835200ac4392e75abc3ce9d0af1a1a26 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 13:27:22 -0600 Subject: [PATCH 101/272] build runner mocks --- test/cached_electrumx_test.mocks.dart | 51 +++- test/electrumx_test.mocks.dart | 13 + .../pages/send_view/send_view_test.mocks.dart | 13 + .../exchange/exchange_view_test.mocks.dart | 13 + ...allet_settings_view_screen_test.mocks.dart | 175 ++++++----- .../bitcoin/bitcoin_wallet_test.mocks.dart | 279 ++++++++++-------- .../bitcoincash_wallet_test.mocks.dart | 279 ++++++++++-------- .../dogecoin/dogecoin_wallet_test.mocks.dart | 279 ++++++++++-------- .../namecoin/namecoin_wallet_test.mocks.dart | 279 ++++++++++-------- .../particl/particl_wallet_test.mocks.dart | 279 ++++++++++-------- .../managed_favorite_test.mocks.dart | 13 + .../node_options_sheet_test.mocks.dart | 13 + .../transaction_card_test.mocks.dart | 13 + 13 files changed, 1036 insertions(+), 663 deletions(-) diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 53ddd8cd6..dcbb78453 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -112,6 +112,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: false, ) as bool); @override + _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override _i5.Future request({ required String? command, List? args = const [], @@ -134,9 +143,9 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -151,9 +160,8 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #retries: retries, }, ), - returnValue: _i5.Future>>.value( - >[]), - ) as _i5.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override _i5.Future ping({ String? requestID, @@ -243,17 +251,17 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { >[]), ) as _i5.Future>>); @override - _i5.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i5.Future>>>.value( - >>{}), - ) as _i5.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override _i5.Future>> getUTXOs({ required String? scripthash, @@ -272,17 +280,17 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { >[]), ) as _i5.Future>>); @override - _i5.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i5.Future>>>.value( - >>{}), - ) as _i5.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override _i5.Future> getTransaction({ required String? txHash, @@ -855,6 +863,19 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override + bool get frostEnabled => (super.noSuchMethod( + Invocation.getter(#frostEnabled), + returnValue: false, + ) as bool); + @override + set frostEnabled(bool? frostEnabled) => super.noSuchMethod( + Invocation.setter( + #frostEnabled, + frostEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/electrumx_test.mocks.dart b/test/electrumx_test.mocks.dart index aaf3e0810..3f0660617 100644 --- a/test/electrumx_test.mocks.dart +++ b/test/electrumx_test.mocks.dart @@ -532,6 +532,19 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override + bool get frostEnabled => (super.noSuchMethod( + Invocation.getter(#frostEnabled), + returnValue: false, + ) as bool); + @override + set frostEnabled(bool? frostEnabled) => super.noSuchMethod( + Invocation.setter( + #frostEnabled, + frostEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index fba574b15..c2f2e6843 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -978,6 +978,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get frostEnabled => (super.noSuchMethod( + Invocation.getter(#frostEnabled), + returnValue: false, + ) as bool); + @override + set frostEnabled(bool? frostEnabled) => super.noSuchMethod( + Invocation.setter( + #frostEnabled, + frostEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 1d97e1d48..fc4a829e1 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -454,6 +454,19 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); @override + bool get frostEnabled => (super.noSuchMethod( + Invocation.getter(#frostEnabled), + returnValue: false, + ) as bool); + @override + set frostEnabled(bool? frostEnabled) => super.noSuchMethod( + Invocation.setter( + #frostEnabled, + frostEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index bb7ed9b5b..669741692 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -3,17 +3,18 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; -import 'dart:ui' as _i10; +import 'dart:async' as _i5; +import 'dart:ui' as _i11; -import 'package:local_auth/auth_strings.dart' as _i7; -import 'package:local_auth/local_auth.dart' as _i6; +import 'package:electrum_adapter/electrum_adapter.dart' as _i3; +import 'package:local_auth/auth_strings.dart' as _i8; +import 'package:local_auth/local_auth.dart' as _i7; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i4; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i2; -import 'package:stackwallet/services/wallets_service.dart' as _i9; -import 'package:stackwallet/utilities/biometrics.dart' as _i8; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i5; +import 'package:stackwallet/services/wallets_service.dart' as _i10; +import 'package:stackwallet/utilities/biometrics.dart' as _i9; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -37,11 +38,22 @@ class _FakeElectrumXClient_0 extends _i1.SmartFake ); } +class _FakeElectrumClient_1 extends _i1.SmartFake + implements _i3.ElectrumClient { + _FakeElectrumClient_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i3.CachedElectrumXClient { + implements _i4.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @@ -55,10 +67,37 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i2.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i3.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( + Invocation.getter(#electrumAdapterClient), + returnValue: _FakeElectrumClient_1( + this, + Invocation.getter(#electrumAdapterClient), + ), + ) as _i3.ElectrumClient); + @override + set electrumAdapterClient(_i3.ElectrumClient? _electrumAdapterClient) => + super.noSuchMethod( + Invocation.setter( + #electrumAdapterClient, + _electrumAdapterClient, + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future<_i3.ElectrumClient> Function() get electrumAdapterUpdateCallback => + (super.noSuchMethod( + Invocation.getter(#electrumAdapterUpdateCallback), + returnValue: () => + _i5.Future<_i3.ElectrumClient>.value(_FakeElectrumClient_1( + this, + Invocation.getter(#electrumAdapterUpdateCallback), + )), + ) as _i5.Future<_i3.ElectrumClient> Function()); + @override + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i5.Coin? coin, + required _i6.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -71,13 +110,13 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i5.Coin? coin, + required _i6.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -90,8 +129,8 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -109,9 +148,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i5.Coin? coin, + required _i6.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -125,11 +164,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i5.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i6.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -141,53 +180,53 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i5.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i5.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i6.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [LocalAuthentication]. /// /// See the documentation for Mockito's code generation for more information. class MockLocalAuthentication extends _i1.Mock - implements _i6.LocalAuthentication { + implements _i7.LocalAuthentication { MockLocalAuthentication() { _i1.throwOnMissingStub(this); } @override - _i4.Future get canCheckBiometrics => (super.noSuchMethod( + _i5.Future get canCheckBiometrics => (super.noSuchMethod( Invocation.getter(#canCheckBiometrics), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future authenticateWithBiometrics({ + _i5.Future authenticateWithBiometrics({ required String? localizedReason, bool? useErrorDialogs = true, bool? stickyAuth = false, - _i7.AndroidAuthMessages? androidAuthStrings = - const _i7.AndroidAuthMessages(), - _i7.IOSAuthMessages? iOSAuthStrings = const _i7.IOSAuthMessages(), + _i8.AndroidAuthMessages? androidAuthStrings = + const _i8.AndroidAuthMessages(), + _i8.IOSAuthMessages? iOSAuthStrings = const _i8.IOSAuthMessages(), bool? sensitiveTransaction = true, }) => (super.noSuchMethod( @@ -203,16 +242,16 @@ class MockLocalAuthentication extends _i1.Mock #sensitiveTransaction: sensitiveTransaction, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future authenticate({ + _i5.Future authenticate({ required String? localizedReason, bool? useErrorDialogs = true, bool? stickyAuth = false, - _i7.AndroidAuthMessages? androidAuthStrings = - const _i7.AndroidAuthMessages(), - _i7.IOSAuthMessages? iOSAuthStrings = const _i7.IOSAuthMessages(), + _i8.AndroidAuthMessages? androidAuthStrings = + const _i8.AndroidAuthMessages(), + _i8.IOSAuthMessages? iOSAuthStrings = const _i8.IOSAuthMessages(), bool? sensitiveTransaction = true, bool? biometricOnly = false, }) => @@ -230,46 +269,46 @@ class MockLocalAuthentication extends _i1.Mock #biometricOnly: biometricOnly, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future stopAuthentication() => (super.noSuchMethod( + _i5.Future stopAuthentication() => (super.noSuchMethod( Invocation.method( #stopAuthentication, [], ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future isDeviceSupported() => (super.noSuchMethod( + _i5.Future isDeviceSupported() => (super.noSuchMethod( Invocation.method( #isDeviceSupported, [], ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getAvailableBiometrics() => + _i5.Future> getAvailableBiometrics() => (super.noSuchMethod( Invocation.method( #getAvailableBiometrics, [], ), returnValue: - _i4.Future>.value(<_i6.BiometricType>[]), - ) as _i4.Future>); + _i5.Future>.value(<_i7.BiometricType>[]), + ) as _i5.Future>); } /// A class which mocks [Biometrics]. /// /// See the documentation for Mockito's code generation for more information. -class MockBiometrics extends _i1.Mock implements _i8.Biometrics { +class MockBiometrics extends _i1.Mock implements _i9.Biometrics { MockBiometrics() { _i1.throwOnMissingStub(this); } @override - _i4.Future authenticate({ + _i5.Future authenticate({ required String? cancelButtonText, required String? localizedReason, required String? title, @@ -284,28 +323,28 @@ class MockBiometrics extends _i1.Mock implements _i8.Biometrics { #title: title, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); } /// A class which mocks [WalletsService]. /// /// See the documentation for Mockito's code generation for more information. -class MockWalletsService extends _i1.Mock implements _i9.WalletsService { +class MockWalletsService extends _i1.Mock implements _i10.WalletsService { @override - _i4.Future> get walletNames => + _i5.Future> get walletNames => (super.noSuchMethod( Invocation.getter(#walletNames), - returnValue: _i4.Future>.value( - {}), - ) as _i4.Future>); + returnValue: _i5.Future>.value( + {}), + ) as _i5.Future>); @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, ) as bool); @override - void addListener(_i10.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i11.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -313,7 +352,7 @@ class MockWalletsService extends _i1.Mock implements _i9.WalletsService { returnValueForMissingStub: null, ); @override - void removeListener(_i10.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i11.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index f3b618de7..9e2e9d205 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -3,15 +3,16 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; import 'package:decimal/decimal.dart' as _i2; +import 'package:electrum_adapter/electrum_adapter.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -55,6 +56,17 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } +class _FakeElectrumClient_3 extends _i1.SmartFake + implements _i4.ElectrumClient { + _FakeElectrumClient_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. @@ -109,7 +121,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +149,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +169,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +185,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +196,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +207,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +223,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +240,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +256,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +285,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +317,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +336,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +352,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +369,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +398,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +414,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +430,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +452,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i2.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +468,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #estimateFee, @@ -459,15 +479,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #relayFee, @@ -475,14 +495,14 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @@ -496,10 +516,37 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i3.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( + Invocation.getter(#electrumAdapterClient), + returnValue: _FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterClient), + ), + ) as _i4.ElectrumClient); + @override + set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => + super.noSuchMethod( + Invocation.setter( + #electrumAdapterClient, + _electrumAdapterClient, + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => + (super.noSuchMethod( + Invocation.getter(#electrumAdapterUpdateCallback), + returnValue: () => + _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterUpdateCallback), + )), + ) as _i5.Future<_i4.ElectrumClient> Function()); + @override + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +559,13 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -531,8 +578,8 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +597,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +613,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +629,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +687,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +704,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index 3a2d12610..a2b73c2ef 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -3,15 +3,16 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; import 'package:decimal/decimal.dart' as _i2; +import 'package:electrum_adapter/electrum_adapter.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -55,6 +56,17 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } +class _FakeElectrumClient_3 extends _i1.SmartFake + implements _i4.ElectrumClient { + _FakeElectrumClient_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. @@ -109,7 +121,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +149,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +169,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +185,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +196,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +207,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +223,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +240,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +256,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +285,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +317,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +336,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +352,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +369,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +398,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +414,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +430,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +452,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i2.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +468,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #estimateFee, @@ -459,15 +479,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #relayFee, @@ -475,14 +495,14 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @@ -496,10 +516,37 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i3.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( + Invocation.getter(#electrumAdapterClient), + returnValue: _FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterClient), + ), + ) as _i4.ElectrumClient); + @override + set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => + super.noSuchMethod( + Invocation.setter( + #electrumAdapterClient, + _electrumAdapterClient, + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => + (super.noSuchMethod( + Invocation.getter(#electrumAdapterUpdateCallback), + returnValue: () => + _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterUpdateCallback), + )), + ) as _i5.Future<_i4.ElectrumClient> Function()); + @override + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +559,13 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -531,8 +578,8 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +597,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +613,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +629,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +687,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +704,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 87341f635..4aae75b2e 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -3,15 +3,16 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; import 'package:decimal/decimal.dart' as _i2; +import 'package:electrum_adapter/electrum_adapter.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -55,6 +56,17 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } +class _FakeElectrumClient_3 extends _i1.SmartFake + implements _i4.ElectrumClient { + _FakeElectrumClient_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. @@ -109,7 +121,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +149,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +169,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +185,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +196,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +207,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +223,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +240,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +256,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +285,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +317,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +336,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +352,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +369,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +398,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +414,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +430,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +452,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i2.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +468,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #estimateFee, @@ -459,15 +479,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #relayFee, @@ -475,14 +495,14 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @@ -496,10 +516,37 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i3.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( + Invocation.getter(#electrumAdapterClient), + returnValue: _FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterClient), + ), + ) as _i4.ElectrumClient); + @override + set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => + super.noSuchMethod( + Invocation.setter( + #electrumAdapterClient, + _electrumAdapterClient, + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => + (super.noSuchMethod( + Invocation.getter(#electrumAdapterUpdateCallback), + returnValue: () => + _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterUpdateCallback), + )), + ) as _i5.Future<_i4.ElectrumClient> Function()); + @override + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +559,13 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -531,8 +578,8 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +597,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +613,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +629,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +687,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +704,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index 1102ebf10..bdf67b8a3 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -3,15 +3,16 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; import 'package:decimal/decimal.dart' as _i2; +import 'package:electrum_adapter/electrum_adapter.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -55,6 +56,17 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } +class _FakeElectrumClient_3 extends _i1.SmartFake + implements _i4.ElectrumClient { + _FakeElectrumClient_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. @@ -109,7 +121,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +149,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +169,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +185,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +196,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +207,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +223,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +240,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +256,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +285,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +317,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +336,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +352,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +369,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +398,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +414,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +430,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +452,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i2.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +468,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #estimateFee, @@ -459,15 +479,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #relayFee, @@ -475,14 +495,14 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @@ -496,10 +516,37 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i3.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( + Invocation.getter(#electrumAdapterClient), + returnValue: _FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterClient), + ), + ) as _i4.ElectrumClient); + @override + set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => + super.noSuchMethod( + Invocation.setter( + #electrumAdapterClient, + _electrumAdapterClient, + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => + (super.noSuchMethod( + Invocation.getter(#electrumAdapterUpdateCallback), + returnValue: () => + _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterUpdateCallback), + )), + ) as _i5.Future<_i4.ElectrumClient> Function()); + @override + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +559,13 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -531,8 +578,8 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +597,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +613,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +629,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +687,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +704,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index b8ef7a694..4c03c2fe0 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -3,15 +3,16 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; import 'package:decimal/decimal.dart' as _i2; +import 'package:electrum_adapter/electrum_adapter.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -55,6 +56,17 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } +class _FakeElectrumClient_3 extends _i1.SmartFake + implements _i4.ElectrumClient { + _FakeElectrumClient_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. @@ -109,7 +121,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +149,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +169,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +185,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +196,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +207,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +223,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +240,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +256,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +285,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +317,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +336,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +352,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +369,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +398,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +414,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +430,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +452,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i2.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +468,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #estimateFee, @@ -459,15 +479,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( this, Invocation.method( #relayFee, @@ -475,14 +495,14 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i2.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @@ -496,10 +516,37 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i3.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( + Invocation.getter(#electrumAdapterClient), + returnValue: _FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterClient), + ), + ) as _i4.ElectrumClient); + @override + set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => + super.noSuchMethod( + Invocation.setter( + #electrumAdapterClient, + _electrumAdapterClient, + ), + returnValueForMissingStub: null, + ); + @override + _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => + (super.noSuchMethod( + Invocation.getter(#electrumAdapterUpdateCallback), + returnValue: () => + _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( + this, + Invocation.getter(#electrumAdapterUpdateCallback), + )), + ) as _i5.Future<_i4.ElectrumClient> Function()); + @override + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +559,13 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -531,8 +578,8 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +597,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +613,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +629,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +687,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +704,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index e78909a96..d2b31bb6d 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -712,6 +712,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get frostEnabled => (super.noSuchMethod( + Invocation.getter(#frostEnabled), + returnValue: false, + ) as bool); + @override + set frostEnabled(bool? frostEnabled) => super.noSuchMethod( + Invocation.setter( + #frostEnabled, + frostEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 3d0a10b68..57c22126e 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -598,6 +598,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get frostEnabled => (super.noSuchMethod( + Invocation.getter(#frostEnabled), + returnValue: false, + ) as bool); + @override + set frostEnabled(bool? frostEnabled) => super.noSuchMethod( + Invocation.setter( + #frostEnabled, + frostEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index e3880f1b6..8841fb5db 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -698,6 +698,19 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { returnValueForMissingStub: null, ); @override + bool get frostEnabled => (super.noSuchMethod( + Invocation.getter(#frostEnabled), + returnValue: false, + ) as bool); + @override + set frostEnabled(bool? frostEnabled) => super.noSuchMethod( + Invocation.setter( + #frostEnabled, + frostEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, From 99373e6dbc1b28bad9412ca5b9c7081e72a8752a Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 13:27:41 -0600 Subject: [PATCH 102/272] build runner isar schema modification --- lib/models/isar/models/blockchain_data/address.g.dart | 2 ++ lib/wallets/isar/models/wallet_info.g.dart | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 7d3aff776..ae96a3ac9 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -267,6 +267,7 @@ const _AddresstypeEnumValueMap = { 'stellar': 11, 'tezos': 12, 'frostMS': 13, + 'p2tr': 14, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -283,6 +284,7 @@ const _AddresstypeValueEnumMap = { 11: AddressType.stellar, 12: AddressType.tezos, 13: AddressType.frostMS, + 14: AddressType.p2tr, }; Id _addressGetId(Address object) { diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index 0b809ddcb..5e0ed135f 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -266,6 +266,7 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'stellar': 11, 'tezos': 12, 'frostMS': 13, + 'p2tr': 14, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -282,6 +283,7 @@ const _WalletInfomainAddressTypeValueEnumMap = { 11: AddressType.stellar, 12: AddressType.tezos, 13: AddressType.frostMS, + 14: AddressType.p2tr, }; Id _walletInfoGetId(WalletInfo object) { From 2f11e0d28c051397c096acd0402180441562103d Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 15:45:24 -0600 Subject: [PATCH 103/272] update libmonero ref with ios boost fix --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index cb876251b..2c684cedb 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit cb876251b97d20b12ddd05268913d2cf4b78f0bf +Subproject commit 2c684cedba6c3d9353c7ea748cadb5a246008027 From 985e2699937176a68f81c8b2abe20432cd986e5c Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 15:47:32 -0600 Subject: [PATCH 104/272] constructor clean up --- .../addresses/sub_widgets/desktop_address_list.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart b/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart index 9a9591e6c..7b94575b4 100644 --- a/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart +++ b/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart @@ -29,10 +29,10 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class DesktopAddressList extends ConsumerStatefulWidget { const DesktopAddressList({ - Key? key, + super.key, required this.walletId, this.searchHeight, - }) : super(key: key); + }); final String walletId; final double? searchHeight; From 023bad0c707cc60b68bb2d4529bdd3382df48327 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 15:48:55 -0600 Subject: [PATCH 105/272] WIP: coinlib 2 migration (taproot txns on btc testnet tested working) --- lib/models/signing_data.dart | 25 +++ .../electrumx_interface.dart | 182 ++++++++++++------ 2 files changed, 149 insertions(+), 58 deletions(-) diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart index 0937a2d8b..f510f7845 100644 --- a/lib/models/signing_data.dart +++ b/lib/models/signing_data.dart @@ -11,6 +11,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'; @@ -40,3 +41,27 @@ class SigningData { "}"; } } + +// class SigningData { +// SigningData({ +// required this.derivePathType, +// required this.utxo, +// this.keyPair, +// this.redeemScript, +// }); +// +// final DerivePathType derivePathType; +// final UTXO utxo; +// ({ECPrivateKey privateKey, ECPublicKey publicKey})? keyPair; +// Uint8List? redeemScript; +// +// @override +// String toString() { +// return "SigningData{\n" +// " derivePathType: $derivePathType,\n" +// " utxo: $utxo,\n" +// " keyPair: $keyPair,\n" +// " redeemScript: $redeemScript,\n" +// "}"; +// } +// } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 07e48b7bc..ff8d7e4da 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'dart:typed_data'; import 'package:bip47/src/util.dart'; import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; @@ -599,7 +600,7 @@ mixin ElectrumXInterface on Bip39HDWallet { // final coinlib.Input input; final pubKey = keys.publicKey.data; - final bitcoindart.PaymentData data; + final bitcoindart.PaymentData? data; switch (sd.derivePathType) { case DerivePathType.bip44: @@ -651,12 +652,20 @@ mixin ElectrumXInterface on Bip39HDWallet { .data; break; + case DerivePathType.bip86: + data = null; + break; + default: throw Exception("DerivePathType unsupported"); } // sd.output = input.script!.compiled; - sd.output = data.output!; + + if (sd.derivePathType != DerivePathType.bip86) { + sd.output = data!.output!; + } + sd.keyPair = bitcoindart.ECPair.fromPrivateKey( keys.privateKey.data, compressed: keys.privateKey.compressed, @@ -680,57 +689,87 @@ mixin ElectrumXInterface on Bip39HDWallet { Logging.instance .log("Starting buildTransaction ----------", level: LogLevel.Info); - // TODO: use coinlib - - // Check if any txData.recipients are taproot/P2TR outputs. - bool hasTaprootOutput = false; - for (final recipient in txData.recipients!) { - if (cryptoCurrency.addressType(address: recipient.address) == - DerivePathType.bip86) { - hasTaprootOutput = true; - } - } - - if (hasTaprootOutput) { - // Use Coinlib to construct taproot transaction. - // TODO [prio=high]: Implement taproot transaction construction. - } - - final txb = bitcoindart.TransactionBuilder( - network: bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ), - maximumFeeRate: maximumFeerate, - ); - const version = 1; // TODO possibly override this for certain coins? - txb.setVersion(version); - // temp tx data to show in gui while waiting for real data from server final List tempInputs = []; final List tempOutputs = []; + final List prevOuts = []; + + coinlib.Transaction clTx = coinlib.Transaction( + version: 1, // TODO: check if we can use 3 (as is default in coinlib) + inputs: [], + outputs: [], + ); + // Add transaction inputs for (var i = 0; i < utxoSigningData.length; i++) { final txid = utxoSigningData[i].utxo.txid; - txb.addInput( - txid, + + final hash = Uint8List.fromList(txid.fromHex.reversed.toList()); + + final prevOutpoint = coinlib.OutPoint( + hash, utxoSigningData[i].utxo.vout, - null, - utxoSigningData[i].output!, - cryptoCurrency.networkParams.bech32Hrp, ); + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(utxoSigningData[i].utxo.value), + coinlib.Address.fromString( + utxoSigningData[i].utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + + final coinlib.Input input; + + switch (utxoSigningData[i].derivePathType) { + case DerivePathType.bip44: + case DerivePathType.bch44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + // publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: coinlib.ECPublicKey( + utxoSigningData[i].keyPair!.publicKey, + ), + sequence: 0xffffffff - 1, + ); + + // TODO: fix this as it is (probably) wrong! + case DerivePathType.bip49: + input = coinlib.P2SHMultisigInput( + prevOut: prevOutpoint, + program: coinlib.MultisigProgram.decompile( + utxoSigningData[i].redeemScript!, + ), + sequence: 0xffffffff - 1, + ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + // publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: coinlib.ECPublicKey( + utxoSigningData[i].keyPair!.publicKey, + ), + sequence: 0xffffffff - 1, + ); + + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + + default: + throw UnsupportedError( + "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + ); + } + + clTx = clTx.addInput(input); + tempInputs.add( InputV2.isarCantDoRequiredInDefaultConstructor( - scriptSigHex: txb.inputs.first.script?.toHex, + scriptSigHex: input.scriptSig.toHex, scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( @@ -751,12 +790,18 @@ mixin ElectrumXInterface on Bip39HDWallet { // Add transaction output for (var i = 0; i < txData.recipients!.length; i++) { - txb.addOutput( + final address = coinlib.Address.fromString( normalizeAddress(txData.recipients![i].address), - txData.recipients![i].amount.raw.toInt(), - cryptoCurrency.networkParams.bech32Hrp, + cryptoCurrency.networkParams, ); + final output = coinlib.Output.fromAddress( + txData.recipients![i].amount.raw, + address, + ); + + clTx = clTx.addOutput(output); + tempOutputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", @@ -779,13 +824,37 @@ mixin ElectrumXInterface on Bip39HDWallet { try { // Sign the transaction accordingly for (var i = 0; i < utxoSigningData.length; i++) { - txb.sign( - vin: i, - keyPair: utxoSigningData[i].keyPair!, - witnessValue: utxoSigningData[i].utxo.value, - redeemScript: utxoSigningData[i].redeemScript, - overridePrefix: cryptoCurrency.networkParams.bech32Hrp, + final value = BigInt.from(utxoSigningData[i].utxo.value); + coinlib.ECPrivateKey key = coinlib.ECPrivateKey( + utxoSigningData[i].keyPair!.privateKey!, + compressed: utxoSigningData[i].keyPair!.compressed, ); + + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot( + internalKey: coinlib.ECPublicKey( + utxoSigningData[i].keyPair!.publicKey, + ), + ); + + key = taproot.tweakPrivateKey(key); + } + + clTx = clTx.sign( + inputN: i, + value: value, + // key: utxoSigningData[i].keyPair!.privateKey, + key: key, + prevOuts: prevOuts, + ); + + // txb.sign( + // vin: i, + // keyPair: utxoSigningData[i].keyPair!, + // witnessValue: utxoSigningData[i].utxo.value, + // redeemScript: utxoSigningData[i].redeemScript, + // overridePrefix: cryptoCurrency.networkParams.bech32Hrp, + // ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", @@ -793,22 +862,19 @@ mixin ElectrumXInterface on Bip39HDWallet { rethrow; } - final builtTx = txb.build(cryptoCurrency.networkParams.bech32Hrp); - final vSize = builtTx.virtualSize(); - return txData.copyWith( - raw: builtTx.toHex(), - vSize: vSize, + raw: clTx.toHex(), + vSize: clTx.size, tempTx: TransactionV2( walletId: walletId, blockHash: null, - hash: builtTx.getId(), - txid: builtTx.getId(), + hash: clTx.hashHex, + txid: clTx.txid, height: null, timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(tempInputs), outputs: List.unmodifiable(tempOutputs), - version: version, + version: clTx.version, type: tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) && txData.paynymAccountLite == null From 8365ad562c081669f03ea29dc984e3d7942c96b2 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 16:51:05 -0600 Subject: [PATCH 106/272] update spark lib version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 242f2fc62..795c34a90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git - ref: 3f986ca1a94bdac5d31373454c989cc2f5842de8 + ref: 65a6676e37f1bcaf2e293afbd50e50c81394276c flutter_libmonero: path: ./crypto_plugins/flutter_libmonero From b27b672b67d5cef6877fe54b27506858cd11e07f Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 16 Apr 2024 09:41:17 -0600 Subject: [PATCH 107/272] update lib --- crypto_plugins/flutter_liblelantus | 2 +- pubspec.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index 9cd241b5e..032407f0f 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit 9cd241b5ea142e21c01dd7639b42603281c43287 +Subproject commit 032407f0f7734f3cec3eefba76c5dc587b9a252d diff --git a/pubspec.lock b/pubspec.lock index e5026de53..d5445f376 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -672,8 +672,8 @@ packages: dependency: "direct main" description: path: "." - ref: "3f986ca1a94bdac5d31373454c989cc2f5842de8" - resolved-ref: "3f986ca1a94bdac5d31373454c989cc2f5842de8" + ref: "65a6676e37f1bcaf2e293afbd50e50c81394276c" + resolved-ref: "65a6676e37f1bcaf2e293afbd50e50c81394276c" url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git version: "0.0.1" From 70a0232de9bd60fc6d4bb4c60d691a6c40fa8c13 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 16 Apr 2024 09:54:28 -0600 Subject: [PATCH 108/272] update frostdart submodule --- crypto_plugins/frostdart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/frostdart b/crypto_plugins/frostdart index 0fbc038a2..d539de234 160000 --- a/crypto_plugins/frostdart +++ b/crypto_plugins/frostdart @@ -1 +1 @@ -Subproject commit 0fbc038a262e3c2d82c7c6e34e194e9a47011d91 +Subproject commit d539de2348bdbb87bac341dcaa6a0755f21d48e2 From 3218216caaaf61ce02f2d491d7ca0b58afde303d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 16 Apr 2024 17:42:51 -0500 Subject: [PATCH 109/272] add tor support bool to CryptoCurrency and override in coin impls --- lib/wallets/crypto_currency/coins/bitcoin.dart | 3 +++ lib/wallets/crypto_currency/coins/bitcoin_frost.dart | 3 +++ lib/wallets/crypto_currency/coins/bitcoincash.dart | 3 +++ lib/wallets/crypto_currency/coins/dogecoin.dart | 3 +++ lib/wallets/crypto_currency/coins/ecash.dart | 3 +++ lib/wallets/crypto_currency/coins/firo.dart | 3 +++ lib/wallets/crypto_currency/coins/litecoin.dart | 3 +++ lib/wallets/crypto_currency/coins/namecoin.dart | 3 +++ lib/wallets/crypto_currency/coins/particl.dart | 3 +++ lib/wallets/crypto_currency/crypto_currency.dart | 3 +++ 10 files changed, 30 insertions(+) diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index a8a1a7aa7..943b74547 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -25,6 +25,9 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { // change this to change the number of confirms a tx needs in order to show as confirmed int get minConfirms => 1; + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index b82d3987c..87b70c038 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -23,6 +23,9 @@ class BitcoinFrost extends FrostCurrency { @override int get minConfirms => 1; + @override + bool get torSupport => true; + @override NodeModel get defaultNode { switch (network) { diff --git a/lib/wallets/crypto_currency/coins/bitcoincash.dart b/lib/wallets/crypto_currency/coins/bitcoincash.dart index 7bda07ad4..b1591dae2 100644 --- a/lib/wallets/crypto_currency/coins/bitcoincash.dart +++ b/lib/wallets/crypto_currency/coins/bitcoincash.dart @@ -34,6 +34,9 @@ class Bitcoincash extends Bip39HDCurrency { // change this to change the number of confirms a tx needs in order to show as confirmed int get minConfirms => 0; // bch zeroconf + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, diff --git a/lib/wallets/crypto_currency/coins/dogecoin.dart b/lib/wallets/crypto_currency/coins/dogecoin.dart index f79dfc2cf..5244fe2d0 100644 --- a/lib/wallets/crypto_currency/coins/dogecoin.dart +++ b/lib/wallets/crypto_currency/coins/dogecoin.dart @@ -20,6 +20,9 @@ class Dogecoin extends Bip39HDCurrency { } } + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, diff --git a/lib/wallets/crypto_currency/coins/ecash.dart b/lib/wallets/crypto_currency/coins/ecash.dart index 35306f065..b6234065f 100644 --- a/lib/wallets/crypto_currency/coins/ecash.dart +++ b/lib/wallets/crypto_currency/coins/ecash.dart @@ -32,6 +32,9 @@ class Ecash extends Bip39HDCurrency { // change this to change the number of confirms a tx needs in order to show as confirmed int get minConfirms => 0; // bch zeroconf + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.eCash44, diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index bcc2411ee..134f5b59a 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -24,6 +24,9 @@ class Firo extends Bip39HDCurrency { @override int get minConfirms => 1; + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index 0088df184..6ec755b4a 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -24,6 +24,9 @@ class Litecoin extends Bip39HDCurrency { // change this to change the number of confirms a tx needs in order to show as confirmed int get minConfirms => 1; + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, diff --git a/lib/wallets/crypto_currency/coins/namecoin.dart b/lib/wallets/crypto_currency/coins/namecoin.dart index 6f5de4068..33449dd74 100644 --- a/lib/wallets/crypto_currency/coins/namecoin.dart +++ b/lib/wallets/crypto_currency/coins/namecoin.dart @@ -22,6 +22,9 @@ class Namecoin extends Bip39HDCurrency { // See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L58 int get minConfirms => 2; + @override + bool get torSupport => true; + @override // See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L80 String constructDerivePath({ diff --git a/lib/wallets/crypto_currency/coins/particl.dart b/lib/wallets/crypto_currency/coins/particl.dart index d9e99988e..0bc34731b 100644 --- a/lib/wallets/crypto_currency/coins/particl.dart +++ b/lib/wallets/crypto_currency/coins/particl.dart @@ -22,6 +22,9 @@ class Particl extends Bip39HDCurrency { // See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L57 int get minConfirms => 1; + @override + bool get torSupport => true; + @override // See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L68 String constructDerivePath( diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 088e83317..8b6f5cdb6 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -20,6 +20,9 @@ abstract class CryptoCurrency { // (used for eth currently) bool get hasTokenSupport => false; + // Override in subclass if the currency has Tor support: + bool get torSupport => false; + // TODO: [prio=low] require these be overridden in concrete implementations to remove reliance on [coin] int get fractionDigits => coin.decimals; BigInt get satsPerCoin => Constants.satsPerCoin(coin); From 1090f5caa20ddfe004e1adae3ffa5f4edc998f3c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 16 Apr 2024 18:19:38 -0500 Subject: [PATCH 110/272] add warning dialog when clicking card of coin incompatible with Tor TODO fix spacing/style. --- lib/widgets/wallet_card.dart | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index bc1c80aa4..31a29fb68 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -30,6 +30,7 @@ import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; @@ -94,6 +95,37 @@ class SimpleWalletCard extends ConsumerWidget { final wallet = ref.read(pWallets).getWallet(walletId); + // If Tor enabled, show a warning if opening a wallet incompatible with Tor. + if (ref.read(prefsChangeNotifierProvider).useTor) { + if (!wallet.cryptoCurrency.torSupport) { + final shouldContinue = await showDialog( + context: context, + builder: (context) => BasicDialog( + title: "Warning! Tor not supported.", + message: "Stacky is not compatible with Tor." + "\n\nBy using it, you will leak your IP address. Are you sure you " + "want to continue?", + // A PrimaryButton widget: + leftButton: PrimaryButton( + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: SecondaryButton( + label: "Continue", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + )) ?? + false; + if (!shouldContinue) { + return; + } + } + } + if (context.mounted) { final Future loadFuture; if (wallet is CwBasedInterface) { From edc8737edc74e97717ea7f4ab4f6697491137aa0 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 17 Apr 2024 13:03:51 -0500 Subject: [PATCH 111/272] add supported coins list to replace the Coin enum someday --- lib/supported_coins.dart | 78 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lib/supported_coins.dart diff --git a/lib/supported_coins.dart b/lib/supported_coins.dart new file mode 100644 index 000000000..f9d081b5a --- /dev/null +++ b/lib/supported_coins.dart @@ -0,0 +1,78 @@ +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/ethereum.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/litecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +/// The supported coins. +class SupportedCoins { + /// A List of our supported coins. + static final List cryptocurrencies = [ + // Mainnet coins. + Bitcoin(CryptoCurrencyNetwork.main), + Monero(CryptoCurrencyNetwork.main), + Banano(CryptoCurrencyNetwork.main), + Bitcoincash(CryptoCurrencyNetwork.main), + BitcoinFrost(CryptoCurrencyNetwork.main), + Dogecoin(CryptoCurrencyNetwork.main), + Ecash(CryptoCurrencyNetwork.main), + Epiccash(CryptoCurrencyNetwork.main), + Ethereum(CryptoCurrencyNetwork.main), + Firo(CryptoCurrencyNetwork.main), + Litecoin(CryptoCurrencyNetwork.main), + Namecoin(CryptoCurrencyNetwork.main), + Nano(CryptoCurrencyNetwork.main), + Particl(CryptoCurrencyNetwork.main), + Stellar(CryptoCurrencyNetwork.main), + Tezos(CryptoCurrencyNetwork.main), + Wownero(CryptoCurrencyNetwork.main), + + /// Testnet coins. + Bitcoin(CryptoCurrencyNetwork.test), + Banano(CryptoCurrencyNetwork.test), + Bitcoincash(CryptoCurrencyNetwork.test), + BitcoinFrost(CryptoCurrencyNetwork.test), + Dogecoin(CryptoCurrencyNetwork.test), + Stellar(CryptoCurrencyNetwork.test), + Firo(CryptoCurrencyNetwork.test), + Litecoin(CryptoCurrencyNetwork.test), + Stellar(CryptoCurrencyNetwork.test), + ]; + + /// A Map linking a CryptoCurrency with its associated Coin. + /// + /// Temporary: Remove when the Coin enum is removed.dd + static final Map coins = { + Coin.bitcoin: Bitcoin(CryptoCurrencyNetwork.main), + Coin.monero: Monero(CryptoCurrencyNetwork.main), + Coin.banano: Banano(CryptoCurrencyNetwork.main), + Coin.bitcoincash: Bitcoincash(CryptoCurrencyNetwork.main), + Coin.bitcoinFrost: BitcoinFrost(CryptoCurrencyNetwork.main), + Coin.dogecoin: Dogecoin(CryptoCurrencyNetwork.main), + Coin.eCash: Ecash(CryptoCurrencyNetwork.main), + Coin.epicCash: Epiccash(CryptoCurrencyNetwork.main), + Coin.ethereum: Ethereum(CryptoCurrencyNetwork.main), + Coin.firo: Firo(CryptoCurrencyNetwork.main), + Coin.litecoin: Litecoin(CryptoCurrencyNetwork.main), + Coin.namecoin: Namecoin(CryptoCurrencyNetwork.main), + Coin.nano: Nano(CryptoCurrencyNetwork.main), + Coin.particl: Particl(CryptoCurrencyNetwork.main), + Coin.stellar: Stellar(CryptoCurrencyNetwork.main), + Coin.tezos: Tezos(CryptoCurrencyNetwork.main), + Coin.wownero: Wownero(CryptoCurrencyNetwork.main), + }; +} From 12030da1b26b67712663c154c4f0318247cd4894 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 17 Apr 2024 13:04:10 -0500 Subject: [PATCH 112/272] add coin impl equality operator overrides --- lib/wallets/crypto_currency/coins/banano.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/bitcoin.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/bitcoin_frost.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/bitcoincash.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/dogecoin.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/ecash.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/epiccash.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/ethereum.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/firo.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/litecoin.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/monero.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/namecoin.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/nano.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/particl.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/stellar.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/tezos.dart | 8 ++++++++ lib/wallets/crypto_currency/coins/wownero.dart | 8 ++++++++ 17 files changed, 136 insertions(+) diff --git a/lib/wallets/crypto_currency/coins/banano.dart b/lib/wallets/crypto_currency/coins/banano.dart index 8f980e947..dfae1b78c 100644 --- a/lib/wallets/crypto_currency/coins/banano.dart +++ b/lib/wallets/crypto_currency/coins/banano.dart @@ -45,4 +45,12 @@ class Banano extends NanoCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Banano && other.network == network; + } + + @override + int get hashCode => Object.hash(Banano, network); } diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index 943b74547..e0126ede1 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -203,4 +203,12 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Bitcoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Bitcoin, network); } diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index 87b70c038..38530f056 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -72,4 +72,12 @@ class BitcoinFrost extends FrostCurrency { // TODO: implement validateAddress for frost addresses return true; } + + @override + bool operator ==(Object other) { + return other is BitcoinFrost && other.network == network; + } + + @override + int get hashCode => Object.hash(BitcoinFrost, network); } diff --git a/lib/wallets/crypto_currency/coins/bitcoincash.dart b/lib/wallets/crypto_currency/coins/bitcoincash.dart index b1591dae2..168e0223a 100644 --- a/lib/wallets/crypto_currency/coins/bitcoincash.dart +++ b/lib/wallets/crypto_currency/coins/bitcoincash.dart @@ -289,4 +289,12 @@ class Bitcoincash extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Bitcoincash && other.network == network; + } + + @override + int get hashCode => Object.hash(Bitcoincash, network); } diff --git a/lib/wallets/crypto_currency/coins/dogecoin.dart b/lib/wallets/crypto_currency/coins/dogecoin.dart index 5244fe2d0..26abdaa85 100644 --- a/lib/wallets/crypto_currency/coins/dogecoin.dart +++ b/lib/wallets/crypto_currency/coins/dogecoin.dart @@ -181,4 +181,12 @@ class Dogecoin extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Dogecoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Dogecoin, network); } diff --git a/lib/wallets/crypto_currency/coins/ecash.dart b/lib/wallets/crypto_currency/coins/ecash.dart index b6234065f..6c068ed4d 100644 --- a/lib/wallets/crypto_currency/coins/ecash.dart +++ b/lib/wallets/crypto_currency/coins/ecash.dart @@ -269,4 +269,12 @@ class Ecash extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Ecash && other.network == network; + } + + @override + int get hashCode => Object.hash(Ecash, network); } diff --git a/lib/wallets/crypto_currency/coins/epiccash.dart b/lib/wallets/crypto_currency/coins/epiccash.dart index 0f4e77af2..49d7a7a7f 100644 --- a/lib/wallets/crypto_currency/coins/epiccash.dart +++ b/lib/wallets/crypto_currency/coins/epiccash.dart @@ -61,4 +61,12 @@ class Epiccash extends Bip39Currency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Epiccash && other.network == network; + } + + @override + int get hashCode => Object.hash(Epiccash, network); } diff --git a/lib/wallets/crypto_currency/coins/ethereum.dart b/lib/wallets/crypto_currency/coins/ethereum.dart index 624fcd3d2..a9bc81380 100644 --- a/lib/wallets/crypto_currency/coins/ethereum.dart +++ b/lib/wallets/crypto_currency/coins/ethereum.dart @@ -34,4 +34,12 @@ class Ethereum extends Bip39Currency { bool validateAddress(String address) { return isValidEthereumAddress(address); } + + @override + bool operator ==(Object other) { + return other is Ethereum && other.network == network; + } + + @override + int get hashCode => Object.hash(Ethereum, network); } diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index 134f5b59a..36ad7d763 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -199,4 +199,12 @@ class Firo extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Firo && other.network == network; + } + + @override + int get hashCode => Object.hash(Firo, network); } diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index 6ec755b4a..efccbb3de 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -212,4 +212,12 @@ class Litecoin extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Litecoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Litecoin, network); } diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 748d1b7eb..34eff8203 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -44,4 +44,12 @@ class Monero extends CryptonoteCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Monero && other.network == network; + } + + @override + int get hashCode => Object.hash(Monero, network); } diff --git a/lib/wallets/crypto_currency/coins/namecoin.dart b/lib/wallets/crypto_currency/coins/namecoin.dart index 33449dd74..c2cff8e57 100644 --- a/lib/wallets/crypto_currency/coins/namecoin.dart +++ b/lib/wallets/crypto_currency/coins/namecoin.dart @@ -185,4 +185,12 @@ class Namecoin extends Bip39HDCurrency { return false; } } + + @override + bool operator ==(Object other) { + return other is Namecoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Namecoin, network); } diff --git a/lib/wallets/crypto_currency/coins/nano.dart b/lib/wallets/crypto_currency/coins/nano.dart index 016a3b796..277a4601f 100644 --- a/lib/wallets/crypto_currency/coins/nano.dart +++ b/lib/wallets/crypto_currency/coins/nano.dart @@ -45,4 +45,12 @@ class Nano extends NanoCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Nano && other.network == network; + } + + @override + int get hashCode => Object.hash(Nano, network); } diff --git a/lib/wallets/crypto_currency/coins/particl.dart b/lib/wallets/crypto_currency/coins/particl.dart index 0bc34731b..ae73d46e5 100644 --- a/lib/wallets/crypto_currency/coins/particl.dart +++ b/lib/wallets/crypto_currency/coins/particl.dart @@ -165,4 +165,12 @@ class Particl extends Bip39HDCurrency { return false; } } + + @override + bool operator ==(Object other) { + return other is Particl && other.network == network; + } + + @override + int get hashCode => Object.hash(Particl, network); } diff --git a/lib/wallets/crypto_currency/coins/stellar.dart b/lib/wallets/crypto_currency/coins/stellar.dart index c7bbc3d0a..aa5f9ce4d 100644 --- a/lib/wallets/crypto_currency/coins/stellar.dart +++ b/lib/wallets/crypto_currency/coins/stellar.dart @@ -39,4 +39,12 @@ class Stellar extends Bip39Currency { @override bool validateAddress(String address) => RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address); + + @override + bool operator ==(Object other) { + return other is Stellar && other.network == network; + } + + @override + int get hashCode => Object.hash(Stellar, network); } diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart index 88a7dadd0..ef0937da6 100644 --- a/lib/wallets/crypto_currency/coins/tezos.dart +++ b/lib/wallets/crypto_currency/coins/tezos.dart @@ -146,4 +146,12 @@ class Tezos extends Bip39Currency { } // =========================================================================== + + @override + bool operator ==(Object other) { + return other is Tezos && other.network == network; + } + + @override + int get hashCode => Object.hash(Tezos, network); } diff --git a/lib/wallets/crypto_currency/coins/wownero.dart b/lib/wallets/crypto_currency/coins/wownero.dart index e96660c00..549d1739f 100644 --- a/lib/wallets/crypto_currency/coins/wownero.dart +++ b/lib/wallets/crypto_currency/coins/wownero.dart @@ -44,4 +44,12 @@ class Wownero extends CryptonoteCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Wownero && other.network == network; + } + + @override + int get hashCode => Object.hash(Wownero, network); } From eff06c88b800841b8fe04d4d3250641823b2ebb1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 17 Apr 2024 13:04:55 -0500 Subject: [PATCH 113/272] add a Tor warning dialog for non-Tor coins on desktop, TODO test mobile. --- .../my_stack_view/wallet_summary_table.dart | 23 +++++++++- lib/widgets/dialogs/tor_warning_dialog.dart | 43 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 lib/widgets/dialogs/tor_warning_dialog.dart diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index 63b45d0c8..8785c7ac6 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -15,6 +15,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/wallets_view/wallets_overview.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/supported_coins.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -24,6 +25,7 @@ import 'package:stackwallet/wallets/isar/providers/all_wallets_info_provider.dar import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletSummaryTable extends ConsumerStatefulWidget { @@ -85,7 +87,26 @@ class _DesktopWalletSummaryRowState extends ConsumerState { bool _hovering = false; - void _onPressed() { + void _onPressed() async { + // Check if Tor is enabled... + if (ref.read(prefsChangeNotifierProvider).useTor) { + // ... and if the coin supports Tor. + final cryptocurrency = SupportedCoins.coins[widget.coin]; + if (cryptocurrency != null && !cryptocurrency!.torSupport) { + // If not, show a Tor warning dialog. + final shouldContinue = await showDialog( + context: context, + builder: (_) => TorWarningDialog( + coin: widget.coin, + ), + ) ?? + false; + if (!shouldContinue) { + return; + } + } + } + showDialog( context: context, builder: (_) => DesktopDialog( diff --git a/lib/widgets/dialogs/tor_warning_dialog.dart b/lib/widgets/dialogs/tor_warning_dialog.dart new file mode 100644 index 000000000..eaa6d37a6 --- /dev/null +++ b/lib/widgets/dialogs/tor_warning_dialog.dart @@ -0,0 +1,43 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; + +class TorWarningDialog extends StatelessWidget { + final Coin coin; + final VoidCallback? onContinue; + final VoidCallback? onCancel; + + TorWarningDialog({ + Key? key, + required this.coin, + this.onContinue, + this.onCancel, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BasicDialog( + title: "Warning! Tor not supported.", + message: "${coin.prettyName} is not compatible with Tor. " + "Continuing will leak your IP address." + "\n\nAre you sure you want to continue?", + // A PrimaryButton widget: + leftButton: PrimaryButton( + label: "Cancel", + onPressed: () { + onCancel?.call(); + Navigator.of(context).pop(false); + }, + ), + rightButton: SecondaryButton( + label: "Continue", + onPressed: () { + onContinue?.call(); + Navigator.of(context).pop(true); + }, + ), + ); + } +} From 30255c665e4faba94d37b677cb374c759846140c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 17 Apr 2024 13:05:23 -0500 Subject: [PATCH 114/272] allow a BasicDialog to optionally flex so the buttons can be pushed to the bottom of the dialog --- lib/widgets/dialogs/basic_dialog.dart | 11 +++++++++++ lib/widgets/dialogs/tor_warning_dialog.dart | 1 + 2 files changed, 12 insertions(+) diff --git a/lib/widgets/dialogs/basic_dialog.dart b/lib/widgets/dialogs/basic_dialog.dart index 271d82049..d2c5e39eb 100644 --- a/lib/widgets/dialogs/basic_dialog.dart +++ b/lib/widgets/dialogs/basic_dialog.dart @@ -26,6 +26,7 @@ class BasicDialog extends StatelessWidget { this.desktopHeight = 474, this.desktopWidth = 641, this.canPopWithBackButton = false, + this.flex = false, }) : super(key: key); final Widget? leftButton; @@ -41,6 +42,8 @@ class BasicDialog extends StatelessWidget { final bool canPopWithBackButton; + final bool flex; + @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; @@ -64,6 +67,10 @@ class BasicDialog extends StatelessWidget { ], ), ), + if (flex) + const Spacer( + flex: 2, + ), if (message != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 32), @@ -72,6 +79,10 @@ class BasicDialog extends StatelessWidget { style: STextStyles.desktopTextSmall(context), ), ), + if (flex) + const Spacer( + flex: 3, + ), if (leftButton != null || rightButton != null) const SizedBox( height: 32, diff --git a/lib/widgets/dialogs/tor_warning_dialog.dart b/lib/widgets/dialogs/tor_warning_dialog.dart index eaa6d37a6..d4bd7dc81 100644 --- a/lib/widgets/dialogs/tor_warning_dialog.dart +++ b/lib/widgets/dialogs/tor_warning_dialog.dart @@ -38,6 +38,7 @@ class TorWarningDialog extends StatelessWidget { Navigator.of(context).pop(true); }, ), + flex: true, ); } } From 39a9dc83db63c8d25b433afc32b8449545c4a50c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 17 Apr 2024 13:18:15 -0500 Subject: [PATCH 115/272] add button to show tor warning dialog in hidden settings menu --- .../global_settings_view/hidden_settings.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 46f1831f9..6ebafad4a 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -10,7 +10,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -20,9 +19,11 @@ 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/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class HiddenSettings extends StatelessWidget { @@ -246,24 +247,23 @@ class HiddenSettings extends StatelessWidget { } }, ), + const SizedBox( + height: 12, + ), Consumer( builder: (_, ref, __) { return GestureDetector( onTap: () async { - ref - .read(prefsChangeNotifierProvider) - .frostEnabled = - !(ref - .read(prefsChangeNotifierProvider) - .frostEnabled); - if (kDebugMode) { - print( - "FROST enabled: ${ref.read(prefsChangeNotifierProvider).frostEnabled}"); - } + await showDialog( + context: context, + builder: (_) => TorWarningDialog( + coin: Coin.stellar, + ), + ); }, child: RoundedWhiteContainer( child: Text( - "Toggle FROST multisig", + "Show Tor warning popup", style: STextStyles.button(context).copyWith( color: Theme.of(context) .extension()! From 1328a5eb654b0c582b88ac06103907a8cf0a37b3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 17 Apr 2024 13:43:34 -0500 Subject: [PATCH 116/272] add Flutter Network DevTools documentation (testing Tor connections) --- docs/building.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/building.md b/docs/building.md index 2417de523..13cf5c03f 100644 --- a/docs/building.md +++ b/docs/building.md @@ -198,3 +198,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. From 53e401edb84bf89effa2cc60e3ef1a5b4913a1c8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 17 Apr 2024 20:41:45 -0500 Subject: [PATCH 117/272] mobile Tor warning --- .../sub_widgets/wallet_list_item.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 194b8df10..2099b05e9 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -17,6 +17,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_overview.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/supported_coins.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -26,6 +27,7 @@ import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; +import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletListItem extends ConsumerWidget { @@ -58,6 +60,25 @@ class WalletListItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () async { + // Check if Tor is enabled... + if (ref.read(prefsChangeNotifierProvider).useTor) { + // ... and if the coin supports Tor. + final cryptocurrency = SupportedCoins.coins[coin]; + if (cryptocurrency != null && !cryptocurrency!.torSupport) { + // If not, show a Tor warning dialog. + final shouldContinue = await showDialog( + context: context, + builder: (_) => TorWarningDialog( + coin: coin, + ), + ) ?? + false; + if (!shouldContinue) { + return; + } + } + } + if (walletCount == 1 && coin != Coin.ethereum) { final wallet = ref .read(pWallets) From edc57f69dd6f14b1eab845863741845e61cf5f24 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 17 Apr 2024 21:32:01 -0500 Subject: [PATCH 118/272] typofix sorry --- lib/supported_coins.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/supported_coins.dart b/lib/supported_coins.dart index f9d081b5a..ef28d8916 100644 --- a/lib/supported_coins.dart +++ b/lib/supported_coins.dart @@ -55,7 +55,7 @@ class SupportedCoins { /// A Map linking a CryptoCurrency with its associated Coin. /// - /// Temporary: Remove when the Coin enum is removed.dd + /// Temporary: Remove when the Coin enum is removed. static final Map coins = { Coin.bitcoin: Bitcoin(CryptoCurrencyNetwork.main), Coin.monero: Monero(CryptoCurrencyNetwork.main), From 9f5a5901f6c3172087c63f0625b2a29c25877a08 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 18 Apr 2024 11:33:23 -0600 Subject: [PATCH 119/272] More bitcoindart -> coinlib migration --- lib/models/signing_data.dart | 37 +--- lib/utilities/address_utils.dart | 30 ---- lib/utilities/bip32_utils.dart | 23 +-- lib/wallets/wallet/impl/particl_wallet.dart | 109 ++++++++++-- .../bcash_interface.dart | 14 +- .../electrumx_interface.dart | 137 ++------------- .../lelantus_interface.dart | 83 +++++++-- .../paynym_interface.dart | 163 ++++++++++++++---- .../spark_interface.dart | 127 +++++++++++++- 9 files changed, 450 insertions(+), 273 deletions(-) diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart index f510f7845..24dac4546 100644 --- a/lib/models/signing_data.dart +++ b/lib/models/signing_data.dart @@ -8,10 +8,7 @@ * */ -import 'dart:typed_data'; - -import 'package:bitcoindart/bitcoindart.dart'; -// import 'package:coinlib_flutter/coinlib_flutter.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'; @@ -19,49 +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" "}"; } } - -// class SigningData { -// SigningData({ -// required this.derivePathType, -// required this.utxo, -// this.keyPair, -// this.redeemScript, -// }); -// -// final DerivePathType derivePathType; -// final UTXO utxo; -// ({ECPrivateKey privateKey, ECPublicKey publicKey})? keyPair; -// Uint8List? redeemScript; -// -// @override -// String toString() { -// return "SigningData{\n" -// " derivePathType: $derivePathType,\n" -// " utxo: $utxo,\n" -// " keyPair: $keyPair,\n" -// " redeemScript: $redeemScript,\n" -// "}"; -// } -// } diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 563ca69fd..672da9059 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -10,8 +10,6 @@ import 'dart:convert'; -import 'package:bitcoindart/bitcoindart.dart'; -import 'package:crypto/crypto.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart'; @@ -38,34 +36,6 @@ class AddressUtils { return '${address.substring(0, 5)}...${address.substring(address.length - 5)}'; } - /// attempts to convert a string to a valid scripthash - /// - /// Returns the scripthash or throws an exception on invalid firo address - static String convertToScriptHash( - String address, - NetworkType network, [ - String overridePrefix = "", - ]) { - try { - final output = - Address.addressToOutputScript(address, network, overridePrefix); - final hash = sha256.convert(output.toList(growable: false)).toString(); - - final chars = hash.split(""); - final reversedPairs = []; - // TODO find a better/faster way to do this? - var i = chars.length - 1; - while (i > 0) { - reversedPairs.add(chars[i - 1]); - reversedPairs.add(chars[i]); - i -= 2; - } - return reversedPairs.join(""); - } catch (e) { - rethrow; - } - } - static bool validateAddress(String address, Coin coin) { //This calls the validate address for each crypto coin, validateAddress is //only used in 2 places, so I just replaced the old functionality here diff --git a/lib/utilities/bip32_utils.dart b/lib/utilities/bip32_utils.dart index dcfdc329a..4129681a3 100644 --- a/lib/utilities/bip32_utils.dart +++ b/lib/utilities/bip32_utils.dart @@ -10,7 +10,6 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; -import 'package:bitcoindart/bitcoindart.dart'; import 'package:flutter/foundation.dart'; import 'package:tuple/tuple.dart'; @@ -19,25 +18,17 @@ abstract class Bip32Utils { static bip32.BIP32 getBip32RootSync( String mnemonic, String mnemonicPassphrase, - NetworkType networkType, + bip32.NetworkType networkType, ) { final seed = bip39.mnemonicToSeed(mnemonic, passphrase: mnemonicPassphrase); - final _networkType = bip32.NetworkType( - wif: networkType.wif, - bip32: bip32.Bip32Type( - public: networkType.bip32.public, - private: networkType.bip32.private, - ), - ); - - final root = bip32.BIP32.fromSeed(seed, _networkType); + final root = bip32.BIP32.fromSeed(seed, networkType); return root; } static Future getBip32Root( String mnemonic, String mnemonicPassphrase, - NetworkType networkType, + bip32.NetworkType networkType, ) async { final root = await compute( _getBip32RootWrapper, @@ -52,7 +43,7 @@ abstract class Bip32Utils { /// wrapper for compute() static bip32.BIP32 _getBip32RootWrapper( - Tuple3 args, + Tuple3 args, ) { return getBip32RootSync( args.item1, @@ -97,7 +88,7 @@ abstract class Bip32Utils { static bip32.BIP32 getBip32NodeSync( String mnemonic, String mnemonicPassphrase, - NetworkType network, + bip32.NetworkType network, String derivePath, ) { final root = getBip32RootSync(mnemonic, mnemonicPassphrase, network); @@ -109,7 +100,7 @@ abstract class Bip32Utils { static Future getBip32Node( String mnemonic, String mnemonicPassphrase, - NetworkType networkType, + bip32.NetworkType networkType, String derivePath, ) async { final node = await compute( @@ -126,7 +117,7 @@ abstract class Bip32Utils { /// wrapper for compute() static bip32.BIP32 _getBip32NodeWrapper( - Tuple4 args, + Tuple4 args, ) { return getBip32NodeSync( args.item1, diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index e0d5bc9c2..8c4a12a28 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; @@ -7,6 +9,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/extensions/impl/uint8_list.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; @@ -340,20 +343,92 @@ class ParticlWallet extends Bip39HDWallet Logging.instance.log("Starting Particl buildTransaction ----------", level: LogLevel.Info); - // TODO: use coinlib + // TODO: use coinlib (For this we need coinlib to support particl) + + final convertedNetwork = bitcoindart.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: bitcoindart.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); + + final List<({Uint8List? output, Uint8List? redeem})> extraData = []; + for (int i = 0; i < utxoSigningData.length; i++) { + final sd = utxoSigningData[i]; + + final pubKey = sd.keyPair!.publicKey.data; + final bitcoindart.PaymentData? data; + Uint8List? redeem, output; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = bitcoindart + .P2PKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip49: + final p2wpkh = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + redeem = p2wpkh.output; + data = bitcoindart + .P2SH( + data: bitcoindart.PaymentData(redeem: p2wpkh), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip84: + // input = coinlib.P2WPKHInput( + // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), + // publicKey: keys.publicKey, + // ); + data = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip86: + data = null; + break; + + default: + throw Exception("DerivePathType unsupported"); + } + + // sd.output = input.script!.compiled; + + if (sd.derivePathType != DerivePathType.bip86) { + output = data!.output!; + } + + extraData.add((output: output, redeem: redeem)); + } final txb = bitcoindart.TransactionBuilder( - network: bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ), + network: convertedNetwork, ); const version = 160; // buildTransaction overridden for Particl to set this. // TODO: [prio=low] refactor overridden buildTransaction to use eg. cryptocurrency.networkParams.txVersion. @@ -370,7 +445,7 @@ class ParticlWallet extends Bip39HDWallet txid, utxoSigningData[i].utxo.vout, null, - utxoSigningData[i].output!, + extraData[i].output!, cryptoCurrency.networkParams.bech32Hrp, ); @@ -427,9 +502,13 @@ class ParticlWallet extends Bip39HDWallet for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[i].keyPair!, + keyPair: bitcoindart.ECPair.fromPrivateKey( + utxoSigningData[i].keyPair!.privateKey.data, + network: convertedNetwork, + compressed: utxoSigningData[i].keyPair!.privateKey.compressed, + ), witnessValue: utxoSigningData[i].utxo.value, - redeemScript: utxoSigningData[i].redeemScript, + redeemScript: extraData[i].redeem, overridePrefix: cryptoCurrency.networkParams.bech32Hrp, ); } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart index e9a3ffab3..c94fd15f9 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart @@ -1,4 +1,5 @@ import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitbox/src/utils/network.dart' as bitbox_utils; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; @@ -89,8 +90,17 @@ mixin BCashInterface on Bip39HDWallet, ElectrumXInterface { try { // Sign the transaction accordingly for (int i = 0; i < utxoSigningData.length; i++) { - final bitboxEC = bitbox.ECPair.fromWIF( - utxoSigningData[i].keyPair!.toWIF(), + final bitboxEC = bitbox.ECPair.fromPrivateKey( + utxoSigningData[i].keyPair!.privateKey.data, + network: bitbox_utils.Network( + cryptoCurrency.networkParams.privHDPrefix, + cryptoCurrency.networkParams.pubHDPrefix, + cryptoCurrency.network == CryptoCurrencyNetwork.test, + cryptoCurrency.networkParams.p2pkhPrefix, + cryptoCurrency.networkParams.wifPrefix, + cryptoCurrency.networkParams.p2pkhPrefix, + ), + compressed: utxoSigningData[i].keyPair!.privateKey.compressed, ); builder.sign( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index ff8d7e4da..df8f8deaf 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; -import 'package:bip47/src/util.dart'; -import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; import 'package:electrum_adapter/electrum_adapter.dart'; @@ -22,6 +20,7 @@ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/paynym_is_api.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -545,18 +544,6 @@ mixin ElectrumXInterface on Bip39HDWallet { ); } - final convertedNetwork = bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ); - final root = await getRootHDNode(); for (final sd in signingData) { @@ -597,80 +584,7 @@ mixin ElectrumXInterface on Bip39HDWallet { "Failed to fetch signing data. Local db corrupt. Rescan wallet."); } - // final coinlib.Input input; - - final pubKey = keys.publicKey.data; - final bitcoindart.PaymentData? data; - - switch (sd.derivePathType) { - case DerivePathType.bip44: - // input = coinlib.P2PKHInput( - // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), - // publicKey: keys.publicKey, - // ); - - data = bitcoindart - .P2PKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; - break; - - case DerivePathType.bip49: - final p2wpkh = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; - sd.redeemScript = p2wpkh.output; - data = bitcoindart - .P2SH( - data: bitcoindart.PaymentData(redeem: p2wpkh), - network: convertedNetwork, - ) - .data; - break; - - case DerivePathType.bip84: - // input = coinlib.P2WPKHInput( - // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), - // publicKey: keys.publicKey, - // ); - data = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; - break; - - case DerivePathType.bip86: - data = null; - break; - - default: - throw Exception("DerivePathType unsupported"); - } - - // sd.output = input.script!.compiled; - - if (sd.derivePathType != DerivePathType.bip86) { - sd.output = data!.output!; - } - - sd.keyPair = bitcoindart.ECPair.fromPrivateKey( - keys.privateKey.data, - compressed: keys.privateKey.compressed, - network: convertedNetwork, - ); + sd.keyPair = keys; } return signingData; @@ -705,7 +619,9 @@ mixin ElectrumXInterface on Bip39HDWallet { for (var i = 0; i < utxoSigningData.length; i++) { final txid = utxoSigningData[i].utxo.txid; - final hash = Uint8List.fromList(txid.fromHex.reversed.toList()); + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), + ); final prevOutpoint = coinlib.OutPoint( hash, @@ -729,30 +645,25 @@ mixin ElectrumXInterface on Bip39HDWallet { case DerivePathType.bch44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - // publicKey: utxoSigningData[i].keyPair!.publicKey, - publicKey: coinlib.ECPublicKey( - utxoSigningData[i].keyPair!.publicKey, - ), + publicKey: utxoSigningData[i].keyPair!.publicKey, sequence: 0xffffffff - 1, ); // TODO: fix this as it is (probably) wrong! case DerivePathType.bip49: - input = coinlib.P2SHMultisigInput( - prevOut: prevOutpoint, - program: coinlib.MultisigProgram.decompile( - utxoSigningData[i].redeemScript!, - ), - sequence: 0xffffffff - 1, - ); + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // utxoSigningData[i].redeemScript!, + // ), + // sequence: 0xffffffff - 1, + // ); case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - // publicKey: utxoSigningData[i].keyPair!.publicKey, - publicKey: coinlib.ECPublicKey( - utxoSigningData[i].keyPair!.publicKey, - ), + publicKey: utxoSigningData[i].keyPair!.publicKey, sequence: 0xffffffff - 1, ); @@ -825,16 +736,11 @@ mixin ElectrumXInterface on Bip39HDWallet { // Sign the transaction accordingly for (var i = 0; i < utxoSigningData.length; i++) { final value = BigInt.from(utxoSigningData[i].utxo.value); - coinlib.ECPrivateKey key = coinlib.ECPrivateKey( - utxoSigningData[i].keyPair!.privateKey!, - compressed: utxoSigningData[i].keyPair!.compressed, - ); + coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: coinlib.ECPublicKey( - utxoSigningData[i].keyPair!.publicKey, - ), + internalKey: utxoSigningData[i].keyPair!.publicKey, ); key = taproot.tweakPrivateKey(key); @@ -843,18 +749,9 @@ mixin ElectrumXInterface on Bip39HDWallet { clTx = clTx.sign( inputN: i, value: value, - // key: utxoSigningData[i].keyPair!.privateKey, key: key, prevOuts: prevOuts, ); - - // txb.sign( - // vin: i, - // keyPair: utxoSigningData[i].keyPair!, - // witnessValue: utxoSigningData[i].utxo.value, - // redeemScript: utxoSigningData[i].redeemScript, - // overridePrefix: cryptoCurrency.networkParams.bech32Hrp, - // ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart index 9e667c2b0..ce8c5064d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart @@ -742,18 +742,20 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface { Future buildMintTransaction({required TxData txData}) async { final signingData = await fetchBuildTxData(txData.utxos!.toList()); - final txb = bitcoindart.TransactionBuilder( - network: bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, + final convertedNetwork = bitcoindart.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: bitcoindart.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); + + final txb = bitcoindart.TransactionBuilder( + network: convertedNetwork, ); txb.setVersion(2); @@ -763,11 +765,62 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface { int amount = 0; // Add transaction inputs for (var i = 0; i < signingData.length; i++) { + final pubKey = signingData[i].keyPair!.publicKey.data; + final bitcoindart.PaymentData? data; + + switch (signingData[i].derivePathType) { + case DerivePathType.bip44: + data = bitcoindart + .P2PKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip49: + final p2wpkh = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + data = bitcoindart + .P2SH( + data: bitcoindart.PaymentData(redeem: p2wpkh), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip84: + data = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip86: + data = null; + break; + + default: + throw Exception("DerivePathType unsupported"); + } + txb.addInput( signingData[i].utxo.txid, signingData[i].utxo.vout, null, - signingData[i].output, + data!.output!, ); amount += signingData[i].utxo.value; } @@ -782,7 +835,11 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface { for (var i = 0; i < signingData.length; i++) { txb.sign( vin: i, - keyPair: signingData[i].keyPair!, + keyPair: bitcoindart.ECPair.fromPrivateKey( + signingData[i].keyPair!.privateKey.data, + network: convertedNetwork, + compressed: signingData[i].keyPair!.privateKey.compressed, + ), witnessValue: signingData[i].utxo.value, ); } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index a2700fff6..c17965584 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -7,6 +7,7 @@ import 'package:bip47/bip47.dart'; import 'package:bitcoindart/bitcoindart.dart' as btc_dart; import 'package:bitcoindart/src/utils/constants/op.dart' as op; import 'package:bitcoindart/src/utils/script.dart' as bscript; +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; import 'package:pointycastle/digests/sha256.dart'; import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart'; @@ -20,6 +21,7 @@ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/bip47_utils.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -290,7 +292,13 @@ mixin PaynymInterface return _cachedRootNode ??= await Bip32Utils.getBip32Root( (await getMnemonic()), (await getMnemonicPassphrase()), - networkType, + bip32.NetworkType( + wif: networkType.wif, + bip32: bip32.Bip32Type( + public: networkType.bip32.public, + private: networkType.bip32.private, + ), + ), ); } @@ -701,7 +709,7 @@ mixin PaynymInterface final myKeyPair = utxoSigningData.first.keyPair!; final S = SecretPoint( - myKeyPair.privateKey!, + myKeyPair.privateKey.data, targetPaymentCode.notificationPublicKey(), ); @@ -719,63 +727,146 @@ mixin PaynymInterface ]); // build a notification tx - final txb = btc_dart.TransactionBuilder(network: networkType); - txb.setVersion(1); - txb.addInput( - utxo.txid, - txPointIndex, - null, - utxoSigningData.first.output!, + final List prevOuts = []; + + coinlib.Transaction clTx = coinlib.Transaction( + version: 1, + inputs: [], + outputs: [], ); - // add rest of possible inputs - for (var i = 1; i < utxoSigningData.length; i++) { - final utxo = utxoSigningData[i].utxo; - txb.addInput( - utxo.txid, - utxo.vout, - null, - utxoSigningData[i].output!, + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), ); + + final prevOutpoint = coinlib.OutPoint( + hash, + utxoSigningData[i].utxo.vout, + ); + + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(utxoSigningData[i].utxo.value), + coinlib.Address.fromString( + utxoSigningData[i].utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + + final coinlib.Input input; + + switch (utxoSigningData[i].derivePathType) { + case DerivePathType.bip44: + case DerivePathType.bch44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: 0xffffffff - 1, + ); + + // TODO: fix this as it is (probably) wrong! (unlikely used in paynyms) + case DerivePathType.bip49: + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // utxoSigningData[i].redeemScript!, + // ), + // sequence: 0xffffffff - 1, + // ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: 0xffffffff - 1, + ); + + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + + default: + throw UnsupportedError( + "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + ); + } + + clTx = clTx.addInput(input); } + final String notificationAddress = targetPaymentCode.notificationAddressP2PKH(); - txb.addOutput( - notificationAddress, - (overrideAmountForTesting ?? cryptoCurrency.dustLimitP2PKH.raw).toInt(), + final address = coinlib.Address.fromString( + normalizeAddress(notificationAddress), + cryptoCurrency.networkParams, + ); + + final output = coinlib.Output.fromAddress( + overrideAmountForTesting ?? cryptoCurrency.dustLimitP2PKH.raw, + address, + ); + + clTx = clTx.addOutput(output); + + clTx = clTx.addOutput( + coinlib.Output.fromScriptBytes( + BigInt.zero, + opReturnScript, + ), ); - txb.addOutput(opReturnScript, 0); // TODO: add possible change output and mark output as dangerous if (change > BigInt.zero) { // generate new change address if current change address has been used await checkChangeAddressForTransactions(); final String changeAddress = (await getCurrentChangeAddress())!.value; - txb.addOutput(changeAddress, change.toInt()); + + final output = coinlib.Output.fromAddress( + change, + coinlib.Address.fromString( + normalizeAddress(changeAddress), + cryptoCurrency.networkParams, + ), + ); + + clTx = clTx.addOutput(output); } - txb.sign( - vin: 0, - keyPair: myKeyPair, - witnessValue: utxo.value, - witnessScript: utxoSigningData.first.redeemScript, + clTx = clTx.sign( + inputN: 0, + value: BigInt.from(utxo.value), + key: myKeyPair.privateKey, + prevOuts: prevOuts, ); // sign rest of possible inputs - for (var i = 1; i < utxoSigningData.length; i++) { - txb.sign( - vin: i, - keyPair: utxoSigningData[i].keyPair!, - witnessValue: utxoSigningData[i].utxo.value, - witnessScript: utxoSigningData[i].redeemScript, + for (int i = 1; i < utxoSigningData.length; i++) { + final value = BigInt.from(utxoSigningData[i].utxo.value); + coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; + + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot( + internalKey: utxoSigningData[i].keyPair!.publicKey, + ); + + key = taproot.tweakPrivateKey(key); + } + + clTx = clTx.sign( + inputN: i, + value: value, + key: key, + prevOuts: prevOuts, ); } - final builtTx = txb.build(); - - return Tuple2(builtTx.toHex(), builtTx.virtualSize()); + return Tuple2(clTx.toHex(), clTx.size); } catch (e, s) { Logging.instance.log( "_createNotificationTx(): $e\n$s", diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 74848182e..83e064dc9 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2 import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; @@ -1001,13 +1002,64 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (final sd in setCoins) { vin.add(sd); + final pubKey = sd.keyPair!.publicKey.data; + final btc.PaymentData? data; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = btc + .P2PKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip49: + final p2wpkh = btc + .P2WPKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + data = btc + .P2SH( + data: btc.PaymentData(redeem: p2wpkh), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip84: + data = btc + .P2WPKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip86: + data = null; + break; + + default: + throw Exception("DerivePathType unsupported"); + } + // add to dummy tx dummyTxb.addInput( sd.utxo.txid, sd.utxo.vout, 0xffffffff - 1, // minus 1 is important. 0xffffffff on its own will burn funds - sd.output, + data!.output!, ); } @@ -1015,9 +1067,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (var i = 0; i < setCoins.length; i++) { dummyTxb.sign( vin: i, - keyPair: setCoins[i].keyPair!, + keyPair: btc.ECPair.fromPrivateKey( + setCoins[i].keyPair!.privateKey.data, + network: _bitcoinDartNetwork, + compressed: setCoins[i].keyPair!.privateKey.compressed, + ), witnessValue: setCoins[i].utxo.value, - redeemScript: setCoins[i].redeemScript, + + // maybe not needed here as this was originally copied from btc? We'll find out... + // redeemScript: setCoins[i].redeemScript, ); } @@ -1114,12 +1172,63 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txb.setVersion(txVersion); txb.setLockTime(lockTime); for (final input in vin) { + final pubKey = input.keyPair!.publicKey.data; + final btc.PaymentData? data; + + switch (input.derivePathType) { + case DerivePathType.bip44: + data = btc + .P2PKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip49: + final p2wpkh = btc + .P2WPKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + data = btc + .P2SH( + data: btc.PaymentData(redeem: p2wpkh), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip84: + data = btc + .P2WPKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip86: + data = null; + break; + + default: + throw Exception("DerivePathType unsupported"); + } + txb.addInput( input.utxo.txid, input.utxo.vout, 0xffffffff - 1, // minus 1 is important. 0xffffffff on its own will burn funds - input.output, + data!.output!, ); tempInputs.add( @@ -1172,9 +1281,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (var i = 0; i < vin.length; i++) { txb.sign( vin: i, - keyPair: vin[i].keyPair!, + keyPair: btc.ECPair.fromPrivateKey( + vin[i].keyPair!.privateKey.data, + network: _bitcoinDartNetwork, + compressed: vin[i].keyPair!.privateKey.compressed, + ), witnessValue: vin[i].utxo.value, - redeemScript: vin[i].redeemScript, + + // maybe not needed here as this was originally copied from btc? We'll find out... + // redeemScript: setCoins[i].redeemScript, ); } } catch (e, s) { From 8d56358ebc8b6d320609e72c49479b4c3a289157 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 18 Apr 2024 15:29:36 -0500 Subject: [PATCH 120/272] validate remainder seed word input(s) --- .../restore_wallet_view.dart | 874 +++++++++--------- 1 file changed, 426 insertions(+), 448 deletions(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index e9f0442d5..14174b22b 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -56,7 +56,6 @@ import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart'; import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; -import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; @@ -724,480 +723,459 @@ class _RestoreWalletViewState extends ConsumerState { ], ), body: Container( - color: Theme.of(context).extension()!.background, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: SingleChildScrollView( - controller: controller, - child: Column( - children: [ - /*if (isDesktop) + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SingleChildScrollView( + controller: controller, + child: Column( + children: [ + /*if (isDesktop) const Spacer( flex: 10, ),*/ - if (!isDesktop) - Text( - widget.walletName, - style: STextStyles.itemSubtitle(context), - ), - SizedBox( - height: isDesktop ? 0 : 4, - ), + if (!isDesktop) Text( - "Recovery phrase", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), + widget.walletName, + style: STextStyles.itemSubtitle(context), ), - SizedBox( - height: isDesktop ? 16 : 8, - ), - Text( - "Enter your $_seedWordCount-word recovery phrase.", - style: isDesktop - ? STextStyles.desktopSubtitleH2(context) - : STextStyles.subtitle(context), - ), - SizedBox( - height: isDesktop ? 16 : 10, - ), - if (isDesktop) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: pasteMnemonic, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.clipboard, - width: 22, - height: 22, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - const SizedBox( - width: 8, - ), - Text( - "Paste", - style: STextStyles - .desktopButtonSmallSecondaryEnabled( - context), - ) - ], - ), + SizedBox( + height: isDesktop ? 0 : 4, + ), + Text( + "Recovery phrase", + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + Text( + "Enter your $_seedWordCount-word recovery phrase.", + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + SizedBox( + height: isDesktop ? 16 : 10, + ), + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: pasteMnemonic, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12, ), - ), - ], - ), - if (isDesktop) - const SizedBox( - height: 20, - ), - if (isDesktop) - ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 1008, - ), - child: Builder( - builder: (BuildContext context) { - const cols = 4; - final int rows = _seedWordCount ~/ cols; - final int remainder = _seedWordCount % cols; - - return Column( + child: Row( children: [ - Form( - key: _formKey, - child: TableView( - shrinkWrap: true, - rowSpacing: 20, - rows: [ - for (int i = 0; i < rows; i++) - TableViewRow( - crossAxisAlignment: - CrossAxisAlignment.start, - spacing: 16, - cells: [ - for (int j = 1; j <= cols; j++) - TableViewCell( - flex: 1, - child: Column( - children: [ - TextFormField( - autocorrect: !isDesktop, - enableSuggestions: - !isDesktop, - textCapitalization: - TextCapitalization.none, - key: Key( - "restoreMnemonicFormField_$i"), - decoration: - _getInputDecorationFor( - _inputStatuses[ - i * 4 + j - 1], - "${i * 4 + j}"), - autovalidateMode: - AutovalidateMode - .onUserInteraction, - selectionControls: i * 4 + - j - - 1 == - 1 - ? textSelectionControls - : null, - // focusNode: - // _focusNodes[i * 4 + j - 1], - onChanged: (value) { - final FormInputStatus - formInputStatus; - - if (value.isEmpty) { - formInputStatus = - FormInputStatus - .empty; - } else if (_isValidMnemonicWord( - value - .trim() - .toLowerCase())) { - formInputStatus = - FormInputStatus - .valid; - } else { - formInputStatus = - FormInputStatus - .invalid; - } - - // if (formInputStatus == - // FormInputStatus.valid) { - // if (i * 4 + j < - // _focusNodes.length) { - // _focusNodes[i * 4 + j] - // .requestFocus(); - // } else if (i * 4 + j == - // _focusNodes.length) { - // _focusNodes[i * 4 + j - 1] - // .unfocus(); - // } - // } - setState(() { - _inputStatuses[ - i * 4 + j - 1] = - formInputStatus; - }); - }, - controller: _controllers[ - i * 4 + j - 1], - style: STextStyles.field( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textRestore, - fontSize: - isDesktop ? 16 : 14, - ), - ), - if (_inputStatuses[ - i * 4 + j - 1] == - FormInputStatus.invalid) - Align( - alignment: - Alignment.topLeft, - child: Padding( - padding: - const EdgeInsets - .only( - left: 12.0, - bottom: 4.0, - ), - child: Text( - "Please check spelling", - textAlign: - TextAlign.left, - style: - STextStyles.label( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textError, - ), - ), - ), - ) - ], - ), - ), - ], - expandingChild: null, - ), - if (remainder > 0) - TableViewRow( - spacing: 16, - cells: [ - for (int i = rows * cols; - i < _seedWordCount; - i++) ...[ - TableViewCell( - flex: 1, - child: Column( - children: [ - TextFormField( - autocorrect: !isDesktop, - enableSuggestions: - !isDesktop, - textCapitalization: - TextCapitalization.none, - key: Key( - "restoreMnemonicFormField_$i"), - decoration: - _getInputDecorationFor( - _inputStatuses[i], - "${i + 1}"), - autovalidateMode: - AutovalidateMode - .onUserInteraction, - selectionControls: i == 1 - ? textSelectionControls - : null, - // focusNode: _focusNodes[i], - onChanged: (value) { - final FormInputStatus - formInputStatus; - - if (value.isEmpty) { - formInputStatus = - FormInputStatus - .empty; - } else if (_isValidMnemonicWord( - value - .trim() - .toLowerCase())) { - formInputStatus = - FormInputStatus - .valid; - } else { - formInputStatus = - FormInputStatus - .invalid; - } - - // if (formInputStatus == - // FormInputStatus - // .valid && - // (i - 1) < - // _focusNodes.length) { - // Focus.of(context) - // .requestFocus( - // _focusNodes[i]); - // } - - // if (formInputStatus == - // FormInputStatus.valid) { - // if (i + 1 < - // _focusNodes.length) { - // _focusNodes[i + 1] - // .requestFocus(); - // } else if (i + 1 == - // _focusNodes.length) { - // _focusNodes[i].unfocus(); - // } - // } - }, - controller: _controllers[i], - style: STextStyles.field( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .overlay, - fontSize: - isDesktop ? 16 : 14, - ), - ), - if (_inputStatuses[i] == - FormInputStatus.invalid) - Align( - alignment: - Alignment.topLeft, - child: Padding( - padding: - const EdgeInsets - .only( - left: 12.0, - bottom: 4.0, - ), - child: Text( - "Please check spelling", - textAlign: - TextAlign.left, - style: - STextStyles.label( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textError, - ), - ), - ), - ) - ], - ), - ), - ], - for (int i = remainder; - i < cols; - i++) ...[ - TableViewCell( - flex: 1, - child: Container(), - ), - ], - ], - expandingChild: null, - ), - ], - ), + SvgPicture.asset( + Assets.svg.clipboard, + width: 22, + height: 22, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, ), const SizedBox( - height: 32, - ), - PrimaryButton( - label: "Restore wallet", - width: 480, - onPressed: requestRestore, + width: 8, ), + Text( + "Paste", + style: STextStyles + .desktopButtonSmallSecondaryEnabled( + context), + ) ], - ); - }, + ), + ), ), + ], + ), + if (isDesktop) + const SizedBox( + height: 20, + ), + if (isDesktop) + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1008, ), - /*if (isDesktop) + child: Builder( + builder: (BuildContext context) { + const cols = 4; + final int rows = _seedWordCount ~/ cols; + final int remainder = _seedWordCount % cols; + + return Column( + children: [ + Form( + key: _formKey, + child: TableView( + shrinkWrap: true, + rowSpacing: 20, + rows: [ + for (int i = 0; i < rows; i++) + TableViewRow( + crossAxisAlignment: + CrossAxisAlignment.start, + spacing: 16, + cells: [ + for (int j = 1; j <= cols; j++) + TableViewCell( + flex: 1, + child: Column( + children: [ + TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + textCapitalization: + TextCapitalization.none, + key: Key( + "restoreMnemonicFormField_$i"), + decoration: + _getInputDecorationFor( + _inputStatuses[ + i * 4 + j - 1], + "${i * 4 + j}"), + autovalidateMode: + AutovalidateMode + .onUserInteraction, + selectionControls: i * 4 + + j - + 1 == + 1 + ? textSelectionControls + : null, + // focusNode: + // _focusNodes[i * 4 + j - 1], + onChanged: (value) { + final FormInputStatus + formInputStatus; + + if (value.isEmpty) { + formInputStatus = + FormInputStatus.empty; + } else if (_isValidMnemonicWord( + value + .trim() + .toLowerCase())) { + formInputStatus = + FormInputStatus.valid; + } else { + formInputStatus = + FormInputStatus + .invalid; + } + + // if (formInputStatus == + // FormInputStatus.valid) { + // if (i * 4 + j < + // _focusNodes.length) { + // _focusNodes[i * 4 + j] + // .requestFocus(); + // } else if (i * 4 + j == + // _focusNodes.length) { + // _focusNodes[i * 4 + j - 1] + // .unfocus(); + // } + // } + setState(() { + _inputStatuses[i * 4 + + j - + 1] = formInputStatus; + }); + }, + controller: _controllers[ + i * 4 + j - 1], + style: + STextStyles.field(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textRestore, + fontSize: + isDesktop ? 16 : 14, + ), + ), + if (_inputStatuses[ + i * 4 + j - 1] == + FormInputStatus.invalid) + Align( + alignment: + Alignment.topLeft, + child: Padding( + padding: + const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: + TextAlign.left, + style: + STextStyles.label( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textError, + ), + ), + ), + ) + ], + ), + ), + ], + expandingChild: null, + ), + if (remainder > 0) + TableViewRow( + spacing: 16, + cells: [ + for (int i = rows * cols; + i < _seedWordCount - remainder; + i++) ...[ + TableViewCell( + flex: 1, + child: Column( + // ... (existing code for input field) + ), + ), + ], + for (int i = _seedWordCount - remainder; + i < _seedWordCount; + i++) ...[ + TableViewCell( + flex: 1, + child: Column( + children: [ + TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + textCapitalization: + TextCapitalization.none, + key: Key( + "restoreMnemonicFormField_$i"), + decoration: + _getInputDecorationFor( + _inputStatuses[i], + "${i + 1}"), + autovalidateMode: + AutovalidateMode + .onUserInteraction, + selectionControls: i == 1 + ? textSelectionControls + : null, + onChanged: (value) { + final FormInputStatus + formInputStatus; + + if (value.isEmpty) { + formInputStatus = + FormInputStatus.empty; + } else if (_isValidMnemonicWord( + value + .trim() + .toLowerCase())) { + formInputStatus = + FormInputStatus.valid; + } else { + formInputStatus = + FormInputStatus + .invalid; + } + + setState(() { + _inputStatuses[i] = + formInputStatus; + }); + }, + controller: _controllers[i], + style: + STextStyles.field(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .overlay, + fontSize: + isDesktop ? 16 : 14, + ), + ), + if (_inputStatuses[i] == + FormInputStatus.invalid) + Align( + alignment: + Alignment.topLeft, + child: Padding( + padding: + const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: + TextAlign.left, + style: + STextStyles.label( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textError, + ), + ), + ), + ) + ], + ), + ), + ], + for (int i = 0; + i < cols - remainder; + i++) ...[ + TableViewCell( + flex: 1, + child: Container(), + ), + ], + ], + expandingChild: null, + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + PrimaryButton( + label: "Restore wallet", + width: 480, + onPressed: requestRestore, + ), + ], + ); + }, + ), + ), + /*if (isDesktop) const Spacer( flex: 15, ),*/ - if (!isDesktop) - Padding( - padding: const EdgeInsets.all(4.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - for (int i = 1; i <= _seedWordCount; i++) - Column( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), - child: TextFormField( - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - textCapitalization: - TextCapitalization.none, - key: Key("restoreMnemonicFormField_$i"), - decoration: _getInputDecorationFor( - _inputStatuses[i - 1], "$i"), - autovalidateMode: - AutovalidateMode.onUserInteraction, - selectionControls: - i == 1 ? textSelectionControls : null, - // focusNode: _focusNodes[i - 1], - onChanged: (value) { - final FormInputStatus formInputStatus; + if (!isDesktop) + Padding( + padding: const EdgeInsets.all(4.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (int i = 1; i <= _seedWordCount; i++) + Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 4), + child: TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + textCapitalization: TextCapitalization.none, + key: Key("restoreMnemonicFormField_$i"), + decoration: _getInputDecorationFor( + _inputStatuses[i - 1], "$i"), + autovalidateMode: + AutovalidateMode.onUserInteraction, + selectionControls: + i == 1 ? textSelectionControls : null, + // focusNode: _focusNodes[i - 1], + onChanged: (value) { + final FormInputStatus formInputStatus; - if (value.isEmpty) { - formInputStatus = - FormInputStatus.empty; - } else if (_isValidMnemonicWord( - value.trim().toLowerCase())) { - formInputStatus = - FormInputStatus.valid; - } else { - formInputStatus = - FormInputStatus.invalid; - } + if (value.isEmpty) { + formInputStatus = FormInputStatus.empty; + } else if (_isValidMnemonicWord( + value.trim().toLowerCase())) { + formInputStatus = FormInputStatus.valid; + } else { + formInputStatus = + FormInputStatus.invalid; + } - // if (formInputStatus == - // FormInputStatus.valid) { - // if (i < _focusNodes.length) { - // _focusNodes[i].requestFocus(); - // } else if (i == _focusNodes.length) { - // _focusNodes[i - 1].unfocus(); - // } - // } - setState(() { - _inputStatuses[i - 1] = - formInputStatus; - }); - }, - controller: _controllers[i - 1], - style: - STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension()! - .textRestore, - fontSize: isDesktop ? 16 : 14, - ), + // if (formInputStatus == + // FormInputStatus.valid) { + // if (i < _focusNodes.length) { + // _focusNodes[i].requestFocus(); + // } else if (i == _focusNodes.length) { + // _focusNodes[i - 1].unfocus(); + // } + // } + setState(() { + _inputStatuses[i - 1] = formInputStatus; + }); + }, + controller: _controllers[i - 1], + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension()! + .textRestore, + fontSize: isDesktop ? 16 : 14, ), ), - if (_inputStatuses[i - 1] == - FormInputStatus.invalid) - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - bottom: 4.0, - ), - child: Text( - "Please check spelling", - textAlign: TextAlign.left, - style: STextStyles.label(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textError, - ), + ), + if (_inputStatuses[i - 1] == + FormInputStatus.invalid) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: TextAlign.left, + style: + STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension()! + .textError, ), ), - ) - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 8.0, - ), - child: PrimaryButton( - onPressed: requestRestore, - label: "Restore", - ), + ), + ) + ], ), - ], - ), + Padding( + padding: const EdgeInsets.only( + top: 8.0, + ), + child: PrimaryButton( + onPressed: requestRestore, + label: "Restore", + ), + ), + ], ), ), - ], - ), + ), + ], ), ), ), - ); + ), + ); } } From 622740a8c070b1ba88b6f473175dc9708ff572cc Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 18 Apr 2024 17:17:45 -0600 Subject: [PATCH 121/272] centralized electrum client management --- lib/db/db_version_migration.dart | 4 +- .../cached_electrumx_client.dart | 66 +-- lib/electrumx_rpc/client_manager.dart | 96 ++++ .../electrumx_chain_height_service.dart | 149 ------- lib/electrumx_rpc/electrumx_client.dart | 196 ++++----- lib/electrumx_rpc/rpc.dart | 413 ------------------ .../add_edit_node_view.dart | 32 +- .../manage_nodes_views/node_details_view.dart | 20 +- lib/services/notifications_service.dart | 2 +- .../electrum_connection_check.dart | 63 +++ .../wallet/impl/bitcoin_frost_wallet.dart | 61 +-- .../electrumx_interface.dart | 86 +--- lib/widgets/node_card.dart | 20 +- lib/widgets/node_options_sheet.dart | 20 +- 14 files changed, 327 insertions(+), 901 deletions(-) create mode 100644 lib/electrumx_rpc/client_manager.dart delete mode 100644 lib/electrumx_rpc/electrumx_chain_height_service.dart delete mode 100644 lib/electrumx_rpc/rpc.dart create mode 100644 lib/utilities/connection_check/electrum_connection_check.dart diff --git a/lib/db/db_version_migration.dart b/lib/db/db_version_migration.dart index e81c01c76..eb75bf5b1 100644 --- a/lib/db/db_version_migration.dart +++ b/lib/db/db_version_migration.dart @@ -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 { diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index b64e2ec7d..3a93bb5d8 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -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 Function() electrumAdapterUpdateCallback; static const minCacheConfirms = 30; - CachedElectrumXClient({ - required this.electrumXClient, - required this.electrumAdapterClient, - required this.electrumAdapterUpdateCallback, - }); + CachedElectrumXClient({required this.electrumXClient}); factory CachedElectrumXClient.from({ required ElectrumXClient electrumXClient, - required ElectrumClient electrumAdapterClient, - required Future Function() electrumAdapterUpdateCallback, }) => CachedElectrumXClient( electrumXClient: electrumXClient, - electrumAdapterClient: electrumAdapterClient, - electrumAdapterUpdateCallback: electrumAdapterUpdateCallback, ); - /// If the client is closed, use the callback to update it. - _checkElectrumAdapterClient() async { - if (electrumAdapterClient.peer.isClosed) { - Logging.instance.log( - "ElectrumAdapterClient is closed, reopening it...", - level: LogLevel.Info, - ); - ElectrumClient? _electrumAdapterClient = - await electrumAdapterUpdateCallback.call(); - electrumAdapterClient = _electrumAdapterClient; - } - } - Future> getAnonymitySet({ required String groupId, String blockhash = "", @@ -80,12 +54,9 @@ class CachedElectrumXClient { set = Map.from(cachedSet); } - await _checkElectrumAdapterClient(); - - final newSet = await (electrumAdapterClient as FiroElectrumClient) - .getLelantusAnonymitySet( + final newSet = await electrumXClient.getLelantusAnonymitySet( groupId: groupId, - blockHash: set["blockHash"] as String, + blockhash: set["blockHash"] as String, ); // update set with new data @@ -157,10 +128,7 @@ class CachedElectrumXClient { set = Map.from(cachedSet); } - await _checkElectrumAdapterClient(); - - final newSet = await (electrumAdapterClient as FiroElectrumClient) - .getSparkAnonymitySet( + final newSet = await electrumXClient.getSparkAnonymitySet( coinGroupId: groupId, startBlockHash: set["blockHash"] as String, ); @@ -218,10 +186,11 @@ class CachedElectrumXClient { final cachedTx = box.get(txHash) as Map?; if (cachedTx == null) { - await _checkElectrumAdapterClient(); - final Map result = - await electrumAdapterClient.getTransaction(txHash); + await electrumXClient.getTransaction( + txHash: txHash, + verbose: verbose, + ); result.remove("hex"); result.remove("lelantusData"); @@ -263,10 +232,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 +280,12 @@ class CachedElectrumXClient { cachedTags.length - 100, // 100 being some arbitrary buffer ); - await _checkElectrumAdapterClient(); - - final tags = - await (electrumAdapterClient as FiroElectrumClient).getUsedCoinsTags( + final newTags = await electrumXClient.getSparkUsedCoinsTags( startNumber: startNumber, ); - // final newSerials = List.from(serials["serials"] as List) - // .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e) - // .toSet(); - - // Convert the Map tags to a Set. - final newTags = (tags["tags"] as List).toSet(); - // ensure we are getting some overlap so we know we are not missing any - if (cachedTags.isNotEmpty && tags.isNotEmpty) { + if (cachedTags.isNotEmpty && newTags.isNotEmpty) { assert(cachedTags.intersection(newTags).isNotEmpty); } diff --git a/lib/electrumx_rpc/client_manager.dart b/lib/electrumx_rpc/client_manager.dart new file mode 100644 index 000000000..8e580ec78 --- /dev/null +++ b/lib/electrumx_rpc/client_manager.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import 'package:electrum_adapter/electrum_adapter.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +class ClientManager { + ClientManager._(); + static final ClientManager sharedInstance = ClientManager._(); + + final Map _map = {}; + final Map _heights = {}; + final Map> _subscriptions = {}; + final Map> _heightCompleters = {}; + + String _keyHelper(CryptoCurrency cryptoCurrency) { + return "${cryptoCurrency.runtimeType}_${cryptoCurrency.network.name}"; + } + + final Finalizer _finalizer = Finalizer((manager) async { + await manager._kill(); + }); + + ElectrumClient? getClient({ + required CryptoCurrency cryptoCurrency, + }) => + _map[_keyHelper(cryptoCurrency)]; + + void addClient( + ElectrumClient client, { + required CryptoCurrency cryptoCurrency, + }) { + final key = _keyHelper(cryptoCurrency); + if (_map[key] != null) { + throw Exception("ElectrumX Client for $key already exists."); + } else { + _map[key] = client; + } + + _heightCompleters[key] = Completer(); + _subscriptions[key] = client.subscribeHeaders().listen((event) { + _heights[key] = event.height; + + if (!_heightCompleters[key]!.isCompleted) { + _heightCompleters[key]!.complete(event.height); + } + }); + } + + Future getChainHeightFor(CryptoCurrency cryptoCurrency) async { + final key = _keyHelper(cryptoCurrency); + + if (_map[key] == null) { + throw Exception( + "No managed ElectrumClient for $cryptoCurrency found.", + ); + } + if (_heightCompleters[key] == null) { + throw Exception( + "No managed _heightCompleters for $cryptoCurrency found.", + ); + } + + return _heights[key] ?? await _heightCompleters[key]!.future; + } + + Future remove({ + required CryptoCurrency cryptoCurrency, + }) async { + final key = _keyHelper(cryptoCurrency); + await _subscriptions[key]?.cancel(); + _subscriptions.remove(key); + _heights.remove(key); + _heightCompleters.remove(key); + + return _map.remove(key); + } + + Future closeAll() async { + await _kill(); + _finalizer.detach(this); + } + + Future _kill() async { + for (final sub in _subscriptions.values) { + await sub.cancel(); + } + for (final client in _map.values) { + await client.close(); + } + + _heightCompleters.clear(); + _heights.clear(); + _subscriptions.clear(); + _map.clear(); + } +} diff --git a/lib/electrumx_rpc/electrumx_chain_height_service.dart b/lib/electrumx_rpc/electrumx_chain_height_service.dart deleted file mode 100644 index 3696e78f9..000000000 --- a/lib/electrumx_rpc/electrumx_chain_height_service.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'dart:async'; - -import 'package:electrum_adapter/electrum_adapter.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/logger.dart'; - -/// Manage chain height subscriptions for each coin. -abstract class ChainHeightServiceManager { - // A map of chain height services for each coin. - static final Map _services = {}; - // Map get services => _services; - - // Get the chain height service for a specific coin. - static ChainHeightService? getService(Coin coin) { - return _services[coin]; - } - - // Add a chain height service for a specific coin. - static void add(ChainHeightService service, Coin coin) { - // Don't add a new service if one already exists. - if (_services[coin] == null) { - _services[coin] = service; - } else { - throw Exception("Chain height service for $coin already managed"); - } - } - - // Remove a chain height service for a specific coin. - static void remove(Coin coin) { - _services.remove(coin); - } - - // Close all subscriptions and clean up resources. - static Future dispose() async { - // Close each subscription. - // - // Create a list of keys to avoid concurrent modification during iteration - var keys = List.from(_services.keys); - - // Iterate over the copy of the keys - for (final coin in keys) { - final ChainHeightService? service = getService(coin); - await service?.cancelListen(); - remove(coin); - } - } -} - -/// A service to fetch and listen for chain height updates. -/// -/// TODO: Add error handling and branching to handle various other scenarios. -class ChainHeightService { - // The electrum_adapter client to use for fetching chain height updates. - ElectrumClient client; - - // The subscription to listen for chain height updates. - StreamSubscription? _subscription; - - // Whether the service has started listening for updates. - bool get started => _subscription != null; - - // The current chain height. - int? _height; - int? get height => _height; - - // Whether the service is currently reconnecting. - bool _isReconnecting = false; - - // The reconnect timer. - Timer? _reconnectTimer; - - // The reconnection timeout duration. - static const Duration _connectionTimeout = Duration(seconds: 10); - - ChainHeightService({required this.client}); - - /// Fetch the current chain height and start listening for updates. - Future fetchHeightAndStartListenForUpdates() async { - // Don't start a new subscription if one already exists. - if (_subscription != null) { - throw Exception( - "Attempted to start a chain height service where an existing" - " subscription already exists!", - ); - } - - // A completer to wait for the current chain height to be fetched. - final completer = Completer(); - - // Fetch the current chain height. - _subscription = client.subscribeHeaders().listen((BlockHeader event) { - _height = event.height; - - if (!completer.isCompleted) { - completer.complete(_height); - } - }); - - _subscription?.onError((dynamic error) { - _handleError(error); - }); - - // Wait for the current chain height to be fetched. - return completer.future; - } - - /// Handle an error from the subscription. - void _handleError(dynamic error) { - Logging.instance.log( - "Error reconnecting for chain height: ${error.toString()}", - level: LogLevel.Error, - ); - - _subscription?.cancel(); - _subscription = null; - _attemptReconnect(); - } - - /// Attempt to reconnect to the electrum server. - void _attemptReconnect() { - // Avoid multiple reconnection attempts. - if (_isReconnecting) return; - _isReconnecting = true; - - // Attempt to reconnect. - unawaited(fetchHeightAndStartListenForUpdates().then((_) { - _isReconnecting = false; - })); - - // Set a timer to on the reconnection attempt and clean up if it fails. - _reconnectTimer?.cancel(); - _reconnectTimer = Timer(_connectionTimeout, () async { - if (_subscription == null) { - await _subscription?.cancel(); - _subscription = null; // Will also occur on an error via handleError. - _reconnectTimer?.cancel(); - _reconnectTimer = null; - _isReconnecting = false; - } - }); - } - - /// Stop listening for chain height updates. - Future cancelListen() async { - await _subscription?.cancel(); - _subscription = null; - _reconnectTimer?.cancel(); - } -} diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 98c6614f9..dd64d64ef 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -20,16 +20,17 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:mutex/mutex.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart'; -import 'package:stackwallet/electrumx_rpc/rpc.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/tor_service.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stream_channel/stream_channel.dart'; class WifiOnlyException implements Exception {} @@ -65,6 +66,8 @@ class ElectrumXNode { } class ElectrumXClient { + final CryptoCurrency cryptoCurrency; + String get host => _host; late String _host; @@ -74,14 +77,13 @@ class ElectrumXClient { bool get useSSL => _useSSL; late bool _useSSL; - JsonRPC? get rpcClient => _rpcClient; - JsonRPC? _rpcClient; - - StreamChannel? get electrumAdapterChannel => _electrumAdapterChannel; + // StreamChannel? get electrumAdapterChannel => _electrumAdapterChannel; StreamChannel? _electrumAdapterChannel; - ElectrumClient? get electrumAdapterClient => _electrumAdapterClient; - ElectrumClient? _electrumAdapterClient; + ElectrumClient? getElectrumAdapter() => + ClientManager.sharedInstance.getClient( + cryptoCurrency: cryptoCurrency, + ); late Prefs _prefs; late TorService _torService; @@ -91,9 +93,6 @@ class ElectrumXClient { final Duration connectionTimeoutForSpecialCaseJsonRPCClients; - Coin? get coin => _coin; - late Coin? _coin; - // add finalizer to cancel stream subscription when all references to an // instance of ElectrumX becomes inaccessible static final Finalizer _finalizer = Finalizer( @@ -114,7 +113,7 @@ class ElectrumXClient { required bool useSSL, required Prefs prefs, required List failovers, - Coin? coin, + required this.cryptoCurrency, this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration(seconds: 60), TorService? torService, @@ -125,7 +124,6 @@ class ElectrumXClient { _host = host; _port = port; _useSSL = useSSL; - _coin = coin; final bus = globalEventBusForTesting ?? GlobalEventBus.instance; @@ -161,10 +159,12 @@ class ElectrumXClient { // setting to null should force the creation of a new json rpc client // on the next request sent through this electrumx instance _electrumAdapterChannel = null; - _electrumAdapterClient = null; + await (await ClientManager.sharedInstance + .remove(cryptoCurrency: cryptoCurrency)) + ?.close(); // Also close any chain height services that are currently open. - await ChainHeightServiceManager.dispose(); + // await ChainHeightServiceManager.dispose(); }, ); } @@ -173,7 +173,7 @@ class ElectrumXClient { required ElectrumXNode node, required Prefs prefs, required List failovers, - required Coin coin, + required CryptoCurrency cryptoCurrency, TorService? torService, EventBus? globalEventBusForTesting, }) { @@ -185,7 +185,7 @@ class ElectrumXClient { torService: torService, failovers: failovers, globalEventBusForTesting: globalEventBusForTesting, - coin: coin, + cryptoCurrency: cryptoCurrency, ); } @@ -197,7 +197,11 @@ class ElectrumXClient { return true; } - Future checkElectrumAdapter() async { + Future closeAdapter() async { + await getElectrumAdapter()?.close(); + } + + Future _checkElectrumAdapter() async { ({InternetAddress host, int port})? proxyInfo; // If we're supposed to use Tor... @@ -223,75 +227,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; + 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 +300,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 +386,16 @@ class ElectrumXClient { if (_requireMutex) { await _torConnectingLock - .protect(() async => await checkElectrumAdapter()); + .protect(() async => await _checkElectrumAdapter()); } else { - await checkElectrumAdapter(); + await _checkElectrumAdapter(); } try { var futures = >[]; - _electrumAdapterClient!.peer.withBatch(() { + 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); @@ -778,8 +767,8 @@ class ElectrumXClient { }) async { Logging.instance.log("attempting to fetch blockchain.transaction.get...", level: LogLevel.Info); - await checkElectrumAdapter(); - dynamic response = await _electrumAdapterClient!.getTransaction(txHash); + await _checkElectrumAdapter(); + dynamic response = await getElectrumAdapter()!.getTransaction(txHash); Logging.instance.log("Fetching blockchain.transaction.get finished", level: LogLevel.Info); @@ -811,9 +800,9 @@ class ElectrumXClient { }) async { Logging.instance.log("attempting to fetch lelantus.getanonymityset...", level: LogLevel.Info); - await checkElectrumAdapter(); + await _checkElectrumAdapter(); Map response = - await (_electrumAdapterClient as FiroElectrumClient)! + await (getElectrumAdapter() as FiroElectrumClient) .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); Logging.instance.log("Fetching lelantus.getanonymityset finished", level: LogLevel.Info); @@ -830,8 +819,8 @@ class ElectrumXClient { }) async { Logging.instance.log("attempting to fetch lelantus.getmintmetadata...", level: LogLevel.Info); - await checkElectrumAdapter(); - dynamic response = await (_electrumAdapterClient as FiroElectrumClient)! + await _checkElectrumAdapter(); + dynamic response = await (getElectrumAdapter() as FiroElectrumClient) .getLelantusMintData(mints: mints); Logging.instance.log("Fetching lelantus.getmintmetadata finished", level: LogLevel.Info); @@ -846,13 +835,13 @@ class ElectrumXClient { }) async { Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...", level: LogLevel.Info); - await checkElectrumAdapter(); + 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", @@ -870,9 +859,9 @@ class ElectrumXClient { Future getLelantusLatestCoinId({String? requestID}) async { Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...", level: LogLevel.Info); - await checkElectrumAdapter(); + await _checkElectrumAdapter(); int response = - await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId(); + await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId(); Logging.instance.log("Fetching lelantus.getlatestcoinid finished", level: LogLevel.Info); return response; @@ -901,9 +890,9 @@ class ElectrumXClient { try { Logging.instance.log("attempting to fetch spark.getsparkanonymityset...", level: LogLevel.Info); - await checkElectrumAdapter(); + await _checkElectrumAdapter(); Map response = - await (_electrumAdapterClient as FiroElectrumClient) + await (getElectrumAdapter() as FiroElectrumClient) .getSparkAnonymitySet( coinGroupId: coinGroupId, startBlockHash: startBlockHash); Logging.instance.log("Fetching spark.getsparkanonymityset finished", @@ -924,11 +913,12 @@ class ElectrumXClient { // Use electrum_adapter package's getSparkUsedCoinsTags method. Logging.instance.log("attempting to fetch spark.getusedcoinstags...", level: LogLevel.Info); - await checkElectrumAdapter(); + await _checkElectrumAdapter(); Map response = - await (_electrumAdapterClient as FiroElectrumClient) + await (getElectrumAdapter() as FiroElectrumClient) .getUsedCoinsTags(startNumber: startNumber); // TODO: Add 2 minute timeout. + // Why 2 minutes? Logging.instance.log("Fetching spark.getusedcoinstags finished", level: LogLevel.Info); final map = Map.from(response); @@ -957,9 +947,9 @@ class ElectrumXClient { try { Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...", level: LogLevel.Info); - await checkElectrumAdapter(); + await _checkElectrumAdapter(); List response = - await (_electrumAdapterClient as FiroElectrumClient) + await (getElectrumAdapter() as FiroElectrumClient) .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes); Logging.instance.log("Fetching spark.getsparkmintmetadata finished", level: LogLevel.Info); @@ -979,8 +969,8 @@ class ElectrumXClient { try { Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...", level: LogLevel.Info); - await checkElectrumAdapter(); - int response = await (_electrumAdapterClient as FiroElectrumClient) + await _checkElectrumAdapter(); + int response = await (getElectrumAdapter() as FiroElectrumClient) .getSparkLatestCoinId(); Logging.instance.log("Fetching spark.getsparklatestcoinid finished", level: LogLevel.Info); @@ -1001,8 +991,8 @@ class ElectrumXClient { /// "rate": 1000, /// } Future> getFeeRate({String? requestID}) async { - await checkElectrumAdapter(); - return await _electrumAdapterClient!.getFeeRate(); + await _checkElectrumAdapter(); + return await getElectrumAdapter()!.getFeeRate(); } /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks]. @@ -1022,7 +1012,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 +1025,7 @@ class ElectrumXClient { return Decimal.parse(response.toString()); } catch (e, s) { final String msg = "Error parsing fee rate. Response: $response" - "\nResult: ${response}\nError: $e\nStack trace: $s"; + "\nResult: $response\nError: $e\nStack trace: $s"; Logging.instance.log(msg, level: LogLevel.Fatal); throw Exception(msg); } diff --git a/lib/electrumx_rpc/rpc.dart b/lib/electrumx_rpc/rpc.dart deleted file mode 100644 index f2044a141..000000000 --- a/lib/electrumx_rpc/rpc.dart +++ /dev/null @@ -1,413 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:mutex/mutex.dart'; -import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; -import 'package:tor_ffi_plugin/socks_socket.dart'; - -// Json RPC class to handle connecting to electrumx servers -class JsonRPC { - JsonRPC({ - required this.host, - required this.port, - this.useSSL = false, - this.connectionTimeout = const Duration(seconds: 60), - required ({InternetAddress host, int port})? proxyInfo, - }); - final bool useSSL; - final String host; - final int port; - final Duration connectionTimeout; - ({InternetAddress host, int port})? proxyInfo; - - final _requestMutex = Mutex(); - final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue(); - Socket? _socket; - SOCKSSocket? _socksSocket; - StreamSubscription>? _subscription; - - void _dataHandler(List data) { - _requestQueue.nextIncompleteReq.then((req) { - if (req != null) { - req.appendDataAndCheckIfComplete(data); - - if (req.isComplete) { - _onReqCompleted(req); - } - } else { - Logging.instance.log( - "_dataHandler found a null req!", - level: LogLevel.Warning, - ); - } - }); - } - - void _errorHandler(Object error, StackTrace trace) { - _requestQueue.nextIncompleteReq.then((req) { - if (req != null) { - req.completer.completeError(error, trace); - _onReqCompleted(req); - } - }); - } - - void _doneHandler() { - disconnect(reason: "JsonRPC _doneHandler() called"); - } - - void _onReqCompleted(_JsonRPCRequest req) { - _requestQueue.remove(req).then((_) { - // attempt to send next request - _sendNextAvailableRequest(); - }); - } - - void _sendNextAvailableRequest() { - _requestQueue.nextIncompleteReq.then((req) { - if (req != null) { - if (!Prefs.instance.useTor) { - if (_socket == null) { - Logging.instance.log( - "JsonRPC _sendNextAvailableRequest attempted with" - " _socket=null on $host:$port", - level: LogLevel.Error, - ); - } - // \r\n required by electrumx server - _socket!.write('${req.jsonRequest}\r\n'); - } else { - if (_socksSocket == null) { - Logging.instance.log( - "JsonRPC _sendNextAvailableRequest attempted with" - " _socksSocket=null on $host:$port", - level: LogLevel.Error, - ); - } - // \r\n required by electrumx server - _socksSocket?.write('${req.jsonRequest}\r\n'); - } - - // TODO different timeout length? - req.initiateTimeout( - onTimedOut: () { - _onReqCompleted(req); - }, - ); - } - }); - } - - Future request( - String jsonRpcRequest, - Duration requestTimeout, - ) async { - await _requestMutex.protect(() async { - if (!Prefs.instance.useTor) { - if (_socket == null) { - Logging.instance.log( - "JsonRPC request: opening socket $host:$port", - level: LogLevel.Info, - ); - await _connect().timeout(requestTimeout, onTimeout: () { - throw Exception("Request timeout: $jsonRpcRequest"); - }); - } - } else { - if (_socksSocket == null) { - Logging.instance.log( - "JsonRPC request: opening SOCKS socket to $host:$port", - level: LogLevel.Info, - ); - await _connect().timeout(requestTimeout, onTimeout: () { - throw Exception("Request timeout: $jsonRpcRequest"); - }); - } - } - }); - - final req = _JsonRPCRequest( - jsonRequest: jsonRpcRequest, - requestTimeout: requestTimeout, - completer: Completer(), - ); - - final future = req.completer.future.onError( - (error, stackTrace) async { - await disconnect( - reason: "return req.completer.future.onError: $error\n$stackTrace", - ); - return JsonRPCResponse( - exception: error is JsonRpcException - ? error - : JsonRpcException( - "req.completer.future.onError: $error\n$stackTrace", - ), - ); - }, - ); - - // if this is the only/first request then send it right away - await _requestQueue.add( - req, - onInitialRequestAdded: _sendNextAvailableRequest, - ); - - return future; - } - - /// DO NOT set [ignoreMutex] to true unless fully aware of the consequences - Future disconnect({ - required String reason, - bool ignoreMutex = false, - }) async { - if (ignoreMutex) { - await _disconnectHelper(reason: reason); - } else { - await _requestMutex.protect(() async { - await _disconnectHelper(reason: reason); - }); - } - } - - Future _disconnectHelper({required String reason}) async { - await _subscription?.cancel(); - _subscription = null; - _socket?.destroy(); - _socket = null; - await _socksSocket?.close(); - _socksSocket = null; - - // clean up remaining queue - await _requestQueue.completeRemainingWithError( - "JsonRPC disconnect() called with reason: \"$reason\"", - ); - } - - Future _connect() async { - // ignore mutex is set to true here as _connect is already called within - // the mutex.protect block. Setting to false here leads to a deadlock - await disconnect( - reason: "New connection requested", - ignoreMutex: true, - ); - - if (!Prefs.instance.useTor) { - if (useSSL) { - _socket = await SecureSocket.connect( - host, - port, - timeout: connectionTimeout, - onBadCertificate: (_) => true, - ); // TODO do not automatically trust bad certificates. - } else { - _socket = await Socket.connect( - host, - port, - timeout: connectionTimeout, - ); - } - - _subscription = _socket!.listen( - _dataHandler, - onError: _errorHandler, - onDone: _doneHandler, - cancelOnError: true, - ); - } else { - if (proxyInfo == null) { - throw JsonRpcException( - "JsonRPC.connect failed with useTor=${Prefs.instance.useTor} and proxyInfo is null"); - } - - // instantiate a socks socket at localhost and on the port selected by the tor service - _socksSocket = await SOCKSSocket.create( - proxyHost: proxyInfo!.host.address, - proxyPort: proxyInfo!.port, - sslEnabled: useSSL, - ); - - try { - Logging.instance.log( - "JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...", - level: LogLevel.Info); - - await _socksSocket?.connect(); - - Logging.instance.log( - "JsonRPC.connect(): connected to SOCKS socket at $proxyInfo...", - level: LogLevel.Info); - } catch (e) { - Logging.instance.log( - "JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e", - level: LogLevel.Error); - throw JsonRpcException( - "JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e"); - } - - try { - Logging.instance.log( - "JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...", - level: LogLevel.Info); - - await _socksSocket?.connectTo(host, port); - - Logging.instance.log( - "JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo", - level: LogLevel.Info); - } catch (e) { - Logging.instance.log( - "JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e", - level: LogLevel.Error); - throw JsonRpcException( - "JsonRPC.connect(): failed to connect to tor proxy, $e"); - } - - _subscription = _socksSocket!.listen( - _dataHandler, - onError: _errorHandler, - onDone: _doneHandler, - cancelOnError: true, - ); - } - - return; - } -} - -class _JsonRPCRequestQueue { - final _lock = Mutex(); - final List<_JsonRPCRequest> _rq = []; - - Future add( - _JsonRPCRequest req, { - VoidCallback? onInitialRequestAdded, - }) async { - return await _lock.protect(() async { - _rq.add(req); - if (_rq.length == 1) { - onInitialRequestAdded?.call(); - } - }); - } - - Future remove(_JsonRPCRequest req) async { - return await _lock.protect(() async { - final result = _rq.remove(req); - return result; - }); - } - - Future<_JsonRPCRequest?> get nextIncompleteReq async { - return await _lock.protect(() async { - int removeCount = 0; - _JsonRPCRequest? returnValue; - for (final req in _rq) { - if (req.isComplete) { - removeCount++; - } else { - returnValue = req; - break; - } - } - - _rq.removeRange(0, removeCount); - - return returnValue; - }); - } - - Future completeRemainingWithError( - String error, { - StackTrace? stackTrace, - }) async { - await _lock.protect(() async { - for (final req in _rq) { - if (!req.isComplete) { - req.completer.completeError(Exception(error), stackTrace); - } - } - _rq.clear(); - }); - } - - Future get isEmpty async { - return await _lock.protect(() async { - return _rq.isEmpty; - }); - } -} - -class _JsonRPCRequest { - // 0x0A is newline - // https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html - static const int separatorByte = 0x0A; - - final String jsonRequest; - final Completer completer; - final Duration requestTimeout; - final List _responseData = []; - - _JsonRPCRequest({ - required this.jsonRequest, - required this.completer, - required this.requestTimeout, - }); - - void appendDataAndCheckIfComplete(List data) { - _responseData.addAll(data); - if (data.last == separatorByte) { - try { - final response = json.decode(String.fromCharCodes(_responseData)); - completer.complete(JsonRPCResponse(data: response)); - } catch (e, s) { - Logging.instance.log( - "JsonRPC json.decode: $e\n$s", - level: LogLevel.Error, - ); - completer.completeError(e, s); - } - } - } - - void initiateTimeout({ - required VoidCallback onTimedOut, - }) { - Future.delayed(requestTimeout).then((_) { - if (!isComplete) { - completer.complete( - JsonRPCResponse( - data: null, - exception: JsonRpcException( - "_JsonRPCRequest timed out: $jsonRequest", - ), - ), - ); - } - onTimedOut.call(); - }); - } - - bool get isComplete => completer.isCompleted; -} - -class JsonRPCResponse { - final dynamic data; - final JsonRpcException? exception; - - JsonRPCResponse({this.data, this.exception}); -} diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 3c56cde4d..17a5e684a 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -14,18 +14,20 @@ 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: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'; @@ -173,17 +175,14 @@ class _AddEditNodeViewState extends ConsumerState { 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; } @@ -191,14 +190,13 @@ class _AddEditNodeViewState extends ConsumerState { break; case Coin.ethereum: - // TODO fix this - // final client = Web3Client( - // "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", - // Client()); try { - // await client.getSyncStatus(); - } catch (_) {} + testPassed = await testEthNodeConnection(formData.host!); + } catch (_) { + testPassed = false; + } break; + case Coin.stellar: case Coin.stellarTestnet: try { diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 83e6bbab9..95868d5c6 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -13,13 +13,14 @@ 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: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'; @@ -150,17 +151,14 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.eCash: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: - final client = ElectrumXClient( - host: node!.host, - port: node.port, - useSSL: node.useSSL, - failovers: [], - prefs: ref.read(prefsChangeNotifierProvider), - coin: coin, - ); - 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; } diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 5a71c4668..182de8747 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -147,7 +147,7 @@ class NotificationsService extends ChangeNotifier { node: eNode, failovers: failovers, prefs: prefs, - coin: coin, + cryptoCurrency: wallet.cryptoCurrency, ); final tx = await client.getTransaction(txHash: txid); diff --git a/lib/utilities/connection_check/electrum_connection_check.dart b/lib/utilities/connection_check/electrum_connection_check.dart new file mode 100644 index 000000000..1dbc003c8 --- /dev/null +++ b/lib/utilities/connection_check/electrum_connection_check.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:electrum_adapter/electrum_adapter.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +Future checkElectrumServer({ + required String host, + required int port, + required bool useSSL, + Prefs? overridePrefs, + TorService? overrideTorService, +}) async { + final _prefs = overridePrefs ?? Prefs.instance; + final _torService = overrideTorService ?? TorService.sharedInstance; + + ({InternetAddress host, int port})? proxyInfo; + + try { + // If we're supposed to use Tor... + if (_prefs.useTor) { + // But Tor isn't running... + if (_torService.status != TorConnectionStatus.connected) { + // And the killswitch isn't set... + if (!_prefs.torKillSwitch) { + // Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function. + Logging.instance.log( + "Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter through clearnet", + level: LogLevel.Warning, + ); + } else { + // ... But if the killswitch is set, then we throw an exception. + throw Exception( + "Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter"); + // TODO [prio=low]: Try to start Tor. + } + } else { + // Get the proxy info from the TorService. + proxyInfo = _torService.getProxyInfo(); + } + } + + final client = await ElectrumClient.connect( + host: host, + port: port, + useSSL: useSSL, + proxyInfo: proxyInfo, + ).timeout( + const Duration(seconds: 5), + onTimeout: () => throw Exception( + "The checkElectrumServer connect() call timed out.", + ), + ); + + await client.ping().timeout(const Duration(seconds: 5)); + + return true; + } catch (_) { + return false; + } +} diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index fdefac4e9..d4cd2b1e2 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'dart:ffi'; -import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; -import 'package:electrum_adapter/electrum_adapter.dart'; import 'package:flutter/foundation.dart'; import 'package:frostdart/frostdart.dart' as frost; import 'package:frostdart/frostdart_bindings_generated.dart'; @@ -20,19 +18,15 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; -import 'package:stream_channel/stream_channel.dart'; class BitcoinFrostWallet extends Wallet { BitcoinFrostWallet(CryptoCurrencyNetwork network) @@ -44,8 +38,6 @@ class BitcoinFrostWallet extends Wallet { .findFirstSync()!; late ElectrumXClient electrumXClient; - late StreamChannel electrumAdapterChannel; - late ElectrumClient electrumAdapterClient; late CachedElectrumXClient electrumXCachedClient; Future initializeNewFrost({ @@ -1102,66 +1094,29 @@ class BitcoinFrostWallet extends Wallet { final newNode = await _getCurrentElectrumXNode(); try { - await electrumXClient.electrumAdapterClient?.close(); - } catch (e, s) { + await electrumXClient.closeAdapter(); + } catch (e) { if (e.toString().contains("initialized")) { // Ignore. This should happen every first time the wallet is opened. } else { - Logging.instance - .log("Error closing electrumXClient: $e", level: LogLevel.Error); + Logging.instance.log( + "Error closing electrumXClient: $e", + level: LogLevel.Error, + ); } } electrumXClient = ElectrumXClient.from( node: newNode, prefs: prefs, failovers: failovers, - coin: cryptoCurrency.coin, + cryptoCurrency: cryptoCurrency, ); - electrumAdapterChannel = await electrum_adapter.connect( - newNode.address, - port: newNode.port, - acceptUnverified: true, - useSSL: newNode.useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - if (electrumXClient.coin == Coin.firo || - electrumXClient.coin == Coin.firoTestNet) { - electrumAdapterClient = FiroElectrumClient( - electrumAdapterChannel, - newNode.address, - newNode.port, - newNode.useSSL, - Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - } else { - electrumAdapterClient = ElectrumClient( - electrumAdapterChannel, - newNode.address, - newNode.port, - newNode.useSSL, - Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - } + electrumXCachedClient = CachedElectrumXClient.from( electrumXClient: electrumXClient, - electrumAdapterClient: electrumAdapterClient, - electrumAdapterUpdateCallback: updateClient, ); } - // TODO [prio=low]: Use ElectrumXInterface method. - Future updateClient() async { - Logging.instance.log( - "Updating electrum node and ElectrumAdapterClient from Frost wallet.", - level: LogLevel.Info); - await updateNode(); - return electrumAdapterClient; - } - bool _duplicateTxCheck( List> allTransactions, String txid) { for (int i = 0; i < allTransactions.length; i++) { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index df8f8deaf..51ca4ca70 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -3,11 +3,9 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; -import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; -import 'package:electrum_adapter/electrum_adapter.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; @@ -15,7 +13,6 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2 import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/signing_data.dart'; -import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; @@ -23,22 +20,16 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/paynym_is_api.dart'; -import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; -import 'package:stream_channel/stream_channel.dart'; mixin ElectrumXInterface on Bip39HDWallet { late ElectrumXClient electrumXClient; - late StreamChannel electrumAdapterChannel; - late ElectrumClient electrumAdapterClient; late CachedElectrumXClient electrumXCachedClient; - // late SubscribableElectrumXClient subscribableElectrumXClient; - late ChainHeightServiceManager chainHeightServiceManager; int? get maximumFeerate => null; @@ -706,6 +697,12 @@ mixin ElectrumXInterface on Bip39HDWallet { cryptoCurrency.networkParams, ); + print("============================================================="); + print("$i ${txData.recipients![i].amount.decimal}"); + print("$i ${txData.recipients![i].amount.raw}"); + print("$address"); + print("============================================================="); + final output = coinlib.Output.fromAddress( txData.recipients![i].amount.raw, address, @@ -785,24 +782,9 @@ mixin ElectrumXInterface on Bip39HDWallet { Future fetchChainHeight() async { try { - // Get the chain height service for the current coin. - ChainHeightService? service = ChainHeightServiceManager.getService( - cryptoCurrency.coin, + return await ClientManager.sharedInstance.getChainHeightFor( + cryptoCurrency, ); - - // ... or create a new one if it doesn't exist. - if (service == null) { - service = ChainHeightService(client: electrumAdapterClient); - ChainHeightServiceManager.add(service, cryptoCurrency.coin); - } - - // If the service hasn't been started, start it and fetch the chain height. - if (!service.started) { - return await service.fetchHeightAndStartListenForUpdates(); - } - - // Return the height as per the service if available or the cached height. - return service.height ?? info.cachedChainHeight; } catch (e, s) { Logging.instance.log( "Exception rethrown in fetchChainHeight\nError: $e\nStack trace: $s", @@ -868,7 +850,7 @@ mixin ElectrumXInterface on Bip39HDWallet { final newNode = await _getCurrentElectrumXNode(); try { - await electrumXClient.electrumAdapterClient?.close(); + await electrumXClient.closeAdapter(); } catch (e) { if (e.toString().contains("initialized")) { // Ignore. This should happen every first time the wallet is opened. @@ -881,50 +863,11 @@ mixin ElectrumXInterface on Bip39HDWallet { node: newNode, prefs: prefs, failovers: failovers, - coin: cryptoCurrency.coin, + cryptoCurrency: cryptoCurrency, ); - electrumAdapterChannel = await electrum_adapter.connect( - newNode.address, - port: newNode.port, - acceptUnverified: true, - useSSL: newNode.useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - if (electrumXClient.coin == Coin.firo || - electrumXClient.coin == Coin.firoTestNet) { - electrumAdapterClient = FiroElectrumClient( - electrumAdapterChannel, - newNode.address, - newNode.port, - newNode.useSSL, - Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - } else { - electrumAdapterClient = ElectrumClient( - electrumAdapterChannel, - newNode.address, - newNode.port, - newNode.useSSL, - Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - } electrumXCachedClient = CachedElectrumXClient.from( electrumXClient: electrumXClient, - electrumAdapterClient: electrumAdapterClient, - electrumAdapterUpdateCallback: updateClient, ); - // Replaced using electrum_adapters' SubscribableClient in fetchChainHeight. - // subscribableElectrumXClient = SubscribableElectrumXClient.from( - // node: newNode, - // prefs: prefs, - // failovers: failovers, - // ); - // await subscribableElectrumXClient.connect( - // host: newNode.address, port: newNode.port); } //============================================================================ @@ -1219,13 +1162,6 @@ mixin ElectrumXInterface on Bip39HDWallet { await updateElectrumX(); } - Future updateClient() async { - Logging.instance.log("Updating electrum node and ElectrumAdapterClient.", - level: LogLevel.Info); - await updateNode(); - return electrumAdapterClient; - } - FeeObject? _cachedFees; @override diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index e0d15a851..50c01f583 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -13,15 +13,16 @@ 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:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import 'package:stackwallet/providers/global/active_wallet_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -171,17 +172,14 @@ class _NodeCardState extends ConsumerState { case Coin.eCash: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: - final client = ElectrumXClient( - host: node.host, - port: node.port, - useSSL: node.useSSL, - failovers: [], - prefs: ref.read(prefsChangeNotifierProvider), - coin: widget.coin, - ); - 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; } diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index eda89e1ae..5eff8c8d1 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -13,7 +13,6 @@ 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:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; @@ -23,6 +22,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -153,18 +153,14 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.eCash: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: - final client = ElectrumXClient( - host: node.host, - port: node.port, - useSSL: node.useSSL, - failovers: [], - prefs: ref.read(prefsChangeNotifierProvider), - torService: ref.read(pTorService), - coin: coin, - ); - 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; } From bfba6d9f5db33c2023da4b75b5da905801de506c Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 22 Apr 2024 10:35:02 -0600 Subject: [PATCH 122/272] eth token price fetch "fix" and clearer eth token price fetch error logging --- lib/services/price.dart | 49 ++++++++++--------- .../electrumx_interface.dart | 6 --- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/lib/services/price.dart b/lib/services/price.dart index ce1a57301..25a3f154b 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -187,33 +187,38 @@ class PriceAPI { } try { - final contractAddressesString = - contractAddresses.reduce((value, element) => "$value,$element"); - final uri = Uri.parse( - "https://api.coingecko.com/api/v3/simple/token_price/ethereum" - "?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses" - "=$contractAddressesString&include_24hr_change=true"); + for (final contractAddress in contractAddresses) { + final uri = Uri.parse( + "https://api.coingecko.com/api/v3/simple/token_price/ethereum" + "?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses" + "=$contractAddress&include_24hr_change=true"); - final coinGeckoResponse = await client.get( - url: uri, - headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); + final coinGeckoResponse = await client.get( + url: uri, + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); - final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; + try { + final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; - for (final key in coinGeckoData.keys) { - final contractAddress = key as String; + final map = coinGeckoData[contractAddress] as Map; - final map = coinGeckoData[contractAddress] as Map; + final price = + Decimal.parse(map[baseCurrency.toLowerCase()].toString()); + final change24h = double.parse( + map["${baseCurrency.toLowerCase()}_24h_change"].toString()); - final price = Decimal.parse(map[baseCurrency.toLowerCase()].toString()); - final change24h = double.parse( - map["${baseCurrency.toLowerCase()}_24h_change"].toString()); - - tokenPrices[contractAddress] = Tuple2(price, change24h); + tokenPrices[contractAddress] = Tuple2(price, change24h); + } catch (e, s) { + // only log the error as we don't want to interrupt the rest of the loop + Logging.instance.log( + "getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddress): $e\n$s\nRESPONSE: $coinGeckoResponse.body", + level: LogLevel.Warning, + ); + } } return tokenPrices; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 51ca4ca70..52ceb2a77 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -697,12 +697,6 @@ mixin ElectrumXInterface on Bip39HDWallet { cryptoCurrency.networkParams, ); - print("============================================================="); - print("$i ${txData.recipients![i].amount.decimal}"); - print("$i ${txData.recipients![i].amount.raw}"); - print("$address"); - print("============================================================="); - final output = coinlib.Output.fromAddress( txData.recipients![i].amount.raw, address, From 3fe7f47d8b5afc42026007a328c68ebc1f71edc3 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 22 Apr 2024 17:04:57 -0600 Subject: [PATCH 123/272] clean up test epicbox connection --- lib/wallets/wallet/impl/epiccash_wallet.dart | 76 ++++++++------------ pubspec.lock | 24 +++---- pubspec.yaml | 2 +- 3 files changed, 44 insertions(+), 58 deletions(-) diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 8228ab7c8..a6365321d 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -36,7 +36,7 @@ import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; -import 'package:websocket_universal/websocket_universal.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; // // refactor of https://github.com/cypherstack/stack_wallet/blob/1d9fb4cd069f22492ece690ac788e05b8f8b1209/lib/services/coins/epiccash/epiccash_wallet.dart @@ -103,15 +103,16 @@ class EpiccashWallet extends Bip39Wallet { } Future getEpicBoxConfig() async { - EpicBoxConfigModel? _epicBoxConfig = - EpicBoxConfigModel.fromServer(DefaultEpicBoxes.defaultEpicBoxServer); + EpicBoxConfigModel? _epicBoxConfig = EpicBoxConfigModel.fromServer( + DefaultEpicBoxes.defaultEpicBoxServer, + ); //Get the default Epicbox server and check if it's conected // bool isEpicboxConnected = await _testEpicboxServer( // DefaultEpicBoxes.defaultEpicBoxServer.host, DefaultEpicBoxes.defaultEpicBoxServer.port ?? 443); // if (isEpicboxConnected) { - //Use default server for as Epicbox config + //Use default server for as Epicbox config // } // else { @@ -231,48 +232,33 @@ class EpiccashWallet extends Bip39Wallet { ); } - Future _testEpicboxServer(String host, int port) async { - // TODO use an EpicBoxServerModel as the only param - final websocketConnectionUri = 'wss://$host:$port'; - const connectionOptions = SocketConnectionOptions( - pingIntervalMs: 3000, - timeoutConnectionMs: 4000, + Future _testEpicboxServer(EpicBoxConfigModel epicboxConfig) async { + final host = epicboxConfig.host; + final port = epicboxConfig.port ?? 443; + WebSocketChannel? channel; + try { + final uri = Uri.parse('wss://$host:$port'); - /// see ping/pong messages in [logEventStream] stream - skipPingMessages: true, + channel = WebSocketChannel.connect( + uri, + ); - /// Set this attribute to `true` if do not need any ping/pong - /// messages and ping measurement. Default is `false` - pingRestrictionForce: true, - ); + await channel.ready; - final IMessageProcessor textSocketProcessor = - SocketSimpleTextProcessor(); - final textSocketHandler = IWebSocketHandler.createClient( - websocketConnectionUri, - textSocketProcessor, - connectionOptions: connectionOptions, - ); + final response = await channel.stream.first.timeout( + const Duration(seconds: 2), + ); - // Listening to server responses: - bool isConnected = true; - textSocketHandler.incomingMessagesStream.listen((inMsg) { + return response is String && response.contains("Challenge"); + } catch (_) { Logging.instance.log( - '> webSocket got text message from server: "$inMsg" ' - '[ping: ${textSocketHandler.pingDelayMs}]', - level: LogLevel.Info); - }); - - // Connecting to server: - final isTextSocketConnected = await textSocketHandler.connect(); - if (!isTextSocketConnected) { - // ignore: avoid_print - Logging.instance.log( - 'Connection to [$websocketConnectionUri] failed for some reason!', - level: LogLevel.Error); - isConnected = false; + "_testEpicBoxConnection failed on \"$host:$port\"", + level: LogLevel.Info, + ); + return false; + } finally { + await channel?.sink.close(); } - return isConnected; } Future _putSendToAddresses( @@ -335,9 +321,9 @@ class EpiccashWallet extends Bip39Wallet { return address; } - Future

thisWalletAddress(int index, EpicBoxConfigModel epicboxConfig) async { - final wallet = - await secureStorageInterface.read(key: '${walletId}_wallet'); + Future
thisWalletAddress( + int index, EpicBoxConfigModel epicboxConfig) async { + final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); // EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); final walletAddress = await epiccash.LibEpiccash.getAddressInfo( @@ -594,7 +580,8 @@ class EpiccashWallet extends Bip39Wallet { if (!receiverAddress.startsWith("http://") || !receiverAddress.startsWith("https://")) { bool isEpicboxConnected = await _testEpicboxServer( - epicboxConfig.host, epicboxConfig.port ?? 443); + epicboxConfig, + ); if (!isEpicboxConnected) { throw Exception("Failed to send TX : Unable to reach epicbox server"); } @@ -934,7 +921,6 @@ class EpiccashWallet extends Bip39Wallet { .findAll(); final myAddressesSet = myAddresses.toSet(); - final transactions = await epiccash.LibEpiccash.getTransactions( wallet: wallet!, refreshFromNode: refreshFromNode, diff --git a/pubspec.lock b/pubspec.lock index d5445f376..31815a754 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1970,6 +1970,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" web3dart: dependency: "direct main" description: @@ -1979,13 +1987,13 @@ packages: source: hosted version: "2.6.1" web_socket_channel: - dependency: transitive + dependency: "direct main" description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" webdriver: dependency: transitive description: @@ -2002,14 +2010,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - websocket_universal: - dependency: "direct main" - description: - name: websocket_universal - sha256: "681e3050bd70b9c94617394f87a021746d24d4b3c8302be8c6b0b2a3e4209d7f" - url: "https://pub.dev" - source: hosted - version: "0.5.2" win32: dependency: transitive description: @@ -2076,5 +2076,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 795c34a90..7680653a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: sdk: flutter ffi: ^2.0.1 mutex: ^3.0.0 - websocket_universal: ^0.5.1 + web_socket_channel: ^2.4.5 lelantus: path: ./crypto_plugins/flutter_liblelantus From 76a0a82b24c1d8ecf602d98c54b688e3334d82de Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 10:55:11 -0600 Subject: [PATCH 124/272] inspector should be off unless debugging --- lib/db/isar/main_db.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 7d10c8720..ac5a544f4 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -72,7 +72,7 @@ class MainDB { ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, - inspector: true, + inspector: false, name: "wallet_data", maxSizeMiB: 512, ); From ff86cbccf69fa0c993981ffa6404d2b22b18c2f3 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 11:01:21 -0600 Subject: [PATCH 125/272] Better error message when trying to send all and the fee is greater than the balance in the wallet --- .../wallet_mixin_interfaces/electrumx_interface.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 52ceb2a77..9a05c368a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -237,6 +237,13 @@ mixin ElectrumXInterface on Bip39HDWallet { } final int amount = satoshiAmountToSend - feeForOneOutput; + + if (amount < 0) { + throw Exception( + "Estimated fee ($feeForOneOutput sats) is greater than balance!", + ); + } + final data = await buildTransaction( txData: txData.copyWith( recipients: await _helperRecipientsConvert( From 351ce4ec027024d26c820a8ff906b4d699456312 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 15:06:36 -0600 Subject: [PATCH 126/272] mobile copy address button on receive screen --- lib/pages/receive_view/receive_view.dart | 39 ++++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 56a341f5d..1c2897e34 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -39,15 +39,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"; @@ -536,6 +538,26 @@ class _ReceiveViewState extends ConsumerState { ), ), ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Copy address", + onPressed: () { + HapticFeedback.lightImpact(); + clipboard.setData( + ClipboardData( + text: _qrcodeContent ?? "", + ), + ); + 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 || @@ -547,20 +569,11 @@ class _ReceiveViewState extends ConsumerState { .select((value) => value.getWallet(walletId))) is MultiAddressInterface || supportsSpark) - TextButton( + SecondaryButton( + label: "Generate new address", onPressed: supportsSpark && _showSparkAddress ? generateNewSparkAddress : generateNewAddress, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Generate new address", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), ), const SizedBox( height: 30, From 68210b2765e26b84d0885dd564b1d0e725bc499e Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Wed, 20 Mar 2024 03:50:42 +0300 Subject: [PATCH 127/272] add solana --- .../isar/models/blockchain_data/address.dart | 3 + .../add_edit_node_view.dart | 16 + .../manage_nodes_views/node_details_view.dart | 15 + lib/services/price.dart | 2 +- lib/themes/color_theme.dart | 3 + lib/themes/stack_colors.dart | 2 + lib/utilities/address_utils.dart | 3 + lib/utilities/amount/amount_unit.dart | 1 + lib/utilities/block_explorers.dart | 2 + lib/utilities/constants.dart | 11 + lib/utilities/default_nodes.dart | 15 + lib/utilities/enums/coin_enum.dart | 20 + .../enums/derive_path_type_enum.dart | 1 + lib/wallets/crypto_currency/coins/solana.dart | 48 +++ lib/wallets/wallet/impl/solana_wallet.dart | 362 ++++++++++++++++++ lib/wallets/wallet/wallet.dart | 5 + lib/widgets/node_card.dart | 15 + lib/widgets/node_options_sheet.dart | 15 + pubspec.lock | 40 +- pubspec.yaml | 6 + 20 files changed, 580 insertions(+), 5 deletions(-) create mode 100644 lib/wallets/crypto_currency/coins/solana.dart create mode 100644 lib/wallets/wallet/impl/solana_wallet.dart diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 16516edf4..a5bd64431 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -164,6 +164,7 @@ enum AddressType { stellar, tezos, frostMS, + solana, p2tr; String get readableName { @@ -196,6 +197,8 @@ enum AddressType { return "Tezos"; case AddressType.frostMS: return "FrostMS"; + case AddressType.solana: + return "Solana"; case AddressType.p2tr: return "Taproot"; // Why not use P2TR, P2PKH, etc.? } diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 17a5e684a..ef7ae7f31 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -14,6 +14,7 @@ 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: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'; @@ -216,6 +217,20 @@ class _AddEditNodeViewState extends ConsumerState { ); } catch (_) {} break; + + case Coin.solana: + try { + RpcClient rpcClient; + if (formData.host!.startsWith("http") || formData.host!.startsWith("https")) { + rpcClient = RpcClient("${formData.host}:${formData.port}"); + } else { + rpcClient = RpcClient("http://${formData.host}:${formData.port}"); + } + await rpcClient.getEpochInfo().then((value) => testPassed = true); + } catch (_) { + testPassed = false; + } + break; } if (showFlushBar && mounted) { @@ -756,6 +771,7 @@ class _NodeFormState extends ConsumerState { case Coin.nano: case Coin.banano: case Coin.eCash: + case Coin.solana: case Coin.stellar: case Coin.stellarTestnet: case Coin.bitcoinFrost: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 95868d5c6..c5516db55 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -13,6 +13,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package: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'; @@ -193,6 +194,20 @@ class _NodeDetailsViewState extends ConsumerState { testPassed = false; } break; + + case Coin.solana: + try { + RpcClient rpcClient; + if (node!.host.startsWith("http") || node.host.startsWith("https")) { + rpcClient = RpcClient("${node.host}:${node.port}"); + } else { + rpcClient = RpcClient("http://${node.host}:${node.port}"); + } + await rpcClient.getEpochInfo().then((value) => testPassed = true); + } catch (_) { + testPassed = false; + } + break; } if (testPassed) { diff --git a/lib/services/price.dart b/lib/services/price.dart index 25a3f154b..296537cb4 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -101,7 +101,7 @@ class PriceAPI { "https://api.coingecko.com/api/v3/coins/markets?vs_currency" "=${baseCurrency.toLowerCase()}" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," - "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos" + "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos,solana" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); final coinGeckoResponse = await client.get( diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index 38de6c636..c67f08ce8 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -28,6 +28,7 @@ class CoinThemeColorDefault { Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); Color get particl => const Color(0xFF8175BD); + Color get solana => const Color(0xFFC696FF); Color get stellar => const Color(0xFF6600FF); Color get nano => const Color(0xFF209CE9); Color get banano => const Color(0xFFFBDD11); @@ -66,6 +67,8 @@ class CoinThemeColorDefault { return wownero; case Coin.particl: return particl; + case Coin.solana: + return solana; case Coin.stellar: case Coin.stellarTestnet: return stellar; diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index 0cc83b04f..1f994934e 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1709,6 +1709,8 @@ class StackColors extends ThemeExtension { return _coin.wownero; case Coin.particl: return _coin.particl; + case Coin.solana: + return _coin.solana; case Coin.stellar: case Coin.stellarTestnet: return _coin.stellar; diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 672da9059..722d9f9af 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; @@ -67,6 +68,8 @@ class AddressUtils { return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.particl: return Particl(CryptoCurrencyNetwork.main).validateAddress(address); + case Coin.solana: + return Solana(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.stellar: return Stellar(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.nano: diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index 87efcc4cd..cfa9fec30 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -55,6 +55,7 @@ enum AmountUnit { case Coin.stellar: // TODO: check if this is correct case Coin.stellarTestnet: case Coin.tezos: + case Coin.solana: return AmountUnit.values.sublist(0, 4); case Coin.monero: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 941f7c599..fcb2f5717 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -66,6 +66,8 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://testnet.stellarchain.io/transactions/$txid"); case Coin.tezos: return Uri.parse("https://tzstats.com/$txid"); + case Coin.solana: + return Uri.parse("https://explorer.solana.com/tx/$txid"); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index f7a6faeb2..f60a0fe1e 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -46,6 +46,7 @@ abstract class Constants { 10000000); // https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/assets#amount-precision static final BigInt _satsPerCoin = BigInt.from(100000000); static final BigInt _satsPerCoinTezos = BigInt.from(1000000); + static final BigInt _satsPerCoinSolana = BigInt.from(1000000000); static const int _decimalPlaces = 8; static const int _decimalPlacesNano = 30; static const int _decimalPlacesBanano = 29; @@ -55,6 +56,7 @@ abstract class Constants { static const int _decimalPlacesECash = 2; static const int _decimalPlacesStellar = 7; static const int _decimalPlacesTezos = 6; + static const int _decimalPlacesSolana = 9; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -109,6 +111,9 @@ abstract class Constants { case Coin.tezos: return _satsPerCoinTezos; + + case Coin.solana: + return _satsPerCoinSolana; } } @@ -155,6 +160,9 @@ abstract class Constants { case Coin.tezos: return _decimalPlacesTezos; + + case Coin.solana: + return _decimalPlacesSolana; } } @@ -176,6 +184,7 @@ abstract class Constants { case Coin.ethereum: case Coin.namecoin: case Coin.particl: + case Coin.solana: case Coin.nano: case Coin.stellar: case Coin.stellarTestnet: @@ -245,6 +254,7 @@ abstract class Constants { case Coin.nano: // TODO: Verify this case Coin.banano: // TODO: Verify this + case Coin.solana: return 1; case Coin.stellar: @@ -272,6 +282,7 @@ abstract class Constants { case Coin.namecoin: case Coin.particl: case Coin.ethereum: + case Coin.solana: return 12; case Coin.wownero: diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index b8f296b68..25d730c43 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -188,6 +188,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get solana => NodeModel( + host: "https://api.mainnet-beta.solana.com", // TODO: Change this to stack wallet one + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.solana), + useSSL: true, + enabled: true, + coinName: Coin.solana.name, + isFailover: true, + isDown: false, + ); + static NodeModel get stellar => NodeModel( host: "https://horizon.stellar.org", port: 443, @@ -348,6 +360,9 @@ abstract class DefaultNodes { case Coin.particl: return particl; + case Coin.solana: + return solana; + case Coin.stellar: return stellar; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index abb6985ea..6551dbc62 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -26,6 +26,7 @@ enum Coin { namecoin, nano, particl, + solana, stellar, tezos, wownero, @@ -69,6 +70,8 @@ extension CoinExt on Coin { return "Monero"; case Coin.particl: return "Particl"; + case Coin.solana: + return "Solana"; case Coin.stellar: return "Stellar"; case Coin.tezos: @@ -121,6 +124,8 @@ extension CoinExt on Coin { return "XMR"; case Coin.particl: return "PART"; + case Coin.solana: + return "SOL"; case Coin.stellar: return "XLM"; case Coin.tezos: @@ -173,6 +178,8 @@ extension CoinExt on Coin { return "monero"; case Coin.particl: return "particl"; + case Coin.solana: + return "solana"; case Coin.stellar: return "stellar"; case Coin.tezos: @@ -229,6 +236,7 @@ extension CoinExt on Coin { case Coin.nano: case Coin.banano: case Coin.tezos: + case Coin.solana: return false; } } @@ -259,6 +267,7 @@ extension CoinExt on Coin { case Coin.firoTestNet: case Coin.nano: case Coin.banano: + case Coin.solana: case Coin.stellar: case Coin.stellarTestnet: return false; @@ -284,6 +293,7 @@ extension CoinExt on Coin { case Coin.banano: case Coin.eCash: case Coin.stellar: + case Coin.solana: return false; case Coin.dogecoinTestNet: @@ -327,6 +337,7 @@ extension CoinExt on Coin { case Coin.banano: case Coin.eCash: case Coin.stellar: + case Coin.solana: return this; case Coin.dogecoinTestNet: @@ -400,6 +411,9 @@ extension CoinExt on Coin { case Coin.stellar: case Coin.stellarTestnet: return AddressType.stellar; + + case Coin.solana: + return AddressType.solana; } } } @@ -448,6 +462,10 @@ Coin coinFromPrettyName(String name) { case "particl": return Coin.particl; + case "Solana": + case "solana": + return Coin.solana; + case "Stellar": case "stellar": return Coin.stellar; @@ -548,6 +566,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.namecoin; case "part": return Coin.particl; + case "sol": + return Coin.solana; case "xlm": return Coin.stellar; case "xtz": diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 95b5d9abb..9f8af7a90 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -55,6 +55,7 @@ extension DerivePathTypeExt on DerivePathType { case Coin.stellar: case Coin.stellarTestnet: case Coin.tezos: // TODO: Is this true? + case Coin.solana: throw UnsupportedError( "$coin does not use bitcoin style derivation paths"); } diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart new file mode 100644 index 000000000..693dd096c --- /dev/null +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -0,0 +1,48 @@ +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; + +class Solana extends Bip39Currency { + Solana(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.solana; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.solana), + useSSL: true, + enabled: true, + coinName: Coin.solana.name, + isFailover: true, + isDown: false, + ); + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 21; + + @override + bool validateAddress(String address) { + RegExp regex = RegExp(r'^[a-zA-Z0-9]{44}$'); + return regex.hasMatch(address); + } + + @override + String get genesisHash => throw UnimplementedError(); +} \ No newline at end of file diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart new file mode 100644 index 000000000..ceb675d37 --- /dev/null +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -0,0 +1,362 @@ + +import 'dart:math'; + +import 'package:isar/isar.dart'; +import 'package:solana/dto.dart'; +import 'package:solana/solana.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' as isar; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:tuple/tuple.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/services/node_service.dart'; + +class SolanaWallet extends Bip39Wallet { + SolanaWallet(CryptoCurrencyNetwork network) : super(Solana(network)); + + NodeModel? _solNode; + + Future _getKeyPair() async { + return Ed25519HDKeyPair.fromMnemonic(await getMnemonic(), account: 0, change: 0); + } + + Future
_getCurrentAddress() async { + var addressStruct = Address( + walletId: walletId, + value: (await _getKeyPair()).address, + publicKey: List.empty(), + derivationIndex: 0, + derivationPath: null, + type: cryptoCurrency.coin.primaryAddressType, + subType: AddressSubType.unknown); + return addressStruct; + } + + Future _getCurrentBalanceInLamports() async { + var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + var balance = await rpcClient.getBalance((await _getKeyPair()).address); + return balance.value; + } + + @override + FilterOperation? get changeAddressFilterOperation => + throw UnimplementedError(); + + @override + Future checkSaveInitialReceivingAddress() async { + try { + var address = (await _getKeyPair()).address; + + await mainDB.updateOrPutAddresses([ + Address( + walletId: walletId, + value: address, + publicKey: List.empty(), + derivationIndex: 0, + derivationPath: null, + type: cryptoCurrency.coin.primaryAddressType, + subType: AddressSubType.unknown) + ]); + } catch (e, s) { + Logging.instance.log( + "$runtimeType checkSaveInitialReceivingAddress() failed: $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future prepareSend({required TxData txData}) async { + try { + if (txData.recipients == null || txData.recipients!.length != 1) { + throw Exception("$runtimeType prepareSend requires 1 recipient"); + } + + Amount sendAmount = txData.amount!; + + if (sendAmount > info.cachedBalance.spendable) { + throw Exception("Insufficient available balance"); + } + + int feeAmount; + var currentFees = await fees; + switch (txData.feeRateType) { + case FeeRateType.fast: + feeAmount = currentFees.fast; + break; + case FeeRateType.slow: + feeAmount = currentFees.slow; + break; + case FeeRateType.average: + default: + feeAmount = currentFees.medium; + break; + } + + // Rent exemption of Solana + final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + final accInfo = await rpcClient.getAccountInfo((await _getKeyPair()).address); + final minimumRent = await rpcClient.getMinimumBalanceForRentExemption(accInfo.value!.data.toString().length); + if (minimumRent > ((await _getCurrentBalanceInLamports()) - txData.amount!.raw.toInt() - feeAmount)) { + throw Exception("Insufficient remaining balance for rent exemption, minimum rent: ${minimumRent / pow(10, cryptoCurrency.fractionDigits)}"); + } + + return txData.copyWith( + fee: Amount( + rawValue: BigInt.from(feeAmount), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + } catch (e, s) { + Logging.instance.log( + "$runtimeType Solana prepareSend failed: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + @override + Future confirmSend({required TxData txData}) async { + try { + final keyPair = await _getKeyPair(); + final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + var recipientAccount = txData.recipients!.first; + var recipientPubKey = Ed25519HDPublicKey.fromBase58(recipientAccount.address); + final message = Message( + instructions: [ + SystemInstruction.transfer(fundingAccount: keyPair.publicKey, recipientAccount: recipientPubKey, lamports: txData.amount!.raw.toInt()), + ComputeBudgetInstruction.setComputeUnitPrice(microLamports: txData.fee!.raw.toInt()), + ], + ); + + final txid = await rpcClient.signAndSendTransaction(message, [keyPair]); + return txData.copyWith( + txid: txid, + ); + } catch (e, s) { + Logging.instance.log( + "$runtimeType Solana confirmSend failed: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + if (info.cachedBalance.spendable.raw == BigInt.zero) { + return Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + final fee = await rpcClient.getFees(); + + return Amount( + rawValue: BigInt.from(fee.value.feeCalculator.lamportsPerSignature), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + Future get fees async { + final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + final fees = await rpcClient.getFees(); + return FeeObject( + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: fees.value.feeCalculator.lamportsPerSignature, + medium: fees.value.feeCalculator.lamportsPerSignature, + slow: fees.value.feeCalculator.lamportsPerSignature + ); + } + + @override + Future pingCheck() { + return Future.value(false); + } + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + @override + Future recover({required bool isRescan}) async { + await refreshMutex.protect(() async { + var addressStruct = await _getCurrentAddress(); + + await mainDB.updateOrPutAddresses([addressStruct]); + + if (info.cachedReceivingAddress != addressStruct.value) { + await info.updateReceivingAddress( + newAddress: addressStruct.value, + isar: mainDB.isar, + ); + } + + await Future.wait([ + updateBalance(), + updateChainHeight(), + updateTransactions(), + ]); + }); + } + + @override + Future updateBalance() async { + try { + var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + var balance = await rpcClient.getBalance(info.cachedReceivingAddress); + + // Rent exemption of Solana + final accInfo = await rpcClient.getAccountInfo((await _getKeyPair()).address); + final minimumRent = await rpcClient.getMinimumBalanceForRentExemption(accInfo.value!.data.toString().length); + var spendableBalance = balance.value - minimumRent; + + final newBalance = Balance( + total: Amount( + rawValue: BigInt.from(balance.value), + fractionDigits: Coin.solana.decimals, + ), + spendable: Amount( + rawValue: BigInt.from(spendableBalance), + fractionDigits: Coin.solana.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.from(minimumRent), + fractionDigits: Coin.solana.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: Coin.solana.decimals, + ), + ); + + await info.updateBalance(newBalance: newBalance, isar: mainDB.isar); + } catch (e, s) { + Logging.instance.log( + "Error getting balance in solana_wallet.dart: $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future updateChainHeight() async { + try { + var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + var blockHeight = await rpcClient.getSlot(); + + await info.updateCachedChainHeight( + newHeight: blockHeight, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.log( + "Error occurred in solana_wallet.dart while getting" + " chain height for solana: $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future updateNode() async { + _solNode = getCurrentNode(); + await refresh(); + } + + @override + NodeModel getCurrentNode() { + return _solNode ?? + NodeService(secureStorageInterface: secureStorageInterface) + .getPrimaryNodeFor(coin: info.coin) ?? + DefaultNodes.getNodeFor(info.coin); + } + + @override + Future updateTransactions() async { + try { + var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + var transactionsList = await rpcClient.getTransactionsList((await _getKeyPair()).publicKey, encoding: Encoding.jsonParsed); + var txsList = List>.empty(growable: true); + + for (final tx in transactionsList) { + var senderAddress = (tx.transaction as ParsedTransaction).message.accountKeys[0].pubkey; + var receiverAddress = (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; + var txType = isar.TransactionType.unknown; + var txAmount = Amount( + rawValue: BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + if ((senderAddress == (await _getKeyPair()).address) && (receiverAddress == (await _getKeyPair()).address) ){ + txType = isar.TransactionType.sentToSelf; + } else if (senderAddress == (await _getKeyPair()).address) { + txType = isar.TransactionType.outgoing; + } else if (receiverAddress == (await _getKeyPair()).address) { + txType = isar.TransactionType.incoming; + } + + var transaction = isar.Transaction( + walletId: walletId, + txid: (tx.transaction as ParsedTransaction).signatures[0], + timestamp: tx.blockTime!, + type: txType, + subType: isar.TransactionSubType.none, + amount: tx.meta!.postBalances[1] - tx.meta!.preBalances[1], + amountString: txAmount.toJsonString(), + fee: tx.meta!.fee, + height: tx.slot, + isCancelled: false, + isLelantus: false, + slateId: null, + otherData: null, + inputs: [], + outputs: [], + nonce: null, + numberOfMessages: 0, + ); + + var txAddress = Address( + walletId: walletId, + value: receiverAddress, + publicKey: List.empty(), + derivationIndex: 0, + derivationPath: null, + type: AddressType.solana, + subType: txType == isar.TransactionType.outgoing ? AddressSubType.unknown : AddressSubType.receiving + ); + + txsList.add(Tuple2(transaction, txAddress)); + } + await mainDB.addNewTransactionData(txsList, walletId); + } catch (e, s) { + Logging.instance.log( + "Error occurred in solana_wallet.dart while getting" + " transactions for solana: $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future updateUTXOs() { + // No UTXOs in Solana + return Future.value(false); + } +} \ No newline at end of file diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 00131b88f..f8473450b 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -52,6 +52,8 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interf import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import 'impl/solana_wallet.dart'; + abstract class Wallet { // default to Transaction class. For TransactionV2 set to 2 int get isarTransactionVersion => 1; @@ -362,6 +364,9 @@ abstract class Wallet { case Coin.particl: return ParticlWallet(CryptoCurrencyNetwork.main); + case Coin.solana: + return SolanaWallet(CryptoCurrencyNetwork.main); + case Coin.stellar: return StellarWallet(CryptoCurrencyNetwork.main); case Coin.stellarTestnet: diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 50c01f583..006364342 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -13,6 +13,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:solana/solana.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; @@ -213,6 +214,20 @@ class _NodeCardState extends ConsumerState { testPassed = false; } break; + + case Coin.solana: + try { + RpcClient rpcClient; + if (node.host.startsWith("http") || node.host.startsWith("https")) { + rpcClient = RpcClient("${node.host}:${node.port}"); + } else { + rpcClient = RpcClient("http://${node.host}:${node.port}"); + } + await rpcClient.getEpochInfo().then((value) => testPassed = true); + } catch (_) { + testPassed = false; + } + break; } if (testPassed) { diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 5eff8c8d1..a91f63fba 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -13,6 +13,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:solana/solana.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; @@ -182,6 +183,20 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.stellarTestnet: throw UnimplementedError(); //TODO: check network/node + + case Coin.solana: + try { + RpcClient rpcClient; + if (node.host.startsWith("http") || node.host.startsWith("https")) { + rpcClient = RpcClient("${node.host}:${node.port}"); + } else { + rpcClient = RpcClient("http://${node.host}:${node.port}"); + } + await rpcClient.getEpochInfo().then((value) => testPassed = true); + } catch (_) { + testPassed = false; + } + break; } if (testPassed) { diff --git a/pubspec.lock b/pubspec.lock index 31815a754..439ad990f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -158,6 +158,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + borsh_annotation: + dependency: transitive + description: + name: borsh_annotation + sha256: "4a226cf8b7a165ecf8020c0c8d366b2728167fd102ef9b9e89d94d86f89ac57b" + url: "https://pub.dev" + source: hosted + version: "0.3.1+5" bs58check: dependency: "direct main" description: @@ -506,6 +514,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.9" + ed25519_hd_key: + dependency: transitive + description: + name: ed25519_hd_key + sha256: c5c9f11a03f5789bf9dcd9ae88d641571c802640851f1cacdb13123f171b3a26 + url: "https://pub.dev" + source: hosted + version: "2.2.1" eip1559: dependency: transitive description: @@ -588,7 +604,7 @@ packages: source: hosted version: "2.1.0" file: - dependency: transitive + dependency: "direct overridden" description: name: file sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" @@ -694,7 +710,7 @@ packages: source: hosted version: "17.0.0" flutter_local_notifications_linux: - dependency: transitive + dependency: "direct overridden" description: name: flutter_local_notifications_linux sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" @@ -815,6 +831,14 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -1393,7 +1417,7 @@ packages: source: hosted version: "1.2.0-beta-1" process: - dependency: transitive + dependency: "direct overridden" description: name: process sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" @@ -1558,6 +1582,14 @@ packages: url: "https://github.com/cypherstack/socks_socket.git" source: git version: "0.1.0" + solana: + dependency: "direct main" + description: + name: solana + sha256: "99a6a40a847f57ccf4687a730413d67fcaef4fc6778ddd9c3258e7fe8e4c6743" + url: "https://pub.dev" + source: hosted + version: "0.30.3" source_gen: dependency: transitive description: @@ -2036,7 +2068,7 @@ packages: source: git version: "0.1.0" xdg_directories: - dependency: transitive + dependency: "direct overridden" description: name: xdg_directories sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d diff --git a/pubspec.yaml b/pubspec.yaml index 7680653a6..54be2dc06 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -177,6 +177,7 @@ dependencies: url: https://github.com/cypherstack/electrum_adapter.git ref: 9e9441fc1e9ace8907256fff05fe2c607b0933b6 stream_channel: ^2.1.0 + solana: ^0.30.3 dev_dependencies: flutter_test: @@ -251,6 +252,11 @@ dependency_overrides: crypto: 3.0.2 analyzer: ^5.2.0 pinenacl: ^0.3.3 + xdg_directories: ^0.2.0 + flutter_local_notifications_linux: ^0.5.0+1 + process: ^4.0.0 + file: ^6.0.0 + http: ^0.13.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 4e732a5253876ca9f9ff76228b17a2c9d4959dfa Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 21 Mar 2024 12:46:59 -0500 Subject: [PATCH 128/272] add solana enabled pref toggled in dev menu --- .../global_settings_view/hidden_settings.dart | 31 +++++++++++++++++++ lib/utilities/prefs.dart | 22 +++++++++++++ 2 files changed, 53 insertions(+) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 6ebafad4a..0073d94ac 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -276,6 +277,36 @@ class HiddenSettings extends StatelessWidget { const SizedBox( height: 12, ), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + ref + .read(prefsChangeNotifierProvider) + .solanaEnabled = + !(ref + .read(prefsChangeNotifierProvider) + .solanaEnabled); + if (kDebugMode) { + print( + "Solana enabled: ${ref.read(prefsChangeNotifierProvider).solanaEnabled}"); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Toggle Solana", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }, + ), + const SizedBox( + height: 12, + ), Consumer( builder: (_, ref, __) { return GestureDetector( diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 8fbbbf069..d380901c3 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -68,6 +68,7 @@ class Prefs extends ChangeNotifier { await _setMaxDecimals(); _useTor = await _getUseTor(); _fusionServerInfo = await _getFusionServerInfo(); + _solanaEnabled = await _getSolanaEnabled(); _frostEnabled = await _getFrostEnabled(); _initialized = true; @@ -1010,6 +1011,27 @@ class Prefs extends ChangeNotifier { return actualMap; } + // Solana + + bool _solanaEnabled = false; + + bool get solanaEnabled => _solanaEnabled; + + set solanaEnabled(bool solanaEnabled) { + if (_solanaEnabled != solanaEnabled) { + DB.instance.put( + boxName: DB.boxNamePrefs, key: "solanaEnabled", value: solanaEnabled); + _solanaEnabled = solanaEnabled; + notifyListeners(); + } + } + + Future _getSolanaEnabled() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, key: "solanaEnabled") as bool? ?? + false; + } + // FROST multisig bool _frostEnabled = false; From 00cff961313d5dfffcb73a27c06531e858111c4a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 21 Mar 2024 12:54:04 -0500 Subject: [PATCH 129/272] hide solana behind prefs toggle toggle in dev menu --- .../add_wallet_views/add_wallet_view/add_wallet_view.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 3d6c4b8df..d84bfc708 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -139,6 +139,11 @@ class _AddWalletViewState extends ConsumerState { _coins.remove(Coin.bitcoinFrost); } + // Remove Solana from the list of coins based on our frostEnabled preference. + if (!ref.read(prefsChangeNotifierProvider).solanaEnabled) { + _coins.remove(Coin.solana); + } + coinEntities.addAll(_coins.map((e) => CoinEntity(e))); if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) { From 11a5ed33e5c0e48bc46ffca39515bab333ea550e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 21 Mar 2024 13:57:27 -0500 Subject: [PATCH 130/272] solana tor wip --- lib/wallets/wallet/impl/solana_wallet.dart | 165 ++++++++++++++------- 1 file changed, 115 insertions(+), 50 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index ceb675d37..b77c6ef99 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -1,33 +1,40 @@ - +import 'dart:io'; import 'dart:math'; import 'package:isar/isar.dart'; import 'package:solana/dto.dart'; import 'package:solana/solana.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' as isar; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' + as isar; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/models/balance.dart'; import 'package:tuple/tuple.dart'; -import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/models/node_model.dart'; -import 'package:stackwallet/services/node_service.dart'; class SolanaWallet extends Bip39Wallet { SolanaWallet(CryptoCurrencyNetwork network) : super(Solana(network)); NodeModel? _solNode; + ElectrumXClient? electrumXClient; // Used for Tor. + RpcClient? rpcClient; // The Solana RpcClient. + Future _getKeyPair() async { - return Ed25519HDKeyPair.fromMnemonic(await getMnemonic(), account: 0, change: 0); + return Ed25519HDKeyPair.fromMnemonic(await getMnemonic(), + account: 0, change: 0); } Future
_getCurrentAddress() async { @@ -43,9 +50,9 @@ class SolanaWallet extends Bip39Wallet { } Future _getCurrentBalanceInLamports() async { - var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - var balance = await rpcClient.getBalance((await _getKeyPair()).address); - return balance.value; + await _checkClients(); // Check electrumXClient and rpcClient. + var balance = await rpcClient?.getBalance((await _getKeyPair()).address); + return balance!.value; } @override @@ -78,6 +85,8 @@ class SolanaWallet extends Bip39Wallet { @override Future prepareSend({required TxData txData}) async { try { + await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + if (txData.recipients == null || txData.recipients!.length != 1) { throw Exception("$runtimeType prepareSend requires 1 recipient"); } @@ -104,11 +113,17 @@ class SolanaWallet extends Bip39Wallet { } // Rent exemption of Solana - final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - final accInfo = await rpcClient.getAccountInfo((await _getKeyPair()).address); - final minimumRent = await rpcClient.getMinimumBalanceForRentExemption(accInfo.value!.data.toString().length); - if (minimumRent > ((await _getCurrentBalanceInLamports()) - txData.amount!.raw.toInt() - feeAmount)) { - throw Exception("Insufficient remaining balance for rent exemption, minimum rent: ${minimumRent / pow(10, cryptoCurrency.fractionDigits)}"); + final accInfo = + await rpcClient?.getAccountInfo((await _getKeyPair()).address); + int minimumRent = await rpcClient?.getMinimumBalanceForRentExemption( + accInfo!.value!.data.toString().length) ?? + 0; // TODO revisit null condition. + if (minimumRent > + ((await _getCurrentBalanceInLamports()) - + txData.amount!.raw.toInt() - + feeAmount)) { + throw Exception( + "Insufficient remaining balance for rent exemption, minimum rent: ${minimumRent / pow(10, cryptoCurrency.fractionDigits)}"); } return txData.copyWith( @@ -129,18 +144,24 @@ class SolanaWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { try { + await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + final keyPair = await _getKeyPair(); - final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); var recipientAccount = txData.recipients!.first; - var recipientPubKey = Ed25519HDPublicKey.fromBase58(recipientAccount.address); + var recipientPubKey = + Ed25519HDPublicKey.fromBase58(recipientAccount.address); final message = Message( instructions: [ - SystemInstruction.transfer(fundingAccount: keyPair.publicKey, recipientAccount: recipientPubKey, lamports: txData.amount!.raw.toInt()), - ComputeBudgetInstruction.setComputeUnitPrice(microLamports: txData.fee!.raw.toInt()), + SystemInstruction.transfer( + fundingAccount: keyPair.publicKey, + recipientAccount: recipientPubKey, + lamports: txData.amount!.raw.toInt()), + ComputeBudgetInstruction.setComputeUnitPrice( + microLamports: txData.fee!.raw.toInt()), ], ); - final txid = await rpcClient.signAndSendTransaction(message, [keyPair]); + final txid = await rpcClient?.signAndSendTransaction(message, [keyPair]); return txData.copyWith( txid: txid, ); @@ -155,6 +176,8 @@ class SolanaWallet extends Bip39Wallet { @override Future estimateFeeFor(Amount amount, int feeRate) async { + await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + if (info.cachedBalance.spendable.raw == BigInt.zero) { return Amount( rawValue: BigInt.zero, @@ -162,27 +185,28 @@ class SolanaWallet extends Bip39Wallet { ); } - final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - final fee = await rpcClient.getFees(); + final fee = await rpcClient?.getFees(); + // TODO [prio=low]: handle null fee. return Amount( - rawValue: BigInt.from(fee.value.feeCalculator.lamportsPerSignature), + rawValue: BigInt.from(fee!.value.feeCalculator.lamportsPerSignature), fractionDigits: cryptoCurrency.fractionDigits, ); } @override Future get fees async { - final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - final fees = await rpcClient.getFees(); + await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + + final fees = await rpcClient?.getFees(); + // TODO [prio=low]: handle null fees. return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 1, numberOfBlocksSlow: 1, - fast: fees.value.feeCalculator.lamportsPerSignature, - medium: fees.value.feeCalculator.lamportsPerSignature, - slow: fees.value.feeCalculator.lamportsPerSignature - ); + fast: fees!.value.feeCalculator.lamportsPerSignature, + medium: fees!.value.feeCalculator.lamportsPerSignature, + slow: fees!.value.feeCalculator.lamportsPerSignature); } @override @@ -219,13 +243,20 @@ class SolanaWallet extends Bip39Wallet { @override Future updateBalance() async { try { - var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - var balance = await rpcClient.getBalance(info.cachedReceivingAddress); + await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + + var balance = await rpcClient?.getBalance(info.cachedReceivingAddress); // Rent exemption of Solana - final accInfo = await rpcClient.getAccountInfo((await _getKeyPair()).address); - final minimumRent = await rpcClient.getMinimumBalanceForRentExemption(accInfo.value!.data.toString().length); - var spendableBalance = balance.value - minimumRent; + final accInfo = + await rpcClient?.getAccountInfo((await _getKeyPair()).address); + // TODO [prio=low]: handle null account info. + final int minimumRent = + await rpcClient?.getMinimumBalanceForRentExemption( + accInfo!.value!.data.toString().length) ?? + 0; + // TODO [prio=low]: revisit null condition. + var spendableBalance = balance!.value - minimumRent; final newBalance = Balance( total: Amount( @@ -258,8 +289,10 @@ class SolanaWallet extends Bip39Wallet { @override Future updateChainHeight() async { try { - var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - var blockHeight = await rpcClient.getSlot(); + await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + + int blockHeight = await rpcClient?.getSlot() ?? 0; + // TODO [prio=low]: Revisit null condition. await info.updateCachedChainHeight( newHeight: blockHeight, @@ -268,7 +301,7 @@ class SolanaWallet extends Bip39Wallet { } catch (e, s) { Logging.instance.log( "Error occurred in solana_wallet.dart while getting" - " chain height for solana: $e\n$s", + " chain height for solana: $e\n$s", level: LogLevel.Error, ); } @@ -291,20 +324,30 @@ class SolanaWallet extends Bip39Wallet { @override Future updateTransactions() async { try { - var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - var transactionsList = await rpcClient.getTransactionsList((await _getKeyPair()).publicKey, encoding: Encoding.jsonParsed); - var txsList = List>.empty(growable: true); + await _checkClients(); // Check ElectrumXClient and Solana RpcClient. - for (final tx in transactionsList) { - var senderAddress = (tx.transaction as ParsedTransaction).message.accountKeys[0].pubkey; - var receiverAddress = (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; + var transactionsList = await rpcClient?.getTransactionsList( + (await _getKeyPair()).publicKey, + encoding: Encoding.jsonParsed); + var txsList = + List>.empty(growable: true); + + // TODO [prio=low]: Revisit null assertion below. + + for (final tx in transactionsList!) { + var senderAddress = + (tx.transaction as ParsedTransaction).message.accountKeys[0].pubkey; + var receiverAddress = + (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; var txType = isar.TransactionType.unknown; var txAmount = Amount( - rawValue: BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]), + rawValue: + BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]), fractionDigits: cryptoCurrency.fractionDigits, ); - if ((senderAddress == (await _getKeyPair()).address) && (receiverAddress == (await _getKeyPair()).address) ){ + if ((senderAddress == (await _getKeyPair()).address) && + (receiverAddress == (await _getKeyPair()).address)) { txType = isar.TransactionType.sentToSelf; } else if (senderAddress == (await _getKeyPair()).address) { txType = isar.TransactionType.outgoing; @@ -339,8 +382,9 @@ class SolanaWallet extends Bip39Wallet { derivationIndex: 0, derivationPath: null, type: AddressType.solana, - subType: txType == isar.TransactionType.outgoing ? AddressSubType.unknown : AddressSubType.receiving - ); + subType: txType == isar.TransactionType.outgoing + ? AddressSubType.unknown + : AddressSubType.receiving); txsList.add(Tuple2(transaction, txAddress)); } @@ -348,7 +392,7 @@ class SolanaWallet extends Bip39Wallet { } catch (e, s) { Logging.instance.log( "Error occurred in solana_wallet.dart while getting" - " transactions for solana: $e\n$s", + " transactions for solana: $e\n$s", level: LogLevel.Error, ); } @@ -359,4 +403,25 @@ class SolanaWallet extends Bip39Wallet { // No UTXOs in Solana return Future.value(false); } -} \ No newline at end of file + + /// Check that the ElectrumXClient is active and usable by a Solana RpcClient. + Future _checkClients() async { + if (prefs.useTor) { + electrumXClient ??= ElectrumXClient( + host: getCurrentNode().host, + port: getCurrentNode().port, + useSSL: getCurrentNode().useSSL, + failovers: [], + prefs: prefs, + coin: Coin.solana, + ); + int torPort = electrumXClient?.rpcClient?.proxyInfo?.port ?? + TorService.sharedInstance.getProxyInfo().port; + rpcClient = RpcClient("${InternetAddress.loopbackIPv4}:$torPort"); + } else { + rpcClient = + RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + } + return; + } +} From 00f1b3999b484a07becd038d6127dbed56ec10d9 Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:38:59 +0300 Subject: [PATCH 131/272] validation and other fixes --- lib/utilities/enums/derive_path_type_enum.dart | 5 ++++- lib/wallets/crypto_currency/coins/solana.dart | 4 ++-- lib/wallets/wallet/impl/solana_wallet.dart | 18 ++++++++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 9f8af7a90..f830bc077 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -17,6 +17,7 @@ enum DerivePathType { bip84, eth, eCash44, + solana, bip86, } @@ -45,6 +46,9 @@ extension DerivePathTypeExt on DerivePathType { case Coin.ethereum: // TODO: do we need something here? return DerivePathType.eth; + case Coin.solana: + return DerivePathType.solana; + case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: case Coin.epicCash: @@ -55,7 +59,6 @@ extension DerivePathTypeExt on DerivePathType { case Coin.stellar: case Coin.stellarTestnet: case Coin.tezos: // TODO: Is this true? - case Coin.solana: throw UnsupportedError( "$coin does not use bitcoin style derivation paths"); } diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart index 693dd096c..632e0f977 100644 --- a/lib/wallets/crypto_currency/coins/solana.dart +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -1,3 +1,4 @@ +import 'package:solana/solana.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; @@ -39,8 +40,7 @@ class Solana extends Bip39Currency { @override bool validateAddress(String address) { - RegExp regex = RegExp(r'^[a-zA-Z0-9]{44}$'); - return regex.hasMatch(address); + return isPointOnEd25519Curve(Ed25519HDPublicKey.fromBase58(address).toByteArray()); } @override diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index b77c6ef99..1808238c9 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -43,7 +43,7 @@ class SolanaWallet extends Bip39Wallet { value: (await _getKeyPair()).address, publicKey: List.empty(), derivationIndex: 0, - derivationPath: null, + derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'", type: cryptoCurrency.coin.primaryAddressType, subType: AddressSubType.unknown); return addressStruct; @@ -70,7 +70,7 @@ class SolanaWallet extends Bip39Wallet { value: address, publicKey: List.empty(), derivationIndex: 0, - derivationPath: null, + derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'", type: cryptoCurrency.coin.primaryAddressType, subType: AddressSubType.unknown) ]); @@ -211,7 +211,17 @@ class SolanaWallet extends Bip39Wallet { @override Future pingCheck() { - return Future.value(false); + try { + var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + rpcClient.getHealth(); + return Future.value(true); + } catch (e, s) { + Logging.instance.log( + "$runtimeType Solana pingCheck failed: $e\n$s", + level: LogLevel.Error, + ); + return Future.value(false); + } } @override @@ -380,7 +390,7 @@ class SolanaWallet extends Bip39Wallet { value: receiverAddress, publicKey: List.empty(), derivationIndex: 0, - derivationPath: null, + derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'", type: AddressType.solana, subType: txType == isar.TransactionType.outgoing ? AddressSubType.unknown From 896689a90e36d2f256c2b4b4ecd0af1d4f796a56 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 19 Apr 2024 15:28:28 -0500 Subject: [PATCH 132/272] disable Solana package's (espresso_cash_public's) package override --- pubspec.lock | 70 +++++++++++++++++----------------------------------- pubspec.yaml | 3 ++- 2 files changed, 25 insertions(+), 48 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 439ad990f..89aa98402 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -330,10 +330,10 @@ packages: dependency: transitive description: name: coverage - sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "1.6.4" cross_file: dependency: transitive description: @@ -607,10 +607,10 @@ packages: dependency: "direct overridden" description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.1.4" file_picker: dependency: "direct main" description: @@ -710,7 +710,7 @@ packages: source: hosted version: "17.0.0" flutter_local_notifications_linux: - dependency: "direct overridden" + dependency: transitive description: name: flutter_local_notifications_linux sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" @@ -1065,30 +1065,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" lelantus: dependency: "direct main" description: @@ -1132,18 +1108,18 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" memoize: dependency: transitive description: @@ -1156,10 +1132,10 @@ packages: dependency: "direct main" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.10.0" mime: dependency: transitive description: @@ -1260,10 +1236,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_parsing: dependency: transitive description: @@ -1380,10 +1356,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: @@ -1420,10 +1396,10 @@ packages: dependency: "direct overridden" description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "4.2.4" protobuf: dependency: transitive description: @@ -1933,10 +1909,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "11.10.0" wakelock: dependency: "direct main" description: @@ -2030,10 +2006,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.2" webkit_inspection_protocol: dependency: transitive description: @@ -2071,10 +2047,10 @@ packages: dependency: "direct overridden" description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "0.2.0+3" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 54be2dc06..0d1c13426 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -253,7 +253,8 @@ dependency_overrides: analyzer: ^5.2.0 pinenacl: ^0.3.3 xdg_directories: ^0.2.0 - flutter_local_notifications_linux: ^0.5.0+1 + # flutter_local_notifications_linux: ^0.5.0+1 # Overridden by Solana's package (from espresso_cash + # _public). Disabled for compatibility reasons, may affect Linux desktop notifications. process: ^4.0.0 file: ^6.0.0 http: ^0.13.0 From 4f9eae7169f7c96527a20b7a23b90870b2233188 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 19 Apr 2024 16:05:24 -0500 Subject: [PATCH 133/272] pass proxyInfo to Solana RpcClient if Tor is enabled --- lib/wallets/wallet/impl/solana_wallet.dart | 33 ++++++++-------------- pubspec.lock | 15 +++++----- pubspec.yaml | 6 +++- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 1808238c9..93e69254a 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:isar/isar.dart'; import 'package:solana/dto.dart'; import 'package:solana/solana.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' as isar; @@ -29,7 +28,6 @@ class SolanaWallet extends Bip39Wallet { NodeModel? _solNode; - ElectrumXClient? electrumXClient; // Used for Tor. RpcClient? rpcClient; // The Solana RpcClient. Future _getKeyPair() async { @@ -212,8 +210,8 @@ class SolanaWallet extends Bip39Wallet { @override Future pingCheck() { try { - var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - rpcClient.getHealth(); + _checkClient(); + rpcClient?.getHealth(); return Future.value(true); } catch (e, s) { Logging.instance.log( @@ -414,24 +412,17 @@ class SolanaWallet extends Bip39Wallet { return Future.value(false); } - /// Check that the ElectrumXClient is active and usable by a Solana RpcClient. - Future _checkClients() async { + /// Make sure the Solana RpcClient uses Tor if it's enabled. + /// + /// TODO: Make synchronous. + Future _checkClient() async { if (prefs.useTor) { - electrumXClient ??= ElectrumXClient( - host: getCurrentNode().host, - port: getCurrentNode().port, - useSSL: getCurrentNode().useSSL, - failovers: [], - prefs: prefs, - coin: Coin.solana, - ); - int torPort = electrumXClient?.rpcClient?.proxyInfo?.port ?? - TorService.sharedInstance.getProxyInfo().port; - rpcClient = RpcClient("${InternetAddress.loopbackIPv4}:$torPort"); - } else { - rpcClient = - RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); - } + final ({InternetAddress host, int port}) proxyInfo = + TorService.sharedInstance.getProxyInfo(); + // If Tor is enabled, pass the optional proxyInfo to the Solana RpcClient. + rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}", + proxyInfo: {'host': proxyInfo.host, 'port': proxyInfo.port}); + } else {} return; } } diff --git a/pubspec.lock b/pubspec.lock index 89aa98402..ad0b25a45 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1545,10 +1545,10 @@ packages: dependency: "direct main" description: name: socks5_proxy - sha256: e0cba6917cd374de6f6cb0ce081e50e6efc24c61644b8e9f20c8bf8b91bb0b75 + sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" url: "https://pub.dev" source: hosted - version: "1.0.3+dev.3" + version: "1.0.4" socks_socket: dependency: "direct main" description: @@ -1561,11 +1561,12 @@ packages: solana: dependency: "direct main" description: - name: solana - sha256: "99a6a40a847f57ccf4687a730413d67fcaef4fc6778ddd9c3258e7fe8e4c6743" - url: "https://pub.dev" - source: hosted - version: "0.30.3" + path: "packages/solana" + ref: "2d7189d31f1bfd5d6779268c81a897f03f339f5d" + resolved-ref: "2d7189d31f1bfd5d6779268c81a897f03f339f5d" + url: "https://github.com/cypherstack/espresso-cash-public.git" + source: git + version: "0.30.4" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0d1c13426..6df07fc23 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -177,7 +177,11 @@ dependencies: url: https://github.com/cypherstack/electrum_adapter.git ref: 9e9441fc1e9ace8907256fff05fe2c607b0933b6 stream_channel: ^2.1.0 - solana: ^0.30.3 + solana: + git: # TODO: Revert to official package once Tor support is merged upstream. + url: https://github.com/cypherstack/espresso-cash-public.git + ref: 2d7189d31f1bfd5d6779268c81a897f03f339f5d # tor branch. + path: packages/solana dev_dependencies: flutter_test: From 93a3a3f9c19eba444ee870b00f6dead660ba809b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 19 Apr 2024 16:05:30 -0500 Subject: [PATCH 134/272] cleanup redundant comments --- lib/wallets/wallet/impl/solana_wallet.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 93e69254a..96ec9bcc4 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -48,7 +48,7 @@ class SolanaWallet extends Bip39Wallet { } Future _getCurrentBalanceInLamports() async { - await _checkClients(); // Check electrumXClient and rpcClient. + await _checkClient(); var balance = await rpcClient?.getBalance((await _getKeyPair()).address); return balance!.value; } @@ -83,7 +83,7 @@ class SolanaWallet extends Bip39Wallet { @override Future prepareSend({required TxData txData}) async { try { - await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + await _checkClient(); if (txData.recipients == null || txData.recipients!.length != 1) { throw Exception("$runtimeType prepareSend requires 1 recipient"); @@ -142,7 +142,7 @@ class SolanaWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { try { - await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + await _checkClient(); final keyPair = await _getKeyPair(); var recipientAccount = txData.recipients!.first; @@ -174,7 +174,7 @@ class SolanaWallet extends Bip39Wallet { @override Future estimateFeeFor(Amount amount, int feeRate) async { - await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + await _checkClient(); if (info.cachedBalance.spendable.raw == BigInt.zero) { return Amount( @@ -194,7 +194,7 @@ class SolanaWallet extends Bip39Wallet { @override Future get fees async { - await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + await _checkClient(); final fees = await rpcClient?.getFees(); // TODO [prio=low]: handle null fees. @@ -251,7 +251,7 @@ class SolanaWallet extends Bip39Wallet { @override Future updateBalance() async { try { - await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + await _checkClient(); var balance = await rpcClient?.getBalance(info.cachedReceivingAddress); @@ -297,7 +297,7 @@ class SolanaWallet extends Bip39Wallet { @override Future updateChainHeight() async { try { - await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + await _checkClient(); int blockHeight = await rpcClient?.getSlot() ?? 0; // TODO [prio=low]: Revisit null condition. @@ -332,7 +332,7 @@ class SolanaWallet extends Bip39Wallet { @override Future updateTransactions() async { try { - await _checkClients(); // Check ElectrumXClient and Solana RpcClient. + await _checkClient(); var transactionsList = await rpcClient?.getTransactionsList( (await _getKeyPair()).publicKey, From 9a1e622d652add0aad781b2dccc520fb61f0f7d7 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 19 Apr 2024 16:27:33 -0500 Subject: [PATCH 135/272] pass raw String address instead of full InternetAddress and update tor troubleshooting docs to be more specific --- docs/building.md | 2 +- lib/wallets/wallet/impl/solana_wallet.dart | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/building.md b/docs/building.md index 13cf5c03f..d4d3ad6f4 100644 --- a/docs/building.md +++ b/docs/building.md @@ -205,4 +205,4 @@ Run with `-v` or `--verbose` to see a more detailed error. Certain exceptions ( ## 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. +To test Tor usage, run Stack Wallet from Android Studio. Click the Flutter DevTools icon in the Run tab (next to the Hot Reload and Hot Restart buttons) and navigate to the Network tab. Connections using Tor will show as `GET InternetAddress('127.0.0.1', IPv4) 101 ws`. Connections outside of Tor will show the destination address directly (although some Tor requests may also show the destination address directly, check the Headers take for *eg.* `{localPort: 59940, remoteAddress: 127.0.0.1, remotePort: 6725}`. `localPort` should match your Tor port. diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 96ec9bcc4..2dd21b7ca 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -421,8 +421,11 @@ class SolanaWallet extends Bip39Wallet { TorService.sharedInstance.getProxyInfo(); // If Tor is enabled, pass the optional proxyInfo to the Solana RpcClient. rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}", - proxyInfo: {'host': proxyInfo.host, 'port': proxyInfo.port}); - } else {} + proxyInfo: {'host': proxyInfo.host.address, 'port': proxyInfo.port}); + } else { + rpcClient ??= + RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + } return; } } From bd017f835416891b42e566875a33d7bd6641b8f4 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 15:42:47 -0600 Subject: [PATCH 136/272] address type dropdown styling --- lib/pages/receive_view/receive_view.dart | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 1c2897e34..fcd6dcfef 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -325,6 +325,17 @@ class _ReceiveViewState extends ConsumerState { builder: (child) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Text( + "Address type", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + const SizedBox( + height: 10, + ), DropdownButtonHideUnderline( child: DropdownButton2( value: _showSparkAddress, @@ -333,14 +344,14 @@ class _ReceiveViewState extends ConsumerState { value: true, child: Text( "Spark address", - style: STextStyles.desktopTextMedium(context), + style: STextStyles.w500_14(context), ), ), DropdownMenuItem( value: false, child: Text( "Transparent address", - style: STextStyles.desktopTextMedium(context), + style: STextStyles.w500_14(context), ), ), ], @@ -365,6 +376,16 @@ class _ReceiveViewState extends ConsumerState { ), ), ), + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, @@ -598,7 +619,7 @@ class _ReceiveViewState extends ConsumerState { height: 20, ), CustomTextButton( - text: "Create new QR code", + text: "Advanced options", onTap: () async { unawaited(Navigator.of(context).push( RouteGenerator.getRoute( From c0a8829336c6eba6052b628c356c03cf07d7a46f Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Tue, 23 Apr 2024 15:51:10 -0600 Subject: [PATCH 137/272] Update version (v2.0.0, build 219) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6df07fc23..82ee229d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.10.4+217 +version: 2.0.0+219 environment: sdk: ">=3.0.2 <4.0.0" From 32809a93fa0335ca93035d2786cc4df8894418be Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 15:52:53 -0600 Subject: [PATCH 138/272] fix error logging and update pubspec.lock --- lib/services/price.dart | 2 +- pubspec.lock | 56 +++++++++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/lib/services/price.dart b/lib/services/price.dart index 296537cb4..40c19f16e 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -215,7 +215,7 @@ class PriceAPI { } catch (e, s) { // only log the error as we don't want to interrupt the rest of the loop Logging.instance.log( - "getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddress): $e\n$s\nRESPONSE: $coinGeckoResponse.body", + "getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddress): $e\n$s\nRESPONSE: ${coinGeckoResponse.body}", level: LogLevel.Warning, ); } diff --git a/pubspec.lock b/pubspec.lock index ad0b25a45..b680e69f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -330,10 +330,10 @@ packages: dependency: transitive description: name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" url: "https://pub.dev" source: hosted - version: "1.6.4" + version: "1.7.2" cross_file: dependency: transitive description: @@ -1065,6 +1065,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lelantus: dependency: "direct main" description: @@ -1108,18 +1132,18 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" memoize: dependency: transitive description: @@ -1132,10 +1156,10 @@ packages: dependency: "direct main" description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -1236,10 +1260,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1356,10 +1380,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -1910,10 +1934,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" wakelock: dependency: "direct main" description: @@ -2007,10 +2031,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: From f8b9c64ef0134bd08ec1d4527a104827bad4f9de Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 16:04:29 -0600 Subject: [PATCH 139/272] possible bugfix --- lib/electrumx_rpc/electrumx_client.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index dd64d64ef..768b90fc5 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -230,7 +230,7 @@ class ElectrumXClient { // If the current ElectrumAdapterClient is closed, create a new one. if (getElectrumAdapter() != null && getElectrumAdapter()!.peer.isClosed) { _electrumAdapterChannel = null; - ClientManager.sharedInstance.remove(cryptoCurrency: cryptoCurrency); + await ClientManager.sharedInstance.remove(cryptoCurrency: cryptoCurrency); } final String useHost; From 249a01df753a75de2d1ca746e86e89661e4ed431 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 16:58:51 -0600 Subject: [PATCH 140/272] wait before setting rust version for frostdart --- scripts/ios/build_all.sh | 4 ++-- scripts/macos/build_all.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index db806c3bb..8b03d1b6f 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -16,8 +16,8 @@ rustup target add x86_64-apple-ios (cd ../../crypto_plugins/flutter_liblelantus/scripts/ios && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh ) & -set_rust_to_1720 & +(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh ) && +set_rust_to_1720 && (cd ../../crypto_plugins/frostdart/scripts/ios && ./build_all.sh ) & wait diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh index 53d6f9bac..59d1425c9 100755 --- a/scripts/macos/build_all.sh +++ b/scripts/macos/build_all.sh @@ -8,8 +8,8 @@ set_rust_to_1671 (cd ../../crypto_plugins/flutter_liblelantus/scripts/macos && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh ) & -set_rust_to_1720 & +(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh ) && +set_rust_to_1720 && (cd ../../crypto_plugins/frostdart/scripts/macos && ./build_all.sh ) & wait From 87bd3fc8ab0230563b84242f3941ee3e86497816 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 16:59:10 -0600 Subject: [PATCH 141/272] remove unused deps --- pubspec.lock | 18 +++++++++--------- pubspec.yaml | 6 +----- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index b680e69f2..4e4d39cb7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -604,13 +604,13 @@ packages: source: hosted version: "2.1.0" file: - dependency: "direct overridden" + dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -1417,13 +1417,13 @@ packages: source: hosted version: "1.2.0-beta-1" process: - dependency: "direct overridden" + dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" protobuf: dependency: transitive description: @@ -2069,13 +2069,13 @@ packages: source: git version: "0.1.0" xdg_directories: - dependency: "direct overridden" + dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 82ee229d5..0b8d551c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -256,12 +256,8 @@ dependency_overrides: crypto: 3.0.2 analyzer: ^5.2.0 pinenacl: ^0.3.3 - xdg_directories: ^0.2.0 - # flutter_local_notifications_linux: ^0.5.0+1 # Overridden by Solana's package (from espresso_cash - # _public). Disabled for compatibility reasons, may affect Linux desktop notifications. - process: ^4.0.0 - file: ^6.0.0 http: ^0.13.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 6c4f266a1caabfb7983f5b59f500f63619c8a69b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 23 Apr 2024 18:01:27 -0500 Subject: [PATCH 142/272] pass proxy info to Tezos as appropriate --- lib/wallets/wallet/impl/tezos_wallet.dart | 12 +++-- pubspec.lock | 60 ++++++++++++++++------- pubspec.yaml | 2 +- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index d1df08502..bdf3374a8 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:isar/isar.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; @@ -5,6 +7,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -105,9 +108,12 @@ class TezosWallet extends Bip39Wallet { // print("COUNTER: $counter"); // print("customFee: $customFee"); // } - final tezartClient = tezart.TezartClient( - server, - ); + ({InternetAddress host, int port})? proxyInfo = + prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null; + final tezartClient = tezart.TezartClient(server, + proxy: proxyInfo != null + ? "socks5://${proxyInfo.host}:${proxyInfo.port};" + : null); final opList = await tezartClient.transferOperation( source: sourceKeyStore, diff --git a/pubspec.lock b/pubspec.lock index ad0b25a45..372869a27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -330,10 +330,10 @@ packages: dependency: transitive description: name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" url: "https://pub.dev" source: hosted - version: "1.6.4" + version: "1.7.2" cross_file: dependency: transitive description: @@ -1065,6 +1065,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lelantus: dependency: "direct main" description: @@ -1108,18 +1132,18 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" memoize: dependency: transitive description: @@ -1132,10 +1156,10 @@ packages: dependency: "direct main" description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -1236,10 +1260,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1356,10 +1380,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -1716,8 +1740,8 @@ packages: dependency: "direct main" description: path: "." - ref: "8a7070f533e63dd150edae99476f6853bfb25913" - resolved-ref: "8a7070f533e63dd150edae99476f6853bfb25913" + ref: "9d4f326b19ef6ab51a40038ee2ddd3296454f228" + resolved-ref: "9d4f326b19ef6ab51a40038ee2ddd3296454f228" url: "https://github.com/cypherstack/tezart.git" source: git version: "2.0.5" @@ -1910,10 +1934,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" wakelock: dependency: "direct main" description: @@ -2007,10 +2031,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6df07fc23..f77a92b5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -166,7 +166,7 @@ dependencies: tezart: git: url: https://github.com/cypherstack/tezart.git - ref: 8a7070f533e63dd150edae99476f6853bfb25913 + ref: 1fb2669e2b530367a449217e952f220d5e667043 socks5_proxy: ^1.0.3+dev.3 convert: ^3.1.1 flutter_hooks: ^0.20.3 From bb4fc8662947935f76889e6c7f3ffa1f6b357af0 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 23 Apr 2024 17:10:58 -0600 Subject: [PATCH 143/272] update flutter min version --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0b8d551c2..16db28f97 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,8 +14,8 @@ description: Stack Wallet version: 2.0.0+219 environment: - sdk: ">=3.0.2 <4.0.0" - flutter: ^3.16.0 + sdk: ">=3.3.3 <4.0.0" + flutter: ^3.19.5 dependencies: flutter: From 2feb7d0be3fb85c6db270f2a12f0ef8e5ef0334d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 23 Apr 2024 22:03:46 -0500 Subject: [PATCH 144/272] update Solana Tor support to use method amenable to upstream merge --- lib/wallets/wallet/impl/solana_wallet.dart | 23 +++++++++++++++------- pubspec.lock | 4 ++-- pubspec.yaml | 4 ++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 2dd21b7ca..8fd4a8435 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math'; import 'package:isar/isar.dart'; +import 'package:socks5_proxy/socks_client.dart'; import 'package:solana/dto.dart'; import 'package:solana/solana.dart'; import 'package:stackwallet/models/balance.dart'; @@ -414,18 +415,26 @@ class SolanaWallet extends Bip39Wallet { /// Make sure the Solana RpcClient uses Tor if it's enabled. /// - /// TODO: Make synchronous. + /// TODO [prio=low]: Make synchronous. Future _checkClient() async { + HttpClient? httpClient; + if (prefs.useTor) { + // Make proxied HttpClient. final ({InternetAddress host, int port}) proxyInfo = TorService.sharedInstance.getProxyInfo(); - // If Tor is enabled, pass the optional proxyInfo to the Solana RpcClient. - rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}", - proxyInfo: {'host': proxyInfo.host.address, 'port': proxyInfo.port}); - } else { - rpcClient ??= - RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); + + final proxySettings = ProxySettings(proxyInfo.host, proxyInfo.port); + httpClient = HttpClient(); + SocksTCPClient.assignToHttpClient(httpClient, [proxySettings]); } + + rpcClient = RpcClient( + "${getCurrentNode().host}:${getCurrentNode().port}", + timeout: const Duration(seconds: 30), + customHeaders: {}, + httpClient: httpClient, + ); return; } } diff --git a/pubspec.lock b/pubspec.lock index 4e4d39cb7..0791b14ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1586,8 +1586,8 @@ packages: dependency: "direct main" description: path: "packages/solana" - ref: "2d7189d31f1bfd5d6779268c81a897f03f339f5d" - resolved-ref: "2d7189d31f1bfd5d6779268c81a897f03f339f5d" + ref: "5a392a44efe10d144933fd418a92ddf3cd331558" + resolved-ref: "5a392a44efe10d144933fd418a92ddf3cd331558" url: "https://github.com/cypherstack/espresso-cash-public.git" source: git version: "0.30.4" diff --git a/pubspec.yaml b/pubspec.yaml index 16db28f97..0fe617edc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -178,9 +178,9 @@ dependencies: ref: 9e9441fc1e9ace8907256fff05fe2c607b0933b6 stream_channel: ^2.1.0 solana: - git: # TODO: Revert to official package once Tor support is merged upstream. + git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. url: https://github.com/cypherstack/espresso-cash-public.git - ref: 2d7189d31f1bfd5d6779268c81a897f03f339f5d # tor branch. + ref: 0ada1f775c2a2c815de640424270a229f5e91e2f path: packages/solana dev_dependencies: From 50a1b33e3236554b3079bc0c474debd422f5bb69 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 09:36:27 -0600 Subject: [PATCH 145/272] make `_rpcClient` private and `_checkClient` synchronous --- lib/wallets/wallet/impl/solana_wallet.dart | 47 +++++++++++----------- pubspec.lock | 12 +++--- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 8fd4a8435..d98044e57 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -29,7 +29,7 @@ class SolanaWallet extends Bip39Wallet { NodeModel? _solNode; - RpcClient? rpcClient; // The Solana RpcClient. + RpcClient? _rpcClient; // The Solana RpcClient. Future _getKeyPair() async { return Ed25519HDKeyPair.fromMnemonic(await getMnemonic(), @@ -49,8 +49,8 @@ class SolanaWallet extends Bip39Wallet { } Future _getCurrentBalanceInLamports() async { - await _checkClient(); - var balance = await rpcClient?.getBalance((await _getKeyPair()).address); + _checkClient(); + var balance = await _rpcClient?.getBalance((await _getKeyPair()).address); return balance!.value; } @@ -84,7 +84,7 @@ class SolanaWallet extends Bip39Wallet { @override Future prepareSend({required TxData txData}) async { try { - await _checkClient(); + _checkClient(); if (txData.recipients == null || txData.recipients!.length != 1) { throw Exception("$runtimeType prepareSend requires 1 recipient"); @@ -113,8 +113,8 @@ class SolanaWallet extends Bip39Wallet { // Rent exemption of Solana final accInfo = - await rpcClient?.getAccountInfo((await _getKeyPair()).address); - int minimumRent = await rpcClient?.getMinimumBalanceForRentExemption( + await _rpcClient?.getAccountInfo((await _getKeyPair()).address); + int minimumRent = await _rpcClient?.getMinimumBalanceForRentExemption( accInfo!.value!.data.toString().length) ?? 0; // TODO revisit null condition. if (minimumRent > @@ -143,7 +143,7 @@ class SolanaWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { try { - await _checkClient(); + _checkClient(); final keyPair = await _getKeyPair(); var recipientAccount = txData.recipients!.first; @@ -160,7 +160,7 @@ class SolanaWallet extends Bip39Wallet { ], ); - final txid = await rpcClient?.signAndSendTransaction(message, [keyPair]); + final txid = await _rpcClient?.signAndSendTransaction(message, [keyPair]); return txData.copyWith( txid: txid, ); @@ -175,7 +175,7 @@ class SolanaWallet extends Bip39Wallet { @override Future estimateFeeFor(Amount amount, int feeRate) async { - await _checkClient(); + _checkClient(); if (info.cachedBalance.spendable.raw == BigInt.zero) { return Amount( @@ -184,7 +184,7 @@ class SolanaWallet extends Bip39Wallet { ); } - final fee = await rpcClient?.getFees(); + final fee = await _rpcClient?.getFees(); // TODO [prio=low]: handle null fee. return Amount( @@ -195,9 +195,9 @@ class SolanaWallet extends Bip39Wallet { @override Future get fees async { - await _checkClient(); + _checkClient(); - final fees = await rpcClient?.getFees(); + final fees = await _rpcClient?.getFees(); // TODO [prio=low]: handle null fees. return FeeObject( numberOfBlocksFast: 1, @@ -212,7 +212,7 @@ class SolanaWallet extends Bip39Wallet { Future pingCheck() { try { _checkClient(); - rpcClient?.getHealth(); + _rpcClient?.getHealth(); return Future.value(true); } catch (e, s) { Logging.instance.log( @@ -252,16 +252,16 @@ class SolanaWallet extends Bip39Wallet { @override Future updateBalance() async { try { - await _checkClient(); + _checkClient(); - var balance = await rpcClient?.getBalance(info.cachedReceivingAddress); + var balance = await _rpcClient?.getBalance(info.cachedReceivingAddress); // Rent exemption of Solana final accInfo = - await rpcClient?.getAccountInfo((await _getKeyPair()).address); + await _rpcClient?.getAccountInfo((await _getKeyPair()).address); // TODO [prio=low]: handle null account info. final int minimumRent = - await rpcClient?.getMinimumBalanceForRentExemption( + await _rpcClient?.getMinimumBalanceForRentExemption( accInfo!.value!.data.toString().length) ?? 0; // TODO [prio=low]: revisit null condition. @@ -298,9 +298,9 @@ class SolanaWallet extends Bip39Wallet { @override Future updateChainHeight() async { try { - await _checkClient(); + _checkClient(); - int blockHeight = await rpcClient?.getSlot() ?? 0; + int blockHeight = await _rpcClient?.getSlot() ?? 0; // TODO [prio=low]: Revisit null condition. await info.updateCachedChainHeight( @@ -333,9 +333,9 @@ class SolanaWallet extends Bip39Wallet { @override Future updateTransactions() async { try { - await _checkClient(); + _checkClient(); - var transactionsList = await rpcClient?.getTransactionsList( + var transactionsList = await _rpcClient?.getTransactionsList( (await _getKeyPair()).publicKey, encoding: Encoding.jsonParsed); var txsList = @@ -415,8 +415,7 @@ class SolanaWallet extends Bip39Wallet { /// Make sure the Solana RpcClient uses Tor if it's enabled. /// - /// TODO [prio=low]: Make synchronous. - Future _checkClient() async { + void _checkClient() async { HttpClient? httpClient; if (prefs.useTor) { @@ -429,7 +428,7 @@ class SolanaWallet extends Bip39Wallet { SocksTCPClient.assignToHttpClient(httpClient, [proxySettings]); } - rpcClient = RpcClient( + _rpcClient = RpcClient( "${getCurrentNode().host}:${getCurrentNode().port}", timeout: const Duration(seconds: 30), customHeaders: {}, diff --git a/pubspec.lock b/pubspec.lock index 3a9317065..25ba923a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1586,8 +1586,8 @@ packages: dependency: "direct main" description: path: "packages/solana" - ref: "5a392a44efe10d144933fd418a92ddf3cd331558" - resolved-ref: "5a392a44efe10d144933fd418a92ddf3cd331558" + ref: "0ada1f775c2a2c815de640424270a229f5e91e2f" + resolved-ref: "0ada1f775c2a2c815de640424270a229f5e91e2f" url: "https://github.com/cypherstack/espresso-cash-public.git" source: git version: "0.30.4" @@ -1740,8 +1740,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9d4f326b19ef6ab51a40038ee2ddd3296454f228" - resolved-ref: "9d4f326b19ef6ab51a40038ee2ddd3296454f228" + ref: "1fb2669e2b530367a449217e952f220d5e667043" + resolved-ref: "1fb2669e2b530367a449217e952f220d5e667043" url: "https://github.com/cypherstack/tezart.git" source: git version: "2.0.5" @@ -2109,5 +2109,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.3 <4.0.0" + flutter: ">=3.19.5" From 0fdc4c76a35f169e45c5394bcd95abc6fd3cd433 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 09:46:28 -0600 Subject: [PATCH 146/272] new linter rules --- analysis_options.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index c5b4136b6..cc16ffa53 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -90,6 +90,8 @@ linter: unawaited_futures: true avoid_double_and_int_checks: false constant_identifier_names: false + prefer_final_locals: true + prefer_final_in_for_each: true # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule From f7cadcdc6298dc7b6bc67f27f0b518e7159f8e06 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 09:47:35 -0600 Subject: [PATCH 147/272] use final where possible in sol wallet --- lib/wallets/wallet/impl/solana_wallet.dart | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index d98044e57..b9b068ee4 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -37,7 +37,7 @@ class SolanaWallet extends Bip39Wallet { } Future
_getCurrentAddress() async { - var addressStruct = Address( + final addressStruct = Address( walletId: walletId, value: (await _getKeyPair()).address, publicKey: List.empty(), @@ -50,7 +50,7 @@ class SolanaWallet extends Bip39Wallet { Future _getCurrentBalanceInLamports() async { _checkClient(); - var balance = await _rpcClient?.getBalance((await _getKeyPair()).address); + final balance = await _rpcClient?.getBalance((await _getKeyPair()).address); return balance!.value; } @@ -61,7 +61,7 @@ class SolanaWallet extends Bip39Wallet { @override Future checkSaveInitialReceivingAddress() async { try { - var address = (await _getKeyPair()).address; + final address = (await _getKeyPair()).address; await mainDB.updateOrPutAddresses([ Address( @@ -90,14 +90,14 @@ class SolanaWallet extends Bip39Wallet { throw Exception("$runtimeType prepareSend requires 1 recipient"); } - Amount sendAmount = txData.amount!; + final Amount sendAmount = txData.amount!; if (sendAmount > info.cachedBalance.spendable) { throw Exception("Insufficient available balance"); } int feeAmount; - var currentFees = await fees; + final currentFees = await fees; switch (txData.feeRateType) { case FeeRateType.fast: feeAmount = currentFees.fast; @@ -114,9 +114,10 @@ class SolanaWallet extends Bip39Wallet { // Rent exemption of Solana final accInfo = await _rpcClient?.getAccountInfo((await _getKeyPair()).address); - int minimumRent = await _rpcClient?.getMinimumBalanceForRentExemption( - accInfo!.value!.data.toString().length) ?? - 0; // TODO revisit null condition. + final int minimumRent = + await _rpcClient?.getMinimumBalanceForRentExemption( + accInfo!.value!.data.toString().length) ?? + 0; // TODO revisit null condition. if (minimumRent > ((await _getCurrentBalanceInLamports()) - txData.amount!.raw.toInt() - @@ -146,8 +147,8 @@ class SolanaWallet extends Bip39Wallet { _checkClient(); final keyPair = await _getKeyPair(); - var recipientAccount = txData.recipients!.first; - var recipientPubKey = + final recipientAccount = txData.recipients!.first; + final recipientPubKey = Ed25519HDPublicKey.fromBase58(recipientAccount.address); final message = Message( instructions: [ @@ -230,7 +231,7 @@ class SolanaWallet extends Bip39Wallet { @override Future recover({required bool isRescan}) async { await refreshMutex.protect(() async { - var addressStruct = await _getCurrentAddress(); + final addressStruct = await _getCurrentAddress(); await mainDB.updateOrPutAddresses([addressStruct]); @@ -254,7 +255,7 @@ class SolanaWallet extends Bip39Wallet { try { _checkClient(); - var balance = await _rpcClient?.getBalance(info.cachedReceivingAddress); + final balance = await _rpcClient?.getBalance(info.cachedReceivingAddress); // Rent exemption of Solana final accInfo = @@ -265,7 +266,7 @@ class SolanaWallet extends Bip39Wallet { accInfo!.value!.data.toString().length) ?? 0; // TODO [prio=low]: revisit null condition. - var spendableBalance = balance!.value - minimumRent; + final spendableBalance = balance!.value - minimumRent; final newBalance = Balance( total: Amount( @@ -300,7 +301,7 @@ class SolanaWallet extends Bip39Wallet { try { _checkClient(); - int blockHeight = await _rpcClient?.getSlot() ?? 0; + final int blockHeight = await _rpcClient?.getSlot() ?? 0; // TODO [prio=low]: Revisit null condition. await info.updateCachedChainHeight( @@ -335,21 +336,21 @@ class SolanaWallet extends Bip39Wallet { try { _checkClient(); - var transactionsList = await _rpcClient?.getTransactionsList( + final transactionsList = await _rpcClient?.getTransactionsList( (await _getKeyPair()).publicKey, encoding: Encoding.jsonParsed); - var txsList = + final txsList = List>.empty(growable: true); // TODO [prio=low]: Revisit null assertion below. for (final tx in transactionsList!) { - var senderAddress = + final senderAddress = (tx.transaction as ParsedTransaction).message.accountKeys[0].pubkey; - var receiverAddress = + final receiverAddress = (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; var txType = isar.TransactionType.unknown; - var txAmount = Amount( + final txAmount = Amount( rawValue: BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]), fractionDigits: cryptoCurrency.fractionDigits, @@ -364,7 +365,7 @@ class SolanaWallet extends Bip39Wallet { txType = isar.TransactionType.incoming; } - var transaction = isar.Transaction( + final transaction = isar.Transaction( walletId: walletId, txid: (tx.transaction as ParsedTransaction).signatures[0], timestamp: tx.blockTime!, @@ -384,7 +385,7 @@ class SolanaWallet extends Bip39Wallet { numberOfMessages: 0, ); - var txAddress = Address( + final txAddress = Address( walletId: walletId, value: receiverAddress, publicKey: List.empty(), From af87ec76efa62d8a3dfee6d78603f440001e8423 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 15:00:36 -0600 Subject: [PATCH 148/272] build runner run updates --- .../isar/models/blockchain_data/address.dart | 4 +- .../models/blockchain_data/address.g.dart | 2 + lib/wallets/isar/models/wallet_info.g.dart | 2 + test/cached_electrumx_test.mocks.dart | 304 +- test/electrumx_test.dart | 3556 ++++++++--------- test/electrumx_test.mocks.dart | 771 ---- test/json_rpc_test.dart | 67 - .../pages/send_view/send_view_test.mocks.dart | 13 + .../exchange/exchange_view_test.mocks.dart | 13 + ...allet_settings_view_screen_test.mocks.dart | 175 +- .../bitcoin/bitcoin_wallet_test.mocks.dart | 90 +- .../bitcoincash_wallet_test.mocks.dart | 90 +- .../dogecoin/dogecoin_wallet_test.mocks.dart | 90 +- .../namecoin/namecoin_wallet_test.mocks.dart | 90 +- .../particl/particl_wallet_test.mocks.dart | 90 +- .../managed_favorite_test.mocks.dart | 13 + .../node_options_sheet_test.mocks.dart | 13 + .../transaction_card_test.mocks.dart | 13 + 18 files changed, 2266 insertions(+), 3130 deletions(-) delete mode 100644 test/electrumx_test.mocks.dart delete mode 100644 test/json_rpc_test.dart diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index a5bd64431..76d69f104 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -164,8 +164,8 @@ enum AddressType { stellar, tezos, frostMS, - solana, - p2tr; + p2tr, + solana; String get readableName { switch (this) { diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index ae96a3ac9..7e78cbee8 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -268,6 +268,7 @@ const _AddresstypeEnumValueMap = { 'tezos': 12, 'frostMS': 13, 'p2tr': 14, + 'solana': 15, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -285,6 +286,7 @@ const _AddresstypeValueEnumMap = { 12: AddressType.tezos, 13: AddressType.frostMS, 14: AddressType.p2tr, + 15: AddressType.solana, }; Id _addressGetId(Address object) { diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index 5e0ed135f..bc0a5828e 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -267,6 +267,7 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'tezos': 12, 'frostMS': 13, 'p2tr': 14, + 'solana': 15, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -284,6 +285,7 @@ const _WalletInfomainAddressTypeValueEnumMap = { 12: AddressType.tezos, 13: AddressType.frostMS, 14: AddressType.p2tr, + 15: AddressType.solana, }; Id _walletInfoGetId(WalletInfo object) { diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index dcbb78453..1b032ceff 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -3,19 +3,21 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:ui' as _i11; +import 'dart:async' as _i6; +import 'dart:ui' as _i12; -import 'package:decimal/decimal.dart' as _i2; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i9; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i8; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i10; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i7; -import 'package:stackwallet/utilities/prefs.dart' as _i6; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i5; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i10; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i9; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i11; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i8; +import 'package:stackwallet/utilities/prefs.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' - as _i3; + as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -28,8 +30,9 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_i // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -38,8 +41,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -48,8 +51,18 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeFusionInfo_2 extends _i1.SmartFake implements _i3.FusionInfo { - _FakeFusionInfo_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFusionInfo_3 extends _i1.SmartFake implements _i4.FusionInfo { + _FakeFusionInfo_3( Object parent, Invocation parentInvocation, ) : super( @@ -61,13 +74,21 @@ class _FakeFusionInfo_2 extends _i1.SmartFake implements _i3.FusionInfo { /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i5.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -91,7 +112,7 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -112,16 +133,16 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: false, ) as bool); @override - _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + _i6.Future closeAdapter() => (super.noSuchMethod( Invocation.method( - #checkElectrumAdapter, + #closeAdapter, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future request({ + _i6.Future request({ required String? command, List? args = const [], String? requestID, @@ -140,10 +161,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future> batchRequest({ + _i6.Future> batchRequest({ required String? command, required List? args, Duration? requestTimeout = const Duration(seconds: 60), @@ -160,10 +181,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #retries: retries, }, ), - returnValue: _i5.Future>.value([]), - ) as _i5.Future>); + returnValue: _i6.Future>.value([]), + ) as _i6.Future>); @override - _i5.Future ping({ + _i6.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -176,10 +197,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i6.Future.value(false), + ) as _i6.Future); @override - _i5.Future> getBlockHeadTip({String? requestID}) => + _i6.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -187,10 +208,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future> getServerFeatures({String? requestID}) => + _i6.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -198,10 +219,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future broadcastTransaction({ + _i6.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -214,10 +235,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i5.Future.value(''), - ) as _i5.Future); + returnValue: _i6.Future.value(''), + ) as _i6.Future); @override - _i5.Future> getBalance({ + _i6.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -231,10 +252,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future>> getHistory({ + _i6.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -247,11 +268,11 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i5.Future>>.value( + returnValue: _i6.Future>>.value( >[]), - ) as _i5.Future>>); + ) as _i6.Future>>); @override - _i5.Future>>> getBatchHistory( + _i6.Future>>> getBatchHistory( {required List? args}) => (super.noSuchMethod( Invocation.method( @@ -259,11 +280,11 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { [], {#args: args}, ), - returnValue: _i5.Future>>>.value( + returnValue: _i6.Future>>>.value( >>[]), - ) as _i5.Future>>>); + ) as _i6.Future>>>); @override - _i5.Future>> getUTXOs({ + _i6.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -276,11 +297,11 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i5.Future>>.value( + returnValue: _i6.Future>>.value( >[]), - ) as _i5.Future>>); + ) as _i6.Future>>); @override - _i5.Future>>> getBatchUTXOs( + _i6.Future>>> getBatchUTXOs( {required List? args}) => (super.noSuchMethod( Invocation.method( @@ -288,11 +309,11 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { [], {#args: args}, ), - returnValue: _i5.Future>>>.value( + returnValue: _i6.Future>>>.value( >>[]), - ) as _i5.Future>>>); + ) as _i6.Future>>>); @override - _i5.Future> getTransaction({ + _i6.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -308,10 +329,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future> getLelantusAnonymitySet({ + _i6.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -327,10 +348,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future getLelantusMintData({ + _i6.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -343,10 +364,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future> getLelantusUsedCoinSerials({ + _i6.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -360,20 +381,20 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future getLelantusLatestCoinId({String? requestID}) => + _i6.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i5.Future.value(0), - ) as _i5.Future); + returnValue: _i6.Future.value(0), + ) as _i6.Future); @override - _i5.Future> getSparkAnonymitySet({ + _i6.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -389,10 +410,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future> getSparkUsedCoinsTags({ + _i6.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -405,10 +426,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i5.Future>.value({}), - ) as _i5.Future>); + returnValue: _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future>> getSparkMintMetaData({ + _i6.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -421,21 +442,21 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i5.Future>>.value( + returnValue: _i6.Future>>.value( >[]), - ) as _i5.Future>>); + ) as _i6.Future>>); @override - _i5.Future getSparkLatestCoinId({String? requestID}) => + _i6.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i5.Future.value(0), - ) as _i5.Future); + returnValue: _i6.Future.value(0), + ) as _i6.Future); @override - _i5.Future> getFeeRate({String? requestID}) => + _i6.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -443,10 +464,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future<_i2.Decimal> estimateFee({ + _i6.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -459,7 +480,7 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i6.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -470,15 +491,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i6.Future<_i3.Decimal>); @override - _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i6.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i6.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -486,13 +507,13 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i6.Future<_i3.Decimal>); } /// A class which mocks [Prefs]. /// /// See the documentation for Mockito's code generation for more information. -class MockPrefs extends _i1.Mock implements _i6.Prefs { +class MockPrefs extends _i1.Mock implements _i7.Prefs { MockPrefs() { _i1.throwOnMissingStub(this); } @@ -548,12 +569,12 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - _i7.SyncingType get syncType => (super.noSuchMethod( + _i8.SyncingType get syncType => (super.noSuchMethod( Invocation.getter(#syncType), - returnValue: _i7.SyncingType.currentWalletOnly, - ) as _i7.SyncingType); + returnValue: _i8.SyncingType.currentWalletOnly, + ) as _i8.SyncingType); @override - set syncType(_i7.SyncingType? syncType) => super.noSuchMethod( + set syncType(_i8.SyncingType? syncType) => super.noSuchMethod( Invocation.setter( #syncType, syncType, @@ -712,12 +733,12 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - _i8.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + _i9.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( Invocation.getter(#backupFrequencyType), - returnValue: _i8.BackupFrequencyType.everyTenMinutes, - ) as _i8.BackupFrequencyType); + returnValue: _i9.BackupFrequencyType.everyTenMinutes, + ) as _i9.BackupFrequencyType); @override - set backupFrequencyType(_i8.BackupFrequencyType? backupFrequencyType) => + set backupFrequencyType(_i9.BackupFrequencyType? backupFrequencyType) => super.noSuchMethod( Invocation.setter( #backupFrequencyType, @@ -863,6 +884,19 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override + bool get solanaEnabled => (super.noSuchMethod( + Invocation.getter(#solanaEnabled), + returnValue: false, + ) as bool); + @override + set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( + Invocation.setter( + #solanaEnabled, + solanaEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get frostEnabled => (super.noSuchMethod( Invocation.getter(#frostEnabled), returnValue: false, @@ -881,61 +915,61 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValue: false, ) as bool); @override - _i5.Future init() => (super.noSuchMethod( + _i6.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + _i6.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( Invocation.method( #incrementCurrentNotificationIndex, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future isExternalCallsSet() => (super.noSuchMethod( + _i6.Future isExternalCallsSet() => (super.noSuchMethod( Invocation.method( #isExternalCallsSet, [], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i6.Future.value(false), + ) as _i6.Future); @override - _i5.Future saveUserID(String? userId) => (super.noSuchMethod( + _i6.Future saveUserID(String? userId) => (super.noSuchMethod( Invocation.method( #saveUserID, [userId], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + _i6.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( Invocation.method( #saveSignupEpoch, [signupEpoch], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i9.AmountUnit amountUnit(_i10.Coin? coin) => (super.noSuchMethod( + _i10.AmountUnit amountUnit(_i11.Coin? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i9.AmountUnit.normal, - ) as _i9.AmountUnit); + returnValue: _i10.AmountUnit.normal, + ) as _i10.AmountUnit); @override void updateAmountUnit({ - required _i10.Coin? coin, - required _i9.AmountUnit? amountUnit, + required _i11.Coin? coin, + required _i10.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -949,7 +983,7 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - int maxDecimals(_i10.Coin? coin) => (super.noSuchMethod( + int maxDecimals(_i11.Coin? coin) => (super.noSuchMethod( Invocation.method( #maxDecimals, [coin], @@ -958,7 +992,7 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { ) as int); @override void updateMaxDecimals({ - required _i10.Coin? coin, + required _i11.Coin? coin, required int? maxDecimals, }) => super.noSuchMethod( @@ -973,23 +1007,23 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - _i3.FusionInfo getFusionServerInfo(_i10.Coin? coin) => (super.noSuchMethod( + _i4.FusionInfo getFusionServerInfo(_i11.Coin? coin) => (super.noSuchMethod( Invocation.method( #getFusionServerInfo, [coin], ), - returnValue: _FakeFusionInfo_2( + returnValue: _FakeFusionInfo_3( this, Invocation.method( #getFusionServerInfo, [coin], ), ), - ) as _i3.FusionInfo); + ) as _i4.FusionInfo); @override void setFusionServerInfo( - _i10.Coin? coin, - _i3.FusionInfo? fusionServerInfo, + _i11.Coin? coin, + _i4.FusionInfo? fusionServerInfo, ) => super.noSuchMethod( Invocation.method( @@ -1002,7 +1036,7 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - void addListener(_i11.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1010,7 +1044,7 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - void removeListener(_i11.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], diff --git a/test/electrumx_test.dart b/test/electrumx_test.dart index 64dc69a58..06ed6111d 100644 --- a/test/electrumx_test.dart +++ b/test/electrumx_test.dart @@ -1,1778 +1,1778 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; -import 'package:stackwallet/electrumx_rpc/rpc.dart'; -import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; -import 'package:stackwallet/services/tor_service.dart'; -import 'package:stackwallet/utilities/prefs.dart'; - -import 'electrumx_test.mocks.dart'; -import 'sample_data/get_anonymity_set_sample_data.dart'; -import 'sample_data/get_used_serials_sample_data.dart'; -import 'sample_data/transaction_data_samples.dart'; - -@GenerateMocks([JsonRPC, Prefs, TorService]) -void main() { - group("factory constructors and getters", () { - test("electrumxnode .from factory", () { - final nodeA = ElectrumXNode( - address: "some address", - port: 1, - name: "some name", - id: "some ID", - useSSL: true, - ); - - final nodeB = ElectrumXNode.from(nodeA); - - expect(nodeB.toString(), nodeA.toString()); - expect(nodeA == nodeB, false); - }); - - test("electrumx .from factory", () { - final node = ElectrumXNode( - address: "some address", - port: 1, - name: "some name", - id: "some ID", - useSSL: true, - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - - final client = ElectrumXClient.from( - node: node, - failovers: [], - prefs: mockPrefs, - torService: torService, - ); - - expect(client.useSSL, node.useSSL); - expect(client.host, node.address); - expect(client.port, node.port); - expect(client.rpcClient, null); - - verifyNoMoreInteractions(mockPrefs); - }); - }); - - test("Server error", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "error": { - "code": 1, - "message": "None should be a transaction hash", - }, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - failovers: [], - prefs: mockPrefs, - torService: torService, - ); - - expect(() => client.getTransaction(requestID: "some requestId", txHash: ''), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - group("getBlockHeadTip", () { - test("getBlockHeadTip success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.headers.subscribe"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": {"height": 520481, "hex": "some block hex string"}, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = - await (client.getBlockHeadTip(requestID: "some requestId")); - - expect(result["height"], 520481); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getBlockHeadTip throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.headers.subscribe"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.getBlockHeadTip(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("ping", () { - test("ping success", () async { - final mockClient = MockJsonRPC(); - const command = "server.ping"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 2), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": null, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.ping(requestID: "some requestId"); - - expect(result, true); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("ping throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "server.ping"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 2), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.ping(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getServerFeatures", () { - test("getServerFeatures success", () async { - final mockClient = MockJsonRPC(); - const command = "server.features"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": { - "genesis_hash": - "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", - "hosts": { - "0.0.0.0": {"tcp_port": 51001, "ssl_port": 51002} - }, - "protocol_max": "1.0", - "protocol_min": "1.0", - "pruning": null, - "server_version": "ElectrumX 1.0.17", - "hash_function": "sha256" - }, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = - await client.getServerFeatures(requestID: "some requestId"); - - expect(result, { - "genesis_hash": - "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", - "hosts": { - "0.0.0.0": {"tcp_port": 51001, "ssl_port": 51002} - }, - "protocol_max": "1.0", - "protocol_min": "1.0", - "pruning": null, - "server_version": "ElectrumX 1.0.17", - "hash_function": "sha256", - }); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getServerFeatures throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "server.features"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.getServerFeatures(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("broadcastTransaction", () { - test("broadcastTransaction success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.broadcast"; - const jsonArgs = '["some raw transaction string"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": "the txid of the rawtx", - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.broadcastTransaction( - rawTx: "some raw transaction string", requestID: "some requestId"); - - expect(result, "the txid of the rawtx"); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("broadcastTransaction throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.broadcast"; - const jsonArgs = '["some raw transaction string"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.broadcastTransaction( - rawTx: "some raw transaction string", - requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getBalance", () { - test("getBalance success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.get_balance"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": { - "confirmed": 103873966, - "unconfirmed": 23684400, - }, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getBalance( - scripthash: "dummy hash", requestID: "some requestId"); - - expect(result, {"confirmed": 103873966, "unconfirmed": 23684400}); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getBalance throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.get_balance"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getBalance( - scripthash: "dummy hash", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getHistory", () { - test("getHistory success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.get_history"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 5), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": [ - { - "height": 200004, - "tx_hash": - "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" - }, - { - "height": 215008, - "tx_hash": - "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" - } - ], - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getHistory( - scripthash: "dummy hash", requestID: "some requestId"); - - expect(result, [ - { - "height": 200004, - "tx_hash": - "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" - }, - { - "height": 215008, - "tx_hash": - "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" - } - ]); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getHistory throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.get_history"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 5), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getHistory( - scripthash: "dummy hash", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getUTXOs", () { - test("getUTXOs success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.listunspent"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": [ - { - "tx_pos": 0, - "value": 45318048, - "tx_hash": - "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", - "height": 437146 - }, - { - "tx_pos": 0, - "value": 919195, - "tx_hash": - "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", - "height": 441696 - } - ], - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getUTXOs( - scripthash: "dummy hash", requestID: "some requestId"); - - expect(result, [ - { - "tx_pos": 0, - "value": 45318048, - "tx_hash": - "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", - "height": 437146 - }, - { - "tx_pos": 0, - "value": 919195, - "tx_hash": - "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", - "height": 441696 - } - ]); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getUTXOs throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.listunspent"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getUTXOs( - scripthash: "dummy hash", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getTransaction", () { - test("getTransaction success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getTransaction throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getAnonymitySet", () { - test("getAnonymitySet success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getanonymityset"; - const jsonArgs = '["1",""]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": GetAnonymitySetSampleData.data, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusAnonymitySet( - groupId: "1", blockhash: "", requestID: "some requestId"); - - expect(result, GetAnonymitySetSampleData.data); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getAnonymitySet throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getanonymityset"; - const jsonArgs = '["1",""]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusAnonymitySet( - groupId: "1", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getMintData", () { - test("getMintData success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getmintmetadata"; - const jsonArgs = '["some mints"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": "mint meta data", - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusMintData( - mints: "some mints", requestID: "some requestId"); - - expect(result, "mint meta data"); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getMintData throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getmintmetadata"; - const jsonArgs = '["some mints"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusMintData( - mints: "some mints", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getUsedCoinSerials", () { - test("getUsedCoinSerials success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getusedcoinserials"; - const jsonArgs = '["0"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 2), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": GetUsedSerialsSampleData.serials, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusUsedCoinSerials( - requestID: "some requestId", startNumber: 0); - - expect(result, GetUsedSerialsSampleData.serials); - - verify(mockPrefs.wifiOnly).called(3); - verify(mockPrefs.useTor).called(3); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getUsedCoinSerials throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getusedcoinserials"; - const jsonArgs = '["0"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 2), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusUsedCoinSerials( - requestID: "some requestId", startNumber: 0), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getLatestCoinId", () { - test("getLatestCoinId success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getlatestcoinid"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": 1, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = - await client.getLelantusLatestCoinId(requestID: "some requestId"); - - expect(result, 1); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getLatestCoinId throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getlatestcoinid"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusLatestCoinId( - requestID: "some requestId", - ), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getCoinsForRecovery", () { - test("getCoinsForRecovery success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getanonymityset"; - const jsonArgs = '["1",""]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": GetAnonymitySetSampleData.data, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusAnonymitySet( - groupId: "1", blockhash: "", requestID: "some requestId"); - - expect(result, GetAnonymitySetSampleData.data); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getAnonymitySet throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getanonymityset"; - const jsonArgs = '["1",""]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusAnonymitySet( - groupId: "1", - requestID: "some requestId", - ), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getMintData", () { - test("getMintData success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getmintmetadata"; - const jsonArgs = '["some mints"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": "mint meta data", - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusMintData( - mints: "some mints", requestID: "some requestId"); - - expect(result, "mint meta data"); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getMintData throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getmintmetadata"; - const jsonArgs = '["some mints"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusMintData( - mints: "some mints", - requestID: "some requestId", - ), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getUsedCoinSerials", () { - test("getUsedCoinSerials success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getusedcoinserials"; - const jsonArgs = '["0"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 2), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": GetUsedSerialsSampleData.serials, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusUsedCoinSerials( - requestID: "some requestId", startNumber: 0); - - expect(result, GetUsedSerialsSampleData.serials); - - verify(mockPrefs.wifiOnly).called(3); - verify(mockPrefs.useTor).called(3); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getUsedCoinSerials throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getusedcoinserials"; - const jsonArgs = '["0"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 2), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusUsedCoinSerials( - requestID: "some requestId", startNumber: 0), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getLatestCoinId", () { - test("getLatestCoinId success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getlatestcoinid"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": 1, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = - await client.getLelantusLatestCoinId(requestID: "some requestId"); - - expect(result, 1); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getLatestCoinId throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getlatestcoinid"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.getLelantusLatestCoinId(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getFeeRate", () { - test("getFeeRate success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.getfeerate"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": { - "rate": 1000, - }, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getFeeRate(requestID: "some requestId"); - - expect(result, {"rate": 1000}); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getFeeRate throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.getfeerate"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.getFeeRate(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - test("rpcClient is null throws with bad server info", () { - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - client: null, - port: -10, - host: "_ :sa %", - useSSL: false, - prefs: mockPrefs, - torService: torService, - failovers: [], - ); - - expect(() => client.getFeeRate(), throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - group("Tor tests", () { - // useTor is false, so no TorService calls should be made. - test("Tor not in use", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when(mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - )).thenAnswer((_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId", - })); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => false); - when(mockPrefs.torKillSwitch) - .thenAnswer((_) => false); // Or true, shouldn't matter. - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.disconnected); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - failovers: [], - prefs: mockPrefs, - torService: mockTorService, - ); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNever(mockPrefs.torKillSwitch); - verifyNoMoreInteractions(mockPrefs); - verifyNever(mockTorService.status); - verifyNoMoreInteractions(mockTorService); - }); - - // useTor is true, but TorService is not enabled and the killswitch is off, so a clearnet call should be made. - test("Tor in use but Tor unavailable and killswitch off", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when(mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - )).thenAnswer((_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId", - })); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => true); - when(mockPrefs.torKillSwitch).thenAnswer((_) => false); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.disconnected); - when(mockTorService.getProxyInfo()).thenAnswer((_) => ( - host: InternetAddress('1.2.3.4'), - port: -1 - )); // Port is set to -1 until Tor is enabled. - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: mockTorService, - failovers: []); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verify(mockPrefs.torKillSwitch).called(1); - verifyNoMoreInteractions(mockPrefs); - verify(mockTorService.status).called(1); - verifyNever(mockTorService.getProxyInfo()); - verifyNoMoreInteractions(mockTorService); - }); - - // useTor is true and TorService is enabled, so a TorService call should be made. - test("Tor in use and available", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when(mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - )).thenAnswer((_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId", - })); - when(mockClient.proxyInfo) - .thenAnswer((_) => (host: InternetAddress('1.2.3.4'), port: 42)); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => true); - when(mockPrefs.torKillSwitch).thenAnswer((_) => false); // Or true. - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.connected); - when(mockTorService.getProxyInfo()) - .thenAnswer((_) => (host: InternetAddress('1.2.3.4'), port: 42)); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: mockTorService, - failovers: []); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockClient.proxyInfo).called(1); - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNever(mockPrefs.torKillSwitch); - verifyNoMoreInteractions(mockPrefs); - verify(mockTorService.status).called(1); - verify(mockTorService.getProxyInfo()).called(1); - verifyNoMoreInteractions(mockTorService); - }); - - // useTor is true, but TorService is not enabled and the killswitch is on, so no TorService calls should be made. - test("killswitch enabled", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "error": { - "code": 1, - "message": "None should be a transaction hash", - }, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => true); - when(mockPrefs.torKillSwitch).thenAnswer((_) => true); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.disconnected); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - failovers: [], - prefs: mockPrefs, - torService: mockTorService, - ); - - try { - var result = await client.getTransaction( - requestID: "some requestId", txHash: ''); - } catch (e) { - expect(e, isA()); - expect( - e.toString(), - equals( - "Exception: Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX")); - } - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verify(mockPrefs.torKillSwitch).called(1); - verifyNoMoreInteractions(mockPrefs); - verify(mockTorService.status).called(1); - verifyNoMoreInteractions(mockTorService); - }); - - // useTor is true but Tor is not enabled, but because the killswitch is off, a clearnet call should be made. - test("killswitch disabled", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => true); - when(mockPrefs.torKillSwitch).thenAnswer((_) => false); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.disconnected); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - failovers: [], - prefs: mockPrefs, - torService: mockTorService, - ); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verify(mockPrefs.torKillSwitch).called(1); - verifyNoMoreInteractions(mockPrefs); - verify(mockTorService.status).called(1); - verifyNoMoreInteractions(mockTorService); - }); - }); -} +// import 'dart:io'; +// +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/annotations.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +// import 'package:stackwallet/electrumx_rpc/rpc.dart'; +// import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +// import 'package:stackwallet/services/tor_service.dart'; +// import 'package:stackwallet/utilities/prefs.dart'; +// +// import 'electrumx_test.mocks.dart'; +// import 'sample_data/get_anonymity_set_sample_data.dart'; +// import 'sample_data/get_used_serials_sample_data.dart'; +// import 'sample_data/transaction_data_samples.dart'; +// +// @GenerateMocks([JsonRPC, Prefs, TorService]) +// void main() { +// group("factory constructors and getters", () { +// test("electrumxnode .from factory", () { +// final nodeA = ElectrumXNode( +// address: "some address", +// port: 1, +// name: "some name", +// id: "some ID", +// useSSL: true, +// ); +// +// final nodeB = ElectrumXNode.from(nodeA); +// +// expect(nodeB.toString(), nodeA.toString()); +// expect(nodeA == nodeB, false); +// }); +// +// test("electrumx .from factory", () { +// final node = ElectrumXNode( +// address: "some address", +// port: 1, +// name: "some name", +// id: "some ID", +// useSSL: true, +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// +// final client = ElectrumXClient.from( +// node: node, +// failovers: [], +// prefs: mockPrefs, +// torService: torService, +// ); +// +// expect(client.useSSL, node.useSSL); +// expect(client.host, node.address); +// expect(client.port, node.port); +// expect(client.rpcClient, null); +// +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// test("Server error", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "error": { +// "code": 1, +// "message": "None should be a transaction hash", +// }, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// failovers: [], +// prefs: mockPrefs, +// torService: torService, +// ); +// +// expect(() => client.getTransaction(requestID: "some requestId", txHash: ''), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// group("getBlockHeadTip", () { +// test("getBlockHeadTip success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.headers.subscribe"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": {"height": 520481, "hex": "some block hex string"}, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = +// await (client.getBlockHeadTip(requestID: "some requestId")); +// +// expect(result["height"], 520481); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getBlockHeadTip throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.headers.subscribe"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.getBlockHeadTip(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("ping", () { +// test("ping success", () async { +// final mockClient = MockJsonRPC(); +// const command = "server.ping"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 2), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": null, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.ping(requestID: "some requestId"); +// +// expect(result, true); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("ping throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "server.ping"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 2), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.ping(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getServerFeatures", () { +// test("getServerFeatures success", () async { +// final mockClient = MockJsonRPC(); +// const command = "server.features"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": { +// "genesis_hash": +// "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", +// "hosts": { +// "0.0.0.0": {"tcp_port": 51001, "ssl_port": 51002} +// }, +// "protocol_max": "1.0", +// "protocol_min": "1.0", +// "pruning": null, +// "server_version": "ElectrumX 1.0.17", +// "hash_function": "sha256" +// }, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = +// await client.getServerFeatures(requestID: "some requestId"); +// +// expect(result, { +// "genesis_hash": +// "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", +// "hosts": { +// "0.0.0.0": {"tcp_port": 51001, "ssl_port": 51002} +// }, +// "protocol_max": "1.0", +// "protocol_min": "1.0", +// "pruning": null, +// "server_version": "ElectrumX 1.0.17", +// "hash_function": "sha256", +// }); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getServerFeatures throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "server.features"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.getServerFeatures(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("broadcastTransaction", () { +// test("broadcastTransaction success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.broadcast"; +// const jsonArgs = '["some raw transaction string"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": "the txid of the rawtx", +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.broadcastTransaction( +// rawTx: "some raw transaction string", requestID: "some requestId"); +// +// expect(result, "the txid of the rawtx"); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("broadcastTransaction throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.broadcast"; +// const jsonArgs = '["some raw transaction string"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.broadcastTransaction( +// rawTx: "some raw transaction string", +// requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getBalance", () { +// test("getBalance success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.get_balance"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": { +// "confirmed": 103873966, +// "unconfirmed": 23684400, +// }, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getBalance( +// scripthash: "dummy hash", requestID: "some requestId"); +// +// expect(result, {"confirmed": 103873966, "unconfirmed": 23684400}); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getBalance throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.get_balance"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getBalance( +// scripthash: "dummy hash", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getHistory", () { +// test("getHistory success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.get_history"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 5), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": [ +// { +// "height": 200004, +// "tx_hash": +// "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" +// }, +// { +// "height": 215008, +// "tx_hash": +// "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" +// } +// ], +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getHistory( +// scripthash: "dummy hash", requestID: "some requestId"); +// +// expect(result, [ +// { +// "height": 200004, +// "tx_hash": +// "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" +// }, +// { +// "height": 215008, +// "tx_hash": +// "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" +// } +// ]); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getHistory throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.get_history"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 5), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getHistory( +// scripthash: "dummy hash", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getUTXOs", () { +// test("getUTXOs success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.listunspent"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": [ +// { +// "tx_pos": 0, +// "value": 45318048, +// "tx_hash": +// "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", +// "height": 437146 +// }, +// { +// "tx_pos": 0, +// "value": 919195, +// "tx_hash": +// "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", +// "height": 441696 +// } +// ], +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getUTXOs( +// scripthash: "dummy hash", requestID: "some requestId"); +// +// expect(result, [ +// { +// "tx_pos": 0, +// "value": 45318048, +// "tx_hash": +// "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", +// "height": 437146 +// }, +// { +// "tx_pos": 0, +// "value": 919195, +// "tx_hash": +// "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", +// "height": 441696 +// } +// ]); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getUTXOs throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.listunspent"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getUTXOs( +// scripthash: "dummy hash", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getTransaction", () { +// test("getTransaction success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getTransaction throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getAnonymitySet", () { +// test("getAnonymitySet success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getanonymityset"; +// const jsonArgs = '["1",""]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": GetAnonymitySetSampleData.data, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusAnonymitySet( +// groupId: "1", blockhash: "", requestID: "some requestId"); +// +// expect(result, GetAnonymitySetSampleData.data); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getAnonymitySet throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getanonymityset"; +// const jsonArgs = '["1",""]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusAnonymitySet( +// groupId: "1", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getMintData", () { +// test("getMintData success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getmintmetadata"; +// const jsonArgs = '["some mints"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": "mint meta data", +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusMintData( +// mints: "some mints", requestID: "some requestId"); +// +// expect(result, "mint meta data"); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getMintData throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getmintmetadata"; +// const jsonArgs = '["some mints"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusMintData( +// mints: "some mints", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getUsedCoinSerials", () { +// test("getUsedCoinSerials success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getusedcoinserials"; +// const jsonArgs = '["0"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 2), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": GetUsedSerialsSampleData.serials, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusUsedCoinSerials( +// requestID: "some requestId", startNumber: 0); +// +// expect(result, GetUsedSerialsSampleData.serials); +// +// verify(mockPrefs.wifiOnly).called(3); +// verify(mockPrefs.useTor).called(3); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getUsedCoinSerials throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getusedcoinserials"; +// const jsonArgs = '["0"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 2), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusUsedCoinSerials( +// requestID: "some requestId", startNumber: 0), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getLatestCoinId", () { +// test("getLatestCoinId success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getlatestcoinid"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": 1, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = +// await client.getLelantusLatestCoinId(requestID: "some requestId"); +// +// expect(result, 1); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getLatestCoinId throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getlatestcoinid"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusLatestCoinId( +// requestID: "some requestId", +// ), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getCoinsForRecovery", () { +// test("getCoinsForRecovery success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getanonymityset"; +// const jsonArgs = '["1",""]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": GetAnonymitySetSampleData.data, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusAnonymitySet( +// groupId: "1", blockhash: "", requestID: "some requestId"); +// +// expect(result, GetAnonymitySetSampleData.data); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getAnonymitySet throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getanonymityset"; +// const jsonArgs = '["1",""]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusAnonymitySet( +// groupId: "1", +// requestID: "some requestId", +// ), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getMintData", () { +// test("getMintData success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getmintmetadata"; +// const jsonArgs = '["some mints"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": "mint meta data", +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusMintData( +// mints: "some mints", requestID: "some requestId"); +// +// expect(result, "mint meta data"); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getMintData throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getmintmetadata"; +// const jsonArgs = '["some mints"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusMintData( +// mints: "some mints", +// requestID: "some requestId", +// ), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getUsedCoinSerials", () { +// test("getUsedCoinSerials success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getusedcoinserials"; +// const jsonArgs = '["0"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 2), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": GetUsedSerialsSampleData.serials, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusUsedCoinSerials( +// requestID: "some requestId", startNumber: 0); +// +// expect(result, GetUsedSerialsSampleData.serials); +// +// verify(mockPrefs.wifiOnly).called(3); +// verify(mockPrefs.useTor).called(3); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getUsedCoinSerials throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getusedcoinserials"; +// const jsonArgs = '["0"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 2), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusUsedCoinSerials( +// requestID: "some requestId", startNumber: 0), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getLatestCoinId", () { +// test("getLatestCoinId success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getlatestcoinid"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": 1, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = +// await client.getLelantusLatestCoinId(requestID: "some requestId"); +// +// expect(result, 1); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getLatestCoinId throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getlatestcoinid"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.getLelantusLatestCoinId(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getFeeRate", () { +// test("getFeeRate success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.getfeerate"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": { +// "rate": 1000, +// }, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getFeeRate(requestID: "some requestId"); +// +// expect(result, {"rate": 1000}); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getFeeRate throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.getfeerate"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.getFeeRate(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// test("rpcClient is null throws with bad server info", () { +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// client: null, +// port: -10, +// host: "_ :sa %", +// useSSL: false, +// prefs: mockPrefs, +// torService: torService, +// failovers: [], +// ); +// +// expect(() => client.getFeeRate(), throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// group("Tor tests", () { +// // useTor is false, so no TorService calls should be made. +// test("Tor not in use", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when(mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// )).thenAnswer((_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId", +// })); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => false); +// when(mockPrefs.torKillSwitch) +// .thenAnswer((_) => false); // Or true, shouldn't matter. +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.disconnected); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// failovers: [], +// prefs: mockPrefs, +// torService: mockTorService, +// ); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNever(mockPrefs.torKillSwitch); +// verifyNoMoreInteractions(mockPrefs); +// verifyNever(mockTorService.status); +// verifyNoMoreInteractions(mockTorService); +// }); +// +// // useTor is true, but TorService is not enabled and the killswitch is off, so a clearnet call should be made. +// test("Tor in use but Tor unavailable and killswitch off", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when(mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// )).thenAnswer((_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId", +// })); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => true); +// when(mockPrefs.torKillSwitch).thenAnswer((_) => false); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.disconnected); +// when(mockTorService.getProxyInfo()).thenAnswer((_) => ( +// host: InternetAddress('1.2.3.4'), +// port: -1 +// )); // Port is set to -1 until Tor is enabled. +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: mockTorService, +// failovers: []); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verify(mockPrefs.torKillSwitch).called(1); +// verifyNoMoreInteractions(mockPrefs); +// verify(mockTorService.status).called(1); +// verifyNever(mockTorService.getProxyInfo()); +// verifyNoMoreInteractions(mockTorService); +// }); +// +// // useTor is true and TorService is enabled, so a TorService call should be made. +// test("Tor in use and available", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when(mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// )).thenAnswer((_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId", +// })); +// when(mockClient.proxyInfo) +// .thenAnswer((_) => (host: InternetAddress('1.2.3.4'), port: 42)); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => true); +// when(mockPrefs.torKillSwitch).thenAnswer((_) => false); // Or true. +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.connected); +// when(mockTorService.getProxyInfo()) +// .thenAnswer((_) => (host: InternetAddress('1.2.3.4'), port: 42)); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: mockTorService, +// failovers: []); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockClient.proxyInfo).called(1); +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNever(mockPrefs.torKillSwitch); +// verifyNoMoreInteractions(mockPrefs); +// verify(mockTorService.status).called(1); +// verify(mockTorService.getProxyInfo()).called(1); +// verifyNoMoreInteractions(mockTorService); +// }); +// +// // useTor is true, but TorService is not enabled and the killswitch is on, so no TorService calls should be made. +// test("killswitch enabled", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "error": { +// "code": 1, +// "message": "None should be a transaction hash", +// }, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => true); +// when(mockPrefs.torKillSwitch).thenAnswer((_) => true); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.disconnected); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// failovers: [], +// prefs: mockPrefs, +// torService: mockTorService, +// ); +// +// try { +// var result = await client.getTransaction( +// requestID: "some requestId", txHash: ''); +// } catch (e) { +// expect(e, isA()); +// expect( +// e.toString(), +// equals( +// "Exception: Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX")); +// } +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verify(mockPrefs.torKillSwitch).called(1); +// verifyNoMoreInteractions(mockPrefs); +// verify(mockTorService.status).called(1); +// verifyNoMoreInteractions(mockTorService); +// }); +// +// // useTor is true but Tor is not enabled, but because the killswitch is off, a clearnet call should be made. +// test("killswitch disabled", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => true); +// when(mockPrefs.torKillSwitch).thenAnswer((_) => false); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.disconnected); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// failovers: [], +// prefs: mockPrefs, +// torService: mockTorService, +// ); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verify(mockPrefs.torKillSwitch).called(1); +// verifyNoMoreInteractions(mockPrefs); +// verify(mockTorService.status).called(1); +// verifyNoMoreInteractions(mockTorService); +// }); +// }); +// } diff --git a/test/electrumx_test.mocks.dart b/test/electrumx_test.mocks.dart deleted file mode 100644 index 3f0660617..000000000 --- a/test/electrumx_test.mocks.dart +++ /dev/null @@ -1,771 +0,0 @@ -// Mocks generated by Mockito 5.4.2 from annotations -// in stackwallet/test/electrumx_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:io' as _i4; -import 'dart:ui' as _i11; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/rpc.dart' as _i2; -import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart' - as _i13; -import 'package:stackwallet/services/tor_service.dart' as _i12; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i9; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i8; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i10; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i7; -import 'package:stackwallet/utilities/prefs.dart' as _i6; -import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' - as _i3; -import 'package:tor_ffi_plugin/tor_ffi_plugin.dart' as _i14; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeJsonRPCResponse_1 extends _i1.SmartFake - implements _i2.JsonRPCResponse { - _FakeJsonRPCResponse_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeFusionInfo_2 extends _i1.SmartFake implements _i3.FusionInfo { - _FakeFusionInfo_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeInternetAddress_3 extends _i1.SmartFake - implements _i4.InternetAddress { - _FakeInternetAddress_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [JsonRPC]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockJsonRPC extends _i1.Mock implements _i2.JsonRPC { - MockJsonRPC() { - _i1.throwOnMissingStub(this); - } - - @override - bool get useSSL => (super.noSuchMethod( - Invocation.getter(#useSSL), - returnValue: false, - ) as bool); - @override - String get host => (super.noSuchMethod( - Invocation.getter(#host), - returnValue: '', - ) as String); - @override - int get port => (super.noSuchMethod( - Invocation.getter(#port), - returnValue: 0, - ) as int); - @override - Duration get connectionTimeout => (super.noSuchMethod( - Invocation.getter(#connectionTimeout), - returnValue: _FakeDuration_0( - this, - Invocation.getter(#connectionTimeout), - ), - ) as Duration); - @override - set proxyInfo(({_i4.InternetAddress host, int port})? _proxyInfo) => - super.noSuchMethod( - Invocation.setter( - #proxyInfo, - _proxyInfo, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future<_i2.JsonRPCResponse> request( - String? jsonRpcRequest, - Duration? requestTimeout, - ) => - (super.noSuchMethod( - Invocation.method( - #request, - [ - jsonRpcRequest, - requestTimeout, - ], - ), - returnValue: - _i5.Future<_i2.JsonRPCResponse>.value(_FakeJsonRPCResponse_1( - this, - Invocation.method( - #request, - [ - jsonRpcRequest, - requestTimeout, - ], - ), - )), - ) as _i5.Future<_i2.JsonRPCResponse>); - @override - _i5.Future disconnect({ - required String? reason, - bool? ignoreMutex = false, - }) => - (super.noSuchMethod( - Invocation.method( - #disconnect, - [], - { - #reason: reason, - #ignoreMutex: ignoreMutex, - }, - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); -} - -/// A class which mocks [Prefs]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockPrefs extends _i1.Mock implements _i6.Prefs { - MockPrefs() { - _i1.throwOnMissingStub(this); - } - - @override - bool get isInitialized => (super.noSuchMethod( - Invocation.getter(#isInitialized), - returnValue: false, - ) as bool); - @override - int get lastUnlockedTimeout => (super.noSuchMethod( - Invocation.getter(#lastUnlockedTimeout), - returnValue: 0, - ) as int); - @override - set lastUnlockedTimeout(int? lastUnlockedTimeout) => super.noSuchMethod( - Invocation.setter( - #lastUnlockedTimeout, - lastUnlockedTimeout, - ), - returnValueForMissingStub: null, - ); - @override - int get lastUnlocked => (super.noSuchMethod( - Invocation.getter(#lastUnlocked), - returnValue: 0, - ) as int); - @override - set lastUnlocked(int? lastUnlocked) => super.noSuchMethod( - Invocation.setter( - #lastUnlocked, - lastUnlocked, - ), - returnValueForMissingStub: null, - ); - @override - int get currentNotificationId => (super.noSuchMethod( - Invocation.getter(#currentNotificationId), - returnValue: 0, - ) as int); - @override - List get walletIdsSyncOnStartup => (super.noSuchMethod( - Invocation.getter(#walletIdsSyncOnStartup), - returnValue: [], - ) as List); - @override - set walletIdsSyncOnStartup(List? walletIdsSyncOnStartup) => - super.noSuchMethod( - Invocation.setter( - #walletIdsSyncOnStartup, - walletIdsSyncOnStartup, - ), - returnValueForMissingStub: null, - ); - @override - _i7.SyncingType get syncType => (super.noSuchMethod( - Invocation.getter(#syncType), - returnValue: _i7.SyncingType.currentWalletOnly, - ) as _i7.SyncingType); - @override - set syncType(_i7.SyncingType? syncType) => super.noSuchMethod( - Invocation.setter( - #syncType, - syncType, - ), - returnValueForMissingStub: null, - ); - @override - bool get wifiOnly => (super.noSuchMethod( - Invocation.getter(#wifiOnly), - returnValue: false, - ) as bool); - @override - set wifiOnly(bool? wifiOnly) => super.noSuchMethod( - Invocation.setter( - #wifiOnly, - wifiOnly, - ), - returnValueForMissingStub: null, - ); - @override - bool get showFavoriteWallets => (super.noSuchMethod( - Invocation.getter(#showFavoriteWallets), - returnValue: false, - ) as bool); - @override - set showFavoriteWallets(bool? showFavoriteWallets) => super.noSuchMethod( - Invocation.setter( - #showFavoriteWallets, - showFavoriteWallets, - ), - returnValueForMissingStub: null, - ); - @override - String get language => (super.noSuchMethod( - Invocation.getter(#language), - returnValue: '', - ) as String); - @override - set language(String? newLanguage) => super.noSuchMethod( - Invocation.setter( - #language, - newLanguage, - ), - returnValueForMissingStub: null, - ); - @override - String get currency => (super.noSuchMethod( - Invocation.getter(#currency), - returnValue: '', - ) as String); - @override - set currency(String? newCurrency) => super.noSuchMethod( - Invocation.setter( - #currency, - newCurrency, - ), - returnValueForMissingStub: null, - ); - @override - bool get randomizePIN => (super.noSuchMethod( - Invocation.getter(#randomizePIN), - returnValue: false, - ) as bool); - @override - set randomizePIN(bool? randomizePIN) => super.noSuchMethod( - Invocation.setter( - #randomizePIN, - randomizePIN, - ), - returnValueForMissingStub: null, - ); - @override - bool get useBiometrics => (super.noSuchMethod( - Invocation.getter(#useBiometrics), - returnValue: false, - ) as bool); - @override - set useBiometrics(bool? useBiometrics) => super.noSuchMethod( - Invocation.setter( - #useBiometrics, - useBiometrics, - ), - returnValueForMissingStub: null, - ); - @override - bool get hasPin => (super.noSuchMethod( - Invocation.getter(#hasPin), - returnValue: false, - ) as bool); - @override - set hasPin(bool? hasPin) => super.noSuchMethod( - Invocation.setter( - #hasPin, - hasPin, - ), - returnValueForMissingStub: null, - ); - @override - int get familiarity => (super.noSuchMethod( - Invocation.getter(#familiarity), - returnValue: 0, - ) as int); - @override - set familiarity(int? familiarity) => super.noSuchMethod( - Invocation.setter( - #familiarity, - familiarity, - ), - returnValueForMissingStub: null, - ); - @override - bool get torKillSwitch => (super.noSuchMethod( - Invocation.getter(#torKillSwitch), - returnValue: false, - ) as bool); - @override - set torKillSwitch(bool? torKillswitch) => super.noSuchMethod( - Invocation.setter( - #torKillSwitch, - torKillswitch, - ), - returnValueForMissingStub: null, - ); - @override - bool get showTestNetCoins => (super.noSuchMethod( - Invocation.getter(#showTestNetCoins), - returnValue: false, - ) as bool); - @override - set showTestNetCoins(bool? showTestNetCoins) => super.noSuchMethod( - Invocation.setter( - #showTestNetCoins, - showTestNetCoins, - ), - returnValueForMissingStub: null, - ); - @override - bool get isAutoBackupEnabled => (super.noSuchMethod( - Invocation.getter(#isAutoBackupEnabled), - returnValue: false, - ) as bool); - @override - set isAutoBackupEnabled(bool? isAutoBackupEnabled) => super.noSuchMethod( - Invocation.setter( - #isAutoBackupEnabled, - isAutoBackupEnabled, - ), - returnValueForMissingStub: null, - ); - @override - set autoBackupLocation(String? autoBackupLocation) => super.noSuchMethod( - Invocation.setter( - #autoBackupLocation, - autoBackupLocation, - ), - returnValueForMissingStub: null, - ); - @override - _i8.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( - Invocation.getter(#backupFrequencyType), - returnValue: _i8.BackupFrequencyType.everyTenMinutes, - ) as _i8.BackupFrequencyType); - @override - set backupFrequencyType(_i8.BackupFrequencyType? backupFrequencyType) => - super.noSuchMethod( - Invocation.setter( - #backupFrequencyType, - backupFrequencyType, - ), - returnValueForMissingStub: null, - ); - @override - set lastAutoBackup(DateTime? lastAutoBackup) => super.noSuchMethod( - Invocation.setter( - #lastAutoBackup, - lastAutoBackup, - ), - returnValueForMissingStub: null, - ); - @override - bool get hideBlockExplorerWarning => (super.noSuchMethod( - Invocation.getter(#hideBlockExplorerWarning), - returnValue: false, - ) as bool); - @override - set hideBlockExplorerWarning(bool? hideBlockExplorerWarning) => - super.noSuchMethod( - Invocation.setter( - #hideBlockExplorerWarning, - hideBlockExplorerWarning, - ), - returnValueForMissingStub: null, - ); - @override - bool get gotoWalletOnStartup => (super.noSuchMethod( - Invocation.getter(#gotoWalletOnStartup), - returnValue: false, - ) as bool); - @override - set gotoWalletOnStartup(bool? gotoWalletOnStartup) => super.noSuchMethod( - Invocation.setter( - #gotoWalletOnStartup, - gotoWalletOnStartup, - ), - returnValueForMissingStub: null, - ); - @override - set startupWalletId(String? startupWalletId) => super.noSuchMethod( - Invocation.setter( - #startupWalletId, - startupWalletId, - ), - returnValueForMissingStub: null, - ); - @override - bool get externalCalls => (super.noSuchMethod( - Invocation.getter(#externalCalls), - returnValue: false, - ) as bool); - @override - set externalCalls(bool? externalCalls) => super.noSuchMethod( - Invocation.setter( - #externalCalls, - externalCalls, - ), - returnValueForMissingStub: null, - ); - @override - bool get enableCoinControl => (super.noSuchMethod( - Invocation.getter(#enableCoinControl), - returnValue: false, - ) as bool); - @override - set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod( - Invocation.setter( - #enableCoinControl, - enableCoinControl, - ), - returnValueForMissingStub: null, - ); - @override - bool get enableSystemBrightness => (super.noSuchMethod( - Invocation.getter(#enableSystemBrightness), - returnValue: false, - ) as bool); - @override - set enableSystemBrightness(bool? enableSystemBrightness) => - super.noSuchMethod( - Invocation.setter( - #enableSystemBrightness, - enableSystemBrightness, - ), - returnValueForMissingStub: null, - ); - @override - String get themeId => (super.noSuchMethod( - Invocation.getter(#themeId), - returnValue: '', - ) as String); - @override - set themeId(String? themeId) => super.noSuchMethod( - Invocation.setter( - #themeId, - themeId, - ), - returnValueForMissingStub: null, - ); - @override - String get systemBrightnessLightThemeId => (super.noSuchMethod( - Invocation.getter(#systemBrightnessLightThemeId), - returnValue: '', - ) as String); - @override - set systemBrightnessLightThemeId(String? systemBrightnessLightThemeId) => - super.noSuchMethod( - Invocation.setter( - #systemBrightnessLightThemeId, - systemBrightnessLightThemeId, - ), - returnValueForMissingStub: null, - ); - @override - String get systemBrightnessDarkThemeId => (super.noSuchMethod( - Invocation.getter(#systemBrightnessDarkThemeId), - returnValue: '', - ) as String); - @override - set systemBrightnessDarkThemeId(String? systemBrightnessDarkThemeId) => - super.noSuchMethod( - Invocation.setter( - #systemBrightnessDarkThemeId, - systemBrightnessDarkThemeId, - ), - returnValueForMissingStub: null, - ); - @override - bool get useTor => (super.noSuchMethod( - Invocation.getter(#useTor), - returnValue: false, - ) as bool); - @override - set useTor(bool? useTor) => super.noSuchMethod( - Invocation.setter( - #useTor, - useTor, - ), - returnValueForMissingStub: null, - ); - @override - bool get frostEnabled => (super.noSuchMethod( - Invocation.getter(#frostEnabled), - returnValue: false, - ) as bool); - @override - set frostEnabled(bool? frostEnabled) => super.noSuchMethod( - Invocation.setter( - #frostEnabled, - frostEnabled, - ), - returnValueForMissingStub: null, - ); - @override - bool get hasListeners => (super.noSuchMethod( - Invocation.getter(#hasListeners), - returnValue: false, - ) as bool); - @override - _i5.Future init() => (super.noSuchMethod( - Invocation.method( - #init, - [], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( - Invocation.method( - #incrementCurrentNotificationIndex, - [], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future isExternalCallsSet() => (super.noSuchMethod( - Invocation.method( - #isExternalCallsSet, - [], - ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); - @override - _i5.Future saveUserID(String? userId) => (super.noSuchMethod( - Invocation.method( - #saveUserID, - [userId], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( - Invocation.method( - #saveSignupEpoch, - [signupEpoch], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i9.AmountUnit amountUnit(_i10.Coin? coin) => (super.noSuchMethod( - Invocation.method( - #amountUnit, - [coin], - ), - returnValue: _i9.AmountUnit.normal, - ) as _i9.AmountUnit); - @override - void updateAmountUnit({ - required _i10.Coin? coin, - required _i9.AmountUnit? amountUnit, - }) => - super.noSuchMethod( - Invocation.method( - #updateAmountUnit, - [], - { - #coin: coin, - #amountUnit: amountUnit, - }, - ), - returnValueForMissingStub: null, - ); - @override - int maxDecimals(_i10.Coin? coin) => (super.noSuchMethod( - Invocation.method( - #maxDecimals, - [coin], - ), - returnValue: 0, - ) as int); - @override - void updateMaxDecimals({ - required _i10.Coin? coin, - required int? maxDecimals, - }) => - super.noSuchMethod( - Invocation.method( - #updateMaxDecimals, - [], - { - #coin: coin, - #maxDecimals: maxDecimals, - }, - ), - returnValueForMissingStub: null, - ); - @override - _i3.FusionInfo getFusionServerInfo(_i10.Coin? coin) => (super.noSuchMethod( - Invocation.method( - #getFusionServerInfo, - [coin], - ), - returnValue: _FakeFusionInfo_2( - this, - Invocation.method( - #getFusionServerInfo, - [coin], - ), - ), - ) as _i3.FusionInfo); - @override - void setFusionServerInfo( - _i10.Coin? coin, - _i3.FusionInfo? fusionServerInfo, - ) => - super.noSuchMethod( - Invocation.method( - #setFusionServerInfo, - [ - coin, - fusionServerInfo, - ], - ), - returnValueForMissingStub: null, - ); - @override - void addListener(_i11.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - ), - returnValueForMissingStub: null, - ); - @override - void removeListener(_i11.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #removeListener, - [listener], - ), - returnValueForMissingStub: null, - ); - @override - void dispose() => super.noSuchMethod( - Invocation.method( - #dispose, - [], - ), - returnValueForMissingStub: null, - ); - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method( - #notifyListeners, - [], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [TorService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTorService extends _i1.Mock implements _i12.TorService { - MockTorService() { - _i1.throwOnMissingStub(this); - } - - @override - _i13.TorConnectionStatus get status => (super.noSuchMethod( - Invocation.getter(#status), - returnValue: _i13.TorConnectionStatus.disconnected, - ) as _i13.TorConnectionStatus); - @override - ({_i4.InternetAddress host, int port}) getProxyInfo() => (super.noSuchMethod( - Invocation.method( - #getProxyInfo, - [], - ), - returnValue: ( - host: _FakeInternetAddress_3( - this, - Invocation.method( - #getProxyInfo, - [], - ), - ), - port: 0 - ), - ) as ({_i4.InternetAddress host, int port})); - @override - void init({ - required String? torDataDirPath, - _i14.Tor? mockableOverride, - }) => - super.noSuchMethod( - Invocation.method( - #init, - [], - { - #torDataDirPath: torDataDirPath, - #mockableOverride: mockableOverride, - }, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future start() => (super.noSuchMethod( - Invocation.method( - #start, - [], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future disable() => (super.noSuchMethod( - Invocation.method( - #disable, - [], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); -} diff --git a/test/json_rpc_test.dart b/test/json_rpc_test.dart deleted file mode 100644 index e9da77971..000000000 --- a/test/json_rpc_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:stackwallet/electrumx_rpc/rpc.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; - -void main() { - test("REQUIRES INTERNET - JsonRPC.request success", () async { - final jsonRPC = JsonRPC( - host: DefaultNodes.bitcoin.host, - port: DefaultNodes.bitcoin.port, - useSSL: true, - connectionTimeout: const Duration(seconds: 40), - proxyInfo: null, // TODO test for proxyInfo - ); - - const jsonRequestString = - '{"jsonrpc": "2.0", "id": "some id","method": "server.ping","params": []}'; - final result = await jsonRPC.request( - jsonRequestString, - const Duration(seconds: 1), - ); - - expect(result.data, {"jsonrpc": "2.0", "result": null, "id": "some id"}); - }); - - test("JsonRPC.request fails due to SocketException", () async { - final jsonRPC = JsonRPC( - host: "some.bad.address.thingdsfsdfsdaf", - port: 3000, - connectionTimeout: const Duration(seconds: 10), - proxyInfo: null, - ); - - const jsonRequestString = - '{"jsonrpc": "2.0", "id": "some id","method": "server.ping","params": []}'; - - expect( - () => jsonRPC.request( - jsonRequestString, - const Duration(seconds: 1), - ), - throwsA(isA())); - }); - - test("JsonRPC.request fails due to connection timeout", () async { - final jsonRPC = JsonRPC( - host: "8.8.8.8", - port: 3000, - useSSL: false, - connectionTimeout: const Duration(seconds: 1), - proxyInfo: null, - ); - - const jsonRequestString = - '{"jsonrpc": "2.0", "id": "some id","method": "server.ping","params": []}'; - - await expectLater( - jsonRPC.request( - jsonRequestString, - const Duration(seconds: 1), - ), - throwsA(isA() - .having((e) => e.toString(), 'message', contains("Request timeout"))), - ); - }); -} diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index c2f2e6843..710bc087e 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -978,6 +978,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get solanaEnabled => (super.noSuchMethod( + Invocation.getter(#solanaEnabled), + returnValue: false, + ) as bool); + @override + set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( + Invocation.setter( + #solanaEnabled, + solanaEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get frostEnabled => (super.noSuchMethod( Invocation.getter(#frostEnabled), returnValue: false, diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index fc4a829e1..63ddafc1b 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -454,6 +454,19 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); @override + bool get solanaEnabled => (super.noSuchMethod( + Invocation.getter(#solanaEnabled), + returnValue: false, + ) as bool); + @override + set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( + Invocation.setter( + #solanaEnabled, + solanaEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get frostEnabled => (super.noSuchMethod( Invocation.getter(#frostEnabled), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index 669741692..bb7ed9b5b 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -3,18 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:ui' as _i11; +import 'dart:async' as _i4; +import 'dart:ui' as _i10; -import 'package:electrum_adapter/electrum_adapter.dart' as _i3; -import 'package:local_auth/auth_strings.dart' as _i8; -import 'package:local_auth/local_auth.dart' as _i7; +import 'package:local_auth/auth_strings.dart' as _i7; +import 'package:local_auth/local_auth.dart' as _i6; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i4; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i3; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i2; -import 'package:stackwallet/services/wallets_service.dart' as _i10; -import 'package:stackwallet/utilities/biometrics.dart' as _i9; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; +import 'package:stackwallet/services/wallets_service.dart' as _i9; +import 'package:stackwallet/utilities/biometrics.dart' as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i5; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -38,22 +37,11 @@ class _FakeElectrumXClient_0 extends _i1.SmartFake ); } -class _FakeElectrumClient_1 extends _i1.SmartFake - implements _i3.ElectrumClient { - _FakeElectrumClient_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i4.CachedElectrumXClient { + implements _i3.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @@ -67,37 +55,10 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i2.ElectrumXClient); @override - _i3.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( - Invocation.getter(#electrumAdapterClient), - returnValue: _FakeElectrumClient_1( - this, - Invocation.getter(#electrumAdapterClient), - ), - ) as _i3.ElectrumClient); - @override - set electrumAdapterClient(_i3.ElectrumClient? _electrumAdapterClient) => - super.noSuchMethod( - Invocation.setter( - #electrumAdapterClient, - _electrumAdapterClient, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future<_i3.ElectrumClient> Function() get electrumAdapterUpdateCallback => - (super.noSuchMethod( - Invocation.getter(#electrumAdapterUpdateCallback), - returnValue: () => - _i5.Future<_i3.ElectrumClient>.value(_FakeElectrumClient_1( - this, - Invocation.getter(#electrumAdapterUpdateCallback), - )), - ) as _i5.Future<_i3.ElectrumClient> Function()); - @override - _i5.Future> getAnonymitySet({ + _i4.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i5.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -110,13 +71,13 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i4.Future>.value({}), + ) as _i4.Future>); @override - _i5.Future> getSparkAnonymitySet({ + _i4.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i5.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -129,8 +90,8 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i4.Future>.value({}), + ) as _i4.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -148,9 +109,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i5.Future> getTransaction({ + _i4.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i5.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -164,11 +125,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i4.Future>.value({}), + ) as _i4.Future>); @override - _i5.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i4.Future> getUsedCoinSerials({ + required _i5.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -180,53 +141,53 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i5.Future>.value([]), - ) as _i5.Future>); + returnValue: _i4.Future>.value([]), + ) as _i4.Future>); @override - _i5.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i4.Future> getSparkUsedCoinsTags({required _i5.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i5.Future>.value({}), - ) as _i5.Future>); + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); @override - _i5.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i4.Future clearSharedTransactionCache({required _i5.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } /// A class which mocks [LocalAuthentication]. /// /// See the documentation for Mockito's code generation for more information. class MockLocalAuthentication extends _i1.Mock - implements _i7.LocalAuthentication { + implements _i6.LocalAuthentication { MockLocalAuthentication() { _i1.throwOnMissingStub(this); } @override - _i5.Future get canCheckBiometrics => (super.noSuchMethod( + _i4.Future get canCheckBiometrics => (super.noSuchMethod( Invocation.getter(#canCheckBiometrics), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future authenticateWithBiometrics({ + _i4.Future authenticateWithBiometrics({ required String? localizedReason, bool? useErrorDialogs = true, bool? stickyAuth = false, - _i8.AndroidAuthMessages? androidAuthStrings = - const _i8.AndroidAuthMessages(), - _i8.IOSAuthMessages? iOSAuthStrings = const _i8.IOSAuthMessages(), + _i7.AndroidAuthMessages? androidAuthStrings = + const _i7.AndroidAuthMessages(), + _i7.IOSAuthMessages? iOSAuthStrings = const _i7.IOSAuthMessages(), bool? sensitiveTransaction = true, }) => (super.noSuchMethod( @@ -242,16 +203,16 @@ class MockLocalAuthentication extends _i1.Mock #sensitiveTransaction: sensitiveTransaction, }, ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future authenticate({ + _i4.Future authenticate({ required String? localizedReason, bool? useErrorDialogs = true, bool? stickyAuth = false, - _i8.AndroidAuthMessages? androidAuthStrings = - const _i8.AndroidAuthMessages(), - _i8.IOSAuthMessages? iOSAuthStrings = const _i8.IOSAuthMessages(), + _i7.AndroidAuthMessages? androidAuthStrings = + const _i7.AndroidAuthMessages(), + _i7.IOSAuthMessages? iOSAuthStrings = const _i7.IOSAuthMessages(), bool? sensitiveTransaction = true, bool? biometricOnly = false, }) => @@ -269,46 +230,46 @@ class MockLocalAuthentication extends _i1.Mock #biometricOnly: biometricOnly, }, ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future stopAuthentication() => (super.noSuchMethod( + _i4.Future stopAuthentication() => (super.noSuchMethod( Invocation.method( #stopAuthentication, [], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future isDeviceSupported() => (super.noSuchMethod( + _i4.Future isDeviceSupported() => (super.noSuchMethod( Invocation.method( #isDeviceSupported, [], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future> getAvailableBiometrics() => + _i4.Future> getAvailableBiometrics() => (super.noSuchMethod( Invocation.method( #getAvailableBiometrics, [], ), returnValue: - _i5.Future>.value(<_i7.BiometricType>[]), - ) as _i5.Future>); + _i4.Future>.value(<_i6.BiometricType>[]), + ) as _i4.Future>); } /// A class which mocks [Biometrics]. /// /// See the documentation for Mockito's code generation for more information. -class MockBiometrics extends _i1.Mock implements _i9.Biometrics { +class MockBiometrics extends _i1.Mock implements _i8.Biometrics { MockBiometrics() { _i1.throwOnMissingStub(this); } @override - _i5.Future authenticate({ + _i4.Future authenticate({ required String? cancelButtonText, required String? localizedReason, required String? title, @@ -323,28 +284,28 @@ class MockBiometrics extends _i1.Mock implements _i9.Biometrics { #title: title, }, ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); } /// A class which mocks [WalletsService]. /// /// See the documentation for Mockito's code generation for more information. -class MockWalletsService extends _i1.Mock implements _i10.WalletsService { +class MockWalletsService extends _i1.Mock implements _i9.WalletsService { @override - _i5.Future> get walletNames => + _i4.Future> get walletNames => (super.noSuchMethod( Invocation.getter(#walletNames), - returnValue: _i5.Future>.value( - {}), - ) as _i5.Future>); + returnValue: _i4.Future>.value( + {}), + ) as _i4.Future>); @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, ) as bool); @override - void addListener(_i11.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i10.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -352,7 +313,7 @@ class MockWalletsService extends _i1.Mock implements _i10.WalletsService { returnValueForMissingStub: null, ); @override - void removeListener(_i11.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i10.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index 9e2e9d205..85e32012e 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -5,14 +5,15 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; -import 'package:electrum_adapter/electrum_adapter.dart' as _i4; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i8; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -25,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -35,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -45,9 +47,8 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( Object parent, Invocation parentInvocation, ) : super( @@ -56,9 +57,9 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } -class _FakeElectrumClient_3 extends _i1.SmartFake - implements _i4.ElectrumClient { - _FakeElectrumClient_3( +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -70,13 +71,21 @@ class _FakeElectrumClient_3 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -100,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -121,9 +130,9 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + _i5.Future closeAdapter() => (super.noSuchMethod( Invocation.method( - #checkElectrumAdapter, + #closeAdapter, [], ), returnValue: _i5.Future.value(), @@ -455,7 +464,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i5.Future>.value({}), ) as _i5.Future>); @override - _i5.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -468,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -479,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -495,7 +504,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. @@ -508,40 +517,13 @@ class MockCachedElectrumXClient extends _i1.Mock } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); - @override - _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( - Invocation.getter(#electrumAdapterClient), - returnValue: _FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterClient), - ), - ) as _i4.ElectrumClient); - @override - set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => - super.noSuchMethod( - Invocation.setter( - #electrumAdapterClient, - _electrumAdapterClient, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => - (super.noSuchMethod( - Invocation.getter(#electrumAdapterUpdateCallback), - returnValue: () => - _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterUpdateCallback), - )), - ) as _i5.Future<_i4.ElectrumClient> Function()); + ) as _i4.ElectrumXClient); @override _i5.Future> getAnonymitySet({ required String? groupId, diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index a2b73c2ef..0c2ab7d27 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -5,14 +5,15 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; -import 'package:electrum_adapter/electrum_adapter.dart' as _i4; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i8; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -25,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -35,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -45,9 +47,8 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( Object parent, Invocation parentInvocation, ) : super( @@ -56,9 +57,9 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } -class _FakeElectrumClient_3 extends _i1.SmartFake - implements _i4.ElectrumClient { - _FakeElectrumClient_3( +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -70,13 +71,21 @@ class _FakeElectrumClient_3 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -100,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -121,9 +130,9 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + _i5.Future closeAdapter() => (super.noSuchMethod( Invocation.method( - #checkElectrumAdapter, + #closeAdapter, [], ), returnValue: _i5.Future.value(), @@ -455,7 +464,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i5.Future>.value({}), ) as _i5.Future>); @override - _i5.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -468,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -479,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -495,7 +504,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. @@ -508,40 +517,13 @@ class MockCachedElectrumXClient extends _i1.Mock } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); - @override - _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( - Invocation.getter(#electrumAdapterClient), - returnValue: _FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterClient), - ), - ) as _i4.ElectrumClient); - @override - set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => - super.noSuchMethod( - Invocation.setter( - #electrumAdapterClient, - _electrumAdapterClient, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => - (super.noSuchMethod( - Invocation.getter(#electrumAdapterUpdateCallback), - returnValue: () => - _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterUpdateCallback), - )), - ) as _i5.Future<_i4.ElectrumClient> Function()); + ) as _i4.ElectrumXClient); @override _i5.Future> getAnonymitySet({ required String? groupId, diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 4aae75b2e..20a3938e6 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -5,14 +5,15 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; -import 'package:electrum_adapter/electrum_adapter.dart' as _i4; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i8; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -25,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -35,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -45,9 +47,8 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( Object parent, Invocation parentInvocation, ) : super( @@ -56,9 +57,9 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } -class _FakeElectrumClient_3 extends _i1.SmartFake - implements _i4.ElectrumClient { - _FakeElectrumClient_3( +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -70,13 +71,21 @@ class _FakeElectrumClient_3 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -100,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -121,9 +130,9 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + _i5.Future closeAdapter() => (super.noSuchMethod( Invocation.method( - #checkElectrumAdapter, + #closeAdapter, [], ), returnValue: _i5.Future.value(), @@ -455,7 +464,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i5.Future>.value({}), ) as _i5.Future>); @override - _i5.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -468,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -479,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -495,7 +504,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. @@ -508,40 +517,13 @@ class MockCachedElectrumXClient extends _i1.Mock } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); - @override - _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( - Invocation.getter(#electrumAdapterClient), - returnValue: _FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterClient), - ), - ) as _i4.ElectrumClient); - @override - set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => - super.noSuchMethod( - Invocation.setter( - #electrumAdapterClient, - _electrumAdapterClient, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => - (super.noSuchMethod( - Invocation.getter(#electrumAdapterUpdateCallback), - returnValue: () => - _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterUpdateCallback), - )), - ) as _i5.Future<_i4.ElectrumClient> Function()); + ) as _i4.ElectrumXClient); @override _i5.Future> getAnonymitySet({ required String? groupId, diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index bdf67b8a3..34a2b7c9e 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -5,14 +5,15 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; -import 'package:electrum_adapter/electrum_adapter.dart' as _i4; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i8; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -25,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -35,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -45,9 +47,8 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( Object parent, Invocation parentInvocation, ) : super( @@ -56,9 +57,9 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } -class _FakeElectrumClient_3 extends _i1.SmartFake - implements _i4.ElectrumClient { - _FakeElectrumClient_3( +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -70,13 +71,21 @@ class _FakeElectrumClient_3 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -100,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -121,9 +130,9 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + _i5.Future closeAdapter() => (super.noSuchMethod( Invocation.method( - #checkElectrumAdapter, + #closeAdapter, [], ), returnValue: _i5.Future.value(), @@ -455,7 +464,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i5.Future>.value({}), ) as _i5.Future>); @override - _i5.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -468,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -479,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -495,7 +504,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. @@ -508,40 +517,13 @@ class MockCachedElectrumXClient extends _i1.Mock } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); - @override - _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( - Invocation.getter(#electrumAdapterClient), - returnValue: _FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterClient), - ), - ) as _i4.ElectrumClient); - @override - set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => - super.noSuchMethod( - Invocation.setter( - #electrumAdapterClient, - _electrumAdapterClient, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => - (super.noSuchMethod( - Invocation.getter(#electrumAdapterUpdateCallback), - returnValue: () => - _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterUpdateCallback), - )), - ) as _i5.Future<_i4.ElectrumClient> Function()); + ) as _i4.ElectrumXClient); @override _i5.Future> getAnonymitySet({ required String? groupId, diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 4c03c2fe0..447d01329 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -5,14 +5,15 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; -import 'package:electrum_adapter/electrum_adapter.dart' as _i4; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' as _i8; import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -25,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -35,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -45,9 +47,8 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( Object parent, Invocation parentInvocation, ) : super( @@ -56,9 +57,9 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake ); } -class _FakeElectrumClient_3 extends _i1.SmartFake - implements _i4.ElectrumClient { - _FakeElectrumClient_3( +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -70,13 +71,21 @@ class _FakeElectrumClient_3 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -100,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -121,9 +130,9 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i5.Future checkElectrumAdapter() => (super.noSuchMethod( + _i5.Future closeAdapter() => (super.noSuchMethod( Invocation.method( - #checkElectrumAdapter, + #closeAdapter, [], ), returnValue: _i5.Future.value(), @@ -455,7 +464,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { _i5.Future>.value({}), ) as _i5.Future>); @override - _i5.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -468,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -479,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -495,7 +504,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. @@ -508,40 +517,13 @@ class MockCachedElectrumXClient extends _i1.Mock } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); - @override - _i4.ElectrumClient get electrumAdapterClient => (super.noSuchMethod( - Invocation.getter(#electrumAdapterClient), - returnValue: _FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterClient), - ), - ) as _i4.ElectrumClient); - @override - set electrumAdapterClient(_i4.ElectrumClient? _electrumAdapterClient) => - super.noSuchMethod( - Invocation.setter( - #electrumAdapterClient, - _electrumAdapterClient, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future<_i4.ElectrumClient> Function() get electrumAdapterUpdateCallback => - (super.noSuchMethod( - Invocation.getter(#electrumAdapterUpdateCallback), - returnValue: () => - _i5.Future<_i4.ElectrumClient>.value(_FakeElectrumClient_3( - this, - Invocation.getter(#electrumAdapterUpdateCallback), - )), - ) as _i5.Future<_i4.ElectrumClient> Function()); + ) as _i4.ElectrumXClient); @override _i5.Future> getAnonymitySet({ required String? groupId, diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index d2b31bb6d..ebb7ca994 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -712,6 +712,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get solanaEnabled => (super.noSuchMethod( + Invocation.getter(#solanaEnabled), + returnValue: false, + ) as bool); + @override + set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( + Invocation.setter( + #solanaEnabled, + solanaEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get frostEnabled => (super.noSuchMethod( Invocation.getter(#frostEnabled), returnValue: false, diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 57c22126e..ad81db4cd 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -598,6 +598,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get solanaEnabled => (super.noSuchMethod( + Invocation.getter(#solanaEnabled), + returnValue: false, + ) as bool); + @override + set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( + Invocation.setter( + #solanaEnabled, + solanaEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get frostEnabled => (super.noSuchMethod( Invocation.getter(#frostEnabled), returnValue: false, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 8841fb5db..fc08360ac 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -698,6 +698,19 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { returnValueForMissingStub: null, ); @override + bool get solanaEnabled => (super.noSuchMethod( + Invocation.getter(#solanaEnabled), + returnValue: false, + ) as bool); + @override + set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( + Invocation.setter( + #solanaEnabled, + solanaEnabled, + ), + returnValueForMissingStub: null, + ); + @override bool get frostEnabled => (super.noSuchMethod( Invocation.getter(#frostEnabled), returnValue: false, From 02997ffc70c9c93a6e077e76d7b5ef4463ba4391 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 15:12:49 -0600 Subject: [PATCH 149/272] disable eth token price fetching to prevent being throttled --- lib/services/price.dart | 66 ++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/services/price.dart b/lib/services/price.dart index 40c19f16e..281372dcd 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -187,39 +187,39 @@ class PriceAPI { } try { - for (final contractAddress in contractAddresses) { - final uri = Uri.parse( - "https://api.coingecko.com/api/v3/simple/token_price/ethereum" - "?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses" - "=$contractAddress&include_24hr_change=true"); - - final coinGeckoResponse = await client.get( - url: uri, - headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - try { - final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; - - final map = coinGeckoData[contractAddress] as Map; - - final price = - Decimal.parse(map[baseCurrency.toLowerCase()].toString()); - final change24h = double.parse( - map["${baseCurrency.toLowerCase()}_24h_change"].toString()); - - tokenPrices[contractAddress] = Tuple2(price, change24h); - } catch (e, s) { - // only log the error as we don't want to interrupt the rest of the loop - Logging.instance.log( - "getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddress): $e\n$s\nRESPONSE: ${coinGeckoResponse.body}", - level: LogLevel.Warning, - ); - } - } + // for (final contractAddress in contractAddresses) { + // final uri = Uri.parse( + // "https://api.coingecko.com/api/v3/simple/token_price/ethereum" + // "?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses" + // "=$contractAddress&include_24hr_change=true"); + // + // final coinGeckoResponse = await client.get( + // url: uri, + // headers: {'Content-Type': 'application/json'}, + // proxyInfo: Prefs.instance.useTor + // ? TorService.sharedInstance.getProxyInfo() + // : null, + // ); + // + // try { + // final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; + // + // final map = coinGeckoData[contractAddress] as Map; + // + // final price = + // Decimal.parse(map[baseCurrency.toLowerCase()].toString()); + // final change24h = double.parse( + // map["${baseCurrency.toLowerCase()}_24h_change"].toString()); + // + // tokenPrices[contractAddress] = Tuple2(price, change24h); + // } catch (e, s) { + // // only log the error as we don't want to interrupt the rest of the loop + // Logging.instance.log( + // "getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddress): $e\n$s\nRESPONSE: ${coinGeckoResponse.body}", + // level: LogLevel.Warning, + // ); + // } + // } return tokenPrices; } catch (e, s) { From f3ef245fd7f842022d431621cce7b435a676822c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 24 Apr 2024 16:21:25 -0500 Subject: [PATCH 150/272] pass proxy to StellarSdk as appropriate --- lib/wallets/wallet/impl/stellar_wallet.dart | 20 +++++++++++++++++++- pubspec.lock | 13 +++++++------ pubspec.yaml | 5 ++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart index d078dd98e..edea60fe5 100644 --- a/lib/wallets/wallet/impl/stellar_wallet.dart +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:isar/isar.dart'; +import 'package:socks5_proxy/socks.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; @@ -9,6 +11,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart' import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -43,6 +46,7 @@ class StellarWallet extends Bip39Wallet { // ============== Private ==================================================== stellar.StellarSDK? _stellarSdk; + HttpClient? _httpClient; Future _getBaseFee() async { final fees = await stellarSdk.feeStats.execute(); @@ -51,7 +55,21 @@ class StellarWallet extends Bip39Wallet { void _updateSdk() { final currentNode = getCurrentNode(); - _stellarSdk = stellar.StellarSDK("${currentNode.host}:${currentNode.port}"); + + // TODO [prio=med]: refactor out and call before requests in case Tor is enabled/disabled, listen to prefs change, or similar. + if (prefs.useTor) { + final ({InternetAddress host, int port}) proxyInfo = + TorService.sharedInstance.getProxyInfo(); + + _httpClient = HttpClient(); + SocksTCPClient.assignToHttpClient( + _httpClient!, [ProxySettings(proxyInfo.host, proxyInfo.port)]); + } else { + _httpClient = null; + } + + _stellarSdk = stellar.StellarSDK("${currentNode.host}:${currentNode.port}", + httpClient: _httpClient); } Future _accountExists(String accountId) async { diff --git a/pubspec.lock b/pubspec.lock index 4e4d39cb7..afd26b39f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1659,10 +1659,11 @@ packages: stellar_flutter_sdk: dependency: "direct main" description: - name: stellar_flutter_sdk - sha256: "4c55b1b6dfbde7f89bba59a422754280715fa3b5726cff5e7eeaed454d2c4b89" - url: "https://pub.dev" - source: hosted + path: "." + ref: eca1d730e952cf6a6d64502f977cfc03876b75d4 + resolved-ref: eca1d730e952cf6a6d64502f977cfc03876b75d4 + url: "https://github.com/cypherstack/stellar_flutter_sdk.git" + source: git version: "1.5.3" stream_channel: dependency: "direct main" @@ -2109,5 +2110,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.3 <4.0.0" + flutter: ">=3.19.5" diff --git a/pubspec.yaml b/pubspec.yaml index 16db28f97..2c432f6e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -156,7 +156,10 @@ dependencies: desktop_drop: ^0.4.1 nanodart: ^2.0.0 basic_utils: ^5.5.4 - stellar_flutter_sdk: ^1.5.3 + stellar_flutter_sdk: # ^1.5.3 + git: # TODO Revert to official package once Tor support is merged upstream. + url: https://github.com/cypherstack/stellar_flutter_sdk.git + ref: eca1d730e952cf6a6d64502f977cfc03876b75d4 # tor-backport branch (based on 1.5.3). socks_socket: git: url: https://github.com/cypherstack/socks_socket.git From fe531ba19127d8bb9dd46d7ead79dc40acb2bce6 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 24 Apr 2024 16:35:00 -0500 Subject: [PATCH 151/272] point solana package to master on cypherstack/espresso-cash-public --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 723f2aae2..b299c3d71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -180,7 +180,7 @@ dependencies: solana: git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. url: https://github.com/cypherstack/espresso-cash-public.git - ref: 0ada1f775c2a2c815de640424270a229f5e91e2f + ref: a83e375678eb22fe544dc125d29bbec0fb833882 path: packages/solana dev_dependencies: From b17b8d0b4bfce95aa16c4587d22d518e12f71511 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 15:42:17 -0600 Subject: [PATCH 152/272] clean up code --- lib/wallets/wallet/wallet.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index f8473450b..850e2449e 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -39,6 +39,7 @@ import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/namecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/particl_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/solana_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/stellar_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/tezos_wallet.dart'; @@ -52,8 +53,6 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interf import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; -import 'impl/solana_wallet.dart'; - abstract class Wallet { // default to Transaction class. For TransactionV2 set to 2 int get isarTransactionVersion => 1; @@ -398,10 +397,10 @@ abstract class Wallet { } void _periodicPingCheck() async { - bool hasNetwork = await pingCheck(); + final bool hasNetwork = await pingCheck(); if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork + final NodeConnectionStatus status = hasNetwork ? NodeConnectionStatus.connected : NodeConnectionStatus.disconnected; GlobalEventBus.instance.fire( @@ -638,7 +637,7 @@ abstract class Wallet { // Close the subscription if this wallet is not in the list to be synced. if (!prefs.walletIdsSyncOnStartup.contains(walletId)) { // Check if there's another wallet of this coin on the sync list. - List walletIds = []; + final List walletIds = []; for (final id in prefs.walletIdsSyncOnStartup) { final wallet = mainDB.isar.walletInfo .where() From 7070d16addc880bcfdc812345e0c5c5a8db3acf4 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 16:22:56 -0600 Subject: [PATCH 153/272] fix: bug where electrumx wallets don't show address on first time opening receive screen --- .../wallet/wallet_mixin_interfaces/electrumx_interface.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 9a05c368a..80e6f89ef 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1690,9 +1690,6 @@ mixin ElectrumXInterface on Bip39HDWallet { } } - @override - Future checkSaveInitialReceivingAddress() async {} - @override Future init() async { try { From 53b849c8c5abbf4022d89e78782f0e4c77475b25 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 16:36:12 -0600 Subject: [PATCH 154/272] slightly hacked in address type selection and generation based on the wallet's supported address types --- lib/pages/receive_view/receive_view.dart | 262 ++++++++---------- .../enums/derive_path_type_enum.dart | 27 +- .../wallet/intermediate/bip39_hd_wallet.dart | 35 ++- 3 files changed, 170 insertions(+), 154 deletions(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index fcd6dcfef..d1f9e23c4 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -30,8 +30,11 @@ 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/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'; @@ -65,11 +68,14 @@ class _ReceiveViewState extends ConsumerState { late final Coin coin; late final String walletId; late final ClipboardInterface clipboard; - late final bool supportsSpark; + late final bool _supportsSpark; + late final bool _showMultiType; - String? _sparkAddress; - String? _qrcodeContent; - bool _showSparkAddress = true; + int _currentIndex = 0; + + final List _walletAddressTypes = []; + final Map _addressMap = {}; + final Map> _addressSubMap = {}; Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); @@ -97,13 +103,32 @@ class _ReceiveViewState extends ConsumerState { ), ); - await wallet.generateNewReceivingAddress(); + final Address? address; + if (wallet is Bip39HDWallet && wallet is! BCashInterface) { + final type = DerivePathType.values.firstWhere( + (e) => e.getAddressType() == _walletAddressTypes[_currentIndex], + ); + address = await wallet.generateNextReceivingAddress( + derivePathType: type, + ); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address!); + }); + } else { + await wallet.generateNewReceivingAddress(); + address = null; + } shouldPop = true; if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(ReceiveView.routeName)); + + setState(() { + _addressMap[_walletAddressTypes[_currentIndex]] = + address?.value ?? ref.read(pWalletReceivingAddress(walletId)); + }); } } } @@ -142,45 +167,64 @@ class _ReceiveViewState extends ConsumerState { if (mounted) { Navigator.of(context, rootNavigator: true).pop(); - if (_sparkAddress != address.value) { - setState(() { - _sparkAddress = address.value; - }); - } + setState(() { + _addressMap[AddressType.spark] = address.value; + }); } } } - StreamSubscription? _streamSub; - @override void initState() { walletId = widget.walletId; coin = ref.read(pWalletCoin(walletId)); clipboard = widget.clipboard; - supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; + final wallet = ref.read(pWallets).getWallet(walletId); + _supportsSpark = wallet is SparkInterface; + _showMultiType = _supportsSpark || + (wallet is! BCashInterface && + wallet is Bip39HDWallet && + wallet.supportedAddressTypes.length > 1); - if (supportsSpark) { - _streamSub = ref - .read(mainDBProvider) - .isar - .addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .typeEqualTo(AddressType.spark) - .sortByDerivationIndexDesc() - .findFirst() - .asStream() - .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _sparkAddress = event?.value; - }); - } + _walletAddressTypes.add(coin.primaryAddressType); + + if (_showMultiType) { + if (_supportsSpark) { + _walletAddressTypes.insert(0, AddressType.spark); + } else { + _walletAddressTypes.addAll((wallet as Bip39HDWallet) + .supportedAddressTypes + .where((e) => e != coin.primaryAddressType)); + } + } + + _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(); @@ -188,7 +232,9 @@ class _ReceiveViewState extends ConsumerState { @override void dispose() { - _streamSub?.cancel(); + for (final subscription in _addressSubMap.values) { + subscription.cancel(); + } super.dispose(); } @@ -198,14 +244,11 @@ class _ReceiveViewState extends ConsumerState { final ticker = widget.tokenContract?.symbol ?? coin.ticker; - if (supportsSpark) { - if (_showSparkAddress) { - _qrcodeContent = _sparkAddress; - } else { - _qrcodeContent = ref.watch(pWalletReceivingAddress(walletId)); - } + final String address; + if (_showMultiType) { + address = _addressMap[_walletAddressTypes[_currentIndex]]!; } else { - _qrcodeContent = ref.watch(pWalletReceivingAddress(walletId)); + address = ref.watch(pWalletReceivingAddress(walletId)); } return Background( @@ -321,7 +364,7 @@ class _ReceiveViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ConditionalParent( - condition: supportsSpark, + condition: _showMultiType, builder: (child) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -337,28 +380,24 @@ class _ReceiveViewState extends ConsumerState { height: 10, ), DropdownButtonHideUnderline( - child: DropdownButton2( - value: _showSparkAddress, + child: DropdownButton2( + value: _currentIndex, items: [ - DropdownMenuItem( - value: true, - child: Text( - "Spark address", - style: STextStyles.w500_14(context), + for (int i = 0; + i < _walletAddressTypes.length; + i++) + DropdownMenuItem( + value: i, + child: Text( + "${_walletAddressTypes[i].readableName} address", + style: STextStyles.w500_14(context), + ), ), - ), - DropdownMenuItem( - value: false, - child: Text( - "Transparent address", - style: STextStyles.w500_14(context), - ), - ), ], onChanged: (value) { - if (value is bool && value != _showSparkAddress) { + if (value != null && value != _currentIndex) { setState(() { - _showSparkAddress = value; + _currentIndex = value; }); } }, @@ -409,89 +448,7 @@ class _ReceiveViewState extends ConsumerState { const SizedBox( height: 12, ), - if (_showSparkAddress) - GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: _sparkAddress ?? "Error"), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - width: 1, - ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your ${coin.ticker} SPARK address", - style: - STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 15, - height: 15, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 8, - ), - Row( - children: [ - Expanded( - child: Text( - _sparkAddress ?? "Error", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - if (!_showSparkAddress) child, + child, ], ), child: GestureDetector( @@ -499,8 +456,8 @@ class _ReceiveViewState extends ConsumerState { HapticFeedback.lightImpact(); clipboard.setData( ClipboardData( - text: - ref.watch(pWalletReceivingAddress(walletId))), + text: address, + ), ); showFloatingFlushBar( type: FlushBarType.info, @@ -547,8 +504,7 @@ class _ReceiveViewState extends ConsumerState { children: [ Expanded( child: Text( - ref.watch( - pWalletReceivingAddress(walletId)), + address, style: STextStyles.itemSubtitle12(context), ), ), @@ -568,7 +524,7 @@ class _ReceiveViewState extends ConsumerState { HapticFeedback.lightImpact(); clipboard.setData( ClipboardData( - text: _qrcodeContent ?? "", + text: address, ), ); showFloatingFlushBar( @@ -582,17 +538,19 @@ class _ReceiveViewState extends ConsumerState { 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) + _supportsSpark) SecondaryButton( label: "Generate new address", - onPressed: supportsSpark && _showSparkAddress + onPressed: _supportsSpark && + _walletAddressTypes[_currentIndex] == + AddressType.spark ? generateNewSparkAddress : generateNewAddress, ), @@ -608,7 +566,7 @@ class _ReceiveViewState extends ConsumerState { QrImageView( data: AddressUtils.buildUriString( coin, - _qrcodeContent ?? "", + address, {}, ), size: MediaQuery.of(context).size.width / 2, @@ -627,7 +585,7 @@ class _ReceiveViewState extends ConsumerState { RouteGenerator.useMaterialPageRoute, builder: (_) => GenerateUriQrCodeView( coin: coin, - receivingAddress: _qrcodeContent ?? "", + receivingAddress: address, ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index f830bc077..50ec1ab59 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -8,6 +8,7 @@ * */ +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; enum DerivePathType { @@ -18,7 +19,31 @@ enum DerivePathType { eth, eCash44, solana, - bip86, + bip86; + + AddressType getAddressType() { + switch (this) { + case DerivePathType.bip44: + case DerivePathType.bch44: + case DerivePathType.eCash44: + return AddressType.p2pkh; + + case DerivePathType.bip49: + return AddressType.p2sh; + + case DerivePathType.bip84: + return AddressType.p2wpkh; + + case DerivePathType.eth: + return AddressType.ethereum; + + case DerivePathType.solana: + return AddressType.solana; + + case DerivePathType.bip86: + return AddressType.p2tr; + } + } } extension DerivePathTypeExt on DerivePathType { diff --git a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart index 8d352811b..ffb927713 100644 --- a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart +++ b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart @@ -11,7 +11,13 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address abstract class Bip39HDWallet extends Bip39Wallet with MultiAddressInterface { - Bip39HDWallet(T cryptoCurrency) : super(cryptoCurrency); + Bip39HDWallet(super.cryptoCurrency); + + Set get supportedAddressTypes => + cryptoCurrency.supportedDerivationPathTypes + .where((e) => e != DerivePathType.bip49) + .map((e) => e.getAddressType()) + .toSet(); Future getRootHDNode() async { final seed = bip39.mnemonicToSeed( @@ -21,6 +27,33 @@ abstract class Bip39HDWallet extends Bip39Wallet return coinlib.HDPrivateKey.fromSeed(seed); } + Future
generateNextReceivingAddress({ + required DerivePathType derivePathType, + }) async { + if (!cryptoCurrency.supportedDerivationPathTypes.contains(derivePathType)) { + throw Exception( + "Unsupported DerivePathType passed to generateNextReceivingAddress().", + ); + } + + final current = await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(derivePathType.getAddressType()) + .sortByDerivationIndexDesc() + .findFirst(); + final index = current == null ? 0 : current.derivationIndex + 1; + const chain = 0; // receiving address + final address = await _generateAddress( + chain: chain, + index: index, + derivePathType: derivePathType, + ); + + return address; + } + /// Generates a receiving address. If none /// are in the current wallet db it will generate at index 0, otherwise the /// highest index found in the current wallet db. From 420c73ed5d385096618089a4415be7ac1d4ade0e Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Apr 2024 17:17:29 -0600 Subject: [PATCH 155/272] mobile address list changes as per figma --- .../receive_view/addresses/address_card.dart | 278 ++++++++++++++++-- .../addresses/wallet_addresses_view.dart | 195 ++++++------ lib/utilities/text_styles.dart | 22 ++ .../custom_buttons/simple_edit_button.dart | 39 +-- 4 files changed, 394 insertions(+), 140 deletions(-) diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart index 05c645352..51e1402a3 100644 --- a/lib/pages/receive_view/addresses/address_card.dart +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -10,31 +10,46 @@ import 'dart:async'; import 'dart:io'; +import 'dart:ui' as ui; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/pages/receive_view/addresses/address_tag.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_edit_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; class AddressCard extends ConsumerStatefulWidget { const AddressCard({ - Key? key, + super.key, required this.addressId, required this.walletId, required this.coin, this.onPressed, this.clipboard = const ClipboardWrapper(), - }) : super(key: key); + }); final int addressId; final String walletId; @@ -47,6 +62,7 @@ class AddressCard extends ConsumerStatefulWidget { } class _AddressCardState extends ConsumerState { + final _qrKey = GlobalKey(); final isDesktop = Util.isDesktop; late Stream stream; @@ -54,6 +70,72 @@ class _AddressCardState extends ConsumerState { AddressLabel? label; + Future _capturePng(bool shouldSaveInsteadOfShare) async { + try { + final RenderRepaintBoundary boundary = + _qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary; + final ui.Image image = await boundary.toImage(); + final ByteData? byteData = + await image.toByteData(format: ui.ImageByteFormat.png); + final Uint8List pngBytes = byteData!.buffer.asUint8List(); + + if (shouldSaveInsteadOfShare) { + if (Util.isDesktop) { + final dir = Directory("${Platform.environment['HOME']}"); + if (!dir.existsSync()) { + throw Exception( + "Home dir not found while trying to open filepicker on QR image save"); + } + final path = await FilePicker.platform.saveFile( + fileName: "qrcode.png", + initialDirectory: dir.path, + ); + + if (path != null) { + final file = File(path); + if (file.existsSync()) { + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$path already exists!", + context: context, + ), + ); + } + } else { + await file.writeAsBytes(pngBytes); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "$path saved!", + context: context, + ), + ); + } + } + } + } else { + // await DocumentFileSavePlus.saveFile( + // pngBytes, + // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", + // "image/png"); + } + } else { + final tempDir = await getTemporaryDirectory(); + final file = await File("${tempDir.path}/qrcode.png").create(); + await file.writeAsBytes(pngBytes); + + await Share.shareFiles(["${tempDir.path}/qrcode.png"], + text: "Receive URI QR Code"); + } + } catch (e) { + //todo: comeback to this + debugPrint(e.toString()); + } + } + @override void initState() { address = MainDB.instance.isar.addresses @@ -117,16 +199,32 @@ class _AddressCardState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (label!.value.isNotEmpty) - Text( - label!.value, - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - if (label!.value.isNotEmpty) - SizedBox( - height: isDesktop ? 2 : 8, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label!.value.isNotEmpty ? label!.value : "No label", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + SimpleEditButton( + editValue: label!.value, + editLabel: 'label', + overrideTitle: 'Edit label', + disableIcon: true, + onValueChanged: (value) { + MainDB.instance.putAddressLabel( + label!.copyWith( + label: value, + ), + ); + }, + ), + ], + ), + SizedBox( + height: isDesktop ? 2 : 8, + ), Row( children: [ Expanded( @@ -140,18 +238,152 @@ class _AddressCardState extends ConsumerState { const SizedBox( height: 10, ), - if (label!.tags != null && label!.tags!.isNotEmpty) - Wrap( - spacing: 10, - runSpacing: 10, - children: label!.tags! - .map( - (e) => AddressTag( - tag: e, + Row( + children: [ + CustomTextButton( + text: "Copy address", + onTap: () { + widget.clipboard + .setData( + ClipboardData( + text: address.value, ), ) - .toList(), - ), + .then((value) { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }); + }, + ), + const SizedBox( + width: 16, + ), + CustomTextButton( + text: "Show QR code", + onTap: () async { + await showDialog( + context: context, + builder: (_) { + return StackDialogBase( + child: Column( + children: [ + if (label!.value.isNotEmpty) + Text( + label!.value, + style: STextStyles.w600_18(context), + ), + if (label!.value.isNotEmpty) + const SizedBox( + height: 8, + ), + Text( + address.value, + style: + STextStyles.w500_16(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 16, + ), + Center( + child: RepaintBoundary( + key: _qrKey, + child: QrImageView( + data: AddressUtils.buildUriString( + widget.coin, + address.value, + {}, + ), + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .popupBG, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + if (!isDesktop) + Expanded( + child: SecondaryButton( + label: "Share", + buttonHeight: isDesktop + ? ButtonHeight.l + : null, + icon: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await _capturePng(false); + }, + ), + ), + if (isDesktop) + Expanded( + child: PrimaryButton( + buttonHeight: isDesktop + ? ButtonHeight.l + : null, + onPressed: () async { + // TODO: add save functionality instead of share + // save works on linux at the moment + await _capturePng(true); + }, + label: "Save", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + ), + ), + ], + ) + ], + ), + ); + }, + ); + }, + ), + ], + ), + // if (label!.tags != null && label!.tags!.isNotEmpty) + // Wrap( + // spacing: 10, + // runSpacing: 10, + // children: label!.tags! + // .map( + // (e) => AddressTag( + // tag: e, + // ), + // ) + // .toList(), + // ), ], ), ); diff --git a/lib/pages/receive_view/addresses/wallet_addresses_view.dart b/lib/pages/receive_view/addresses/wallet_addresses_view.dart index 597ca44fe..c6cd03215 100644 --- a/lib/pages/receive_view/addresses/wallet_addresses_view.dart +++ b/lib/pages/receive_view/addresses/wallet_addresses_view.dart @@ -10,14 +10,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_details_view.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; @@ -25,13 +23,8 @@ import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; -import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; -import '../../../utilities/assets.dart'; -import '../../../widgets/icon_widgets/x_icon.dart'; -import '../../../widgets/textfield_icon_button.dart'; - class WalletAddressesView extends ConsumerStatefulWidget { const WalletAddressesView({ Key? key, @@ -50,10 +43,10 @@ class WalletAddressesView extends ConsumerStatefulWidget { class _WalletAddressesViewState extends ConsumerState { final bool isDesktop = Util.isDesktop; - String _searchString = ""; + final String _searchString = ""; - late final TextEditingController _searchController; - final searchFieldFocusNode = FocusNode(); + // late final TextEditingController _searchController; + // final searchFieldFocusNode = FocusNode(); Future> _search(String term) async { if (term.isEmpty) { @@ -119,19 +112,19 @@ class _WalletAddressesViewState extends ConsumerState { .findAll(); } - @override - void initState() { - _searchController = TextEditingController(); - - super.initState(); - } - - @override - void dispose() { - _searchController.dispose(); - searchFieldFocusNode.dispose(); - super.dispose(); - } + // @override + // void initState() { + // _searchController = TextEditingController(); + // + // super.initState(); + // } + // + // @override + // void dispose() { + // _searchController.dispose(); + // searchFieldFocusNode.dispose(); + // super.dispose(); + // } @override Widget build(BuildContext context) { @@ -165,74 +158,74 @@ class _WalletAddressesViewState extends ConsumerState { ), child: Column( children: [ - SizedBox( - width: isDesktop ? 490 : null, - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchController, - focusNode: searchFieldFocusNode, - onChanged: (value) { - setState(() { - _searchString = value; - }); - }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: standardInputDecoration( - "Search...", - searchFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 12 : 10, - vertical: isDesktop ? 18 : 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: isDesktop ? 20 : 16, - height: isDesktop ? 20 : 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - ), - SizedBox( - height: isDesktop ? 20 : 16, - ), + // SizedBox( + // width: isDesktop ? 490 : null, + // child: ClipRRect( + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // child: TextField( + // autocorrect: !isDesktop, + // enableSuggestions: !isDesktop, + // controller: _searchController, + // focusNode: searchFieldFocusNode, + // onChanged: (value) { + // setState(() { + // _searchString = value; + // }); + // }, + // style: isDesktop + // ? STextStyles.desktopTextExtraSmall(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .textFieldActiveText, + // height: 1.8, + // ) + // : STextStyles.field(context), + // decoration: standardInputDecoration( + // "Search...", + // searchFieldFocusNode, + // context, + // desktopMed: isDesktop, + // ).copyWith( + // prefixIcon: Padding( + // padding: EdgeInsets.symmetric( + // horizontal: isDesktop ? 12 : 10, + // vertical: isDesktop ? 18 : 16, + // ), + // child: SvgPicture.asset( + // Assets.svg.search, + // width: isDesktop ? 20 : 16, + // height: isDesktop ? 20 : 16, + // ), + // ), + // suffixIcon: _searchController.text.isNotEmpty + // ? Padding( + // padding: const EdgeInsets.only(right: 0), + // child: UnconstrainedBox( + // child: Row( + // children: [ + // TextFieldIconButton( + // child: const XIcon(), + // onTap: () async { + // setState(() { + // _searchController.text = ""; + // _searchString = ""; + // }); + // }, + // ), + // ], + // ), + // ), + // ) + // : null, + // ), + // ), + // ), + // ), + // SizedBox( + // height: isDesktop ? 20 : 16, + // ), Expanded( child: FutureBuilder( future: _search(_searchString), @@ -249,15 +242,17 @@ class _WalletAddressesViewState extends ConsumerState { walletId: widget.walletId, addressId: snapshot.data![index], coin: coin, - onPressed: () { - Navigator.of(context).pushNamed( - AddressDetailsView.routeName, - arguments: Tuple2( - snapshot.data![index], - widget.walletId, - ), - ); - }, + onPressed: !isDesktop + ? null + : () { + Navigator.of(context).pushNamed( + AddressDetailsView.routeName, + arguments: Tuple2( + snapshot.data![index], + widget.walletId, + ), + ); + }, ), ); } else { diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index fab828bef..21089d30d 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -317,6 +317,28 @@ class STextStyles { } } + static TextStyle w600_18(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 18, + ); + } + } + + static TextStyle w500_16(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + } + } + static TextStyle w500_14(BuildContext context) { switch (_theme(context).themeId) { default: diff --git a/lib/widgets/custom_buttons/simple_edit_button.dart b/lib/widgets/custom_buttons/simple_edit_button.dart index 26b7042b9..99d470fe3 100644 --- a/lib/widgets/custom_buttons/simple_edit_button.dart +++ b/lib/widgets/custom_buttons/simple_edit_button.dart @@ -15,29 +15,31 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/icon_widgets/pencil_icon.dart'; import 'package:tuple/tuple.dart'; -import '../desktop/desktop_dialog.dart'; -import '../icon_widgets/pencil_icon.dart'; - class SimpleEditButton extends StatelessWidget { const SimpleEditButton({ - Key? key, + super.key, this.editValue, this.editLabel, + this.overrideTitle, + this.disableIcon = false, this.onValueChanged, this.onPressedOverride, - }) : assert( + }) : assert( (editLabel != null && editValue != null && onValueChanged != null) || (editLabel == null && editValue == null && onValueChanged == null && onPressedOverride != null), - ), - super(key: key); + ); final String? editValue; final String? editLabel; + final String? overrideTitle; + final bool disableIcon; final void Function(String)? onValueChanged; final VoidCallback? onPressedOverride; @@ -101,17 +103,20 @@ class SimpleEditButton extends StatelessWidget { }, child: Row( children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context).extension()!.infoItemIcons, - ), - const SizedBox( - width: 4, - ), + if (!disableIcon) + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: + Theme.of(context).extension()!.infoItemIcons, + ), + if (!disableIcon) + const SizedBox( + width: 4, + ), Text( - "Edit", + overrideTitle ?? "Edit", style: STextStyles.link2(context), ), ], From 083f38ffe1791e24320b8cac90721a70eecbbe63 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Apr 2024 10:52:14 -0500 Subject: [PATCH 156/272] Remove unused socks_socket package So the nonexistence of github.com:cypherstack/socks_socket doesn't interfere with `pub get`s --- pubspec.lock | 6 +++--- pubspec.yaml | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 25ba923a0..765f09bb1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1574,7 +1574,7 @@ packages: source: hosted version: "1.0.4" socks_socket: - dependency: "direct main" + dependency: transitive description: path: "." ref: master @@ -1586,8 +1586,8 @@ packages: dependency: "direct main" description: path: "packages/solana" - ref: "0ada1f775c2a2c815de640424270a229f5e91e2f" - resolved-ref: "0ada1f775c2a2c815de640424270a229f5e91e2f" + ref: a83e375678eb22fe544dc125d29bbec0fb833882 + resolved-ref: a83e375678eb22fe544dc125d29bbec0fb833882 url: "https://github.com/cypherstack/espresso-cash-public.git" source: git version: "0.30.4" diff --git a/pubspec.yaml b/pubspec.yaml index 723f2aae2..df55b3fb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -157,10 +157,6 @@ dependencies: nanodart: ^2.0.0 basic_utils: ^5.5.4 stellar_flutter_sdk: ^1.5.3 - socks_socket: - git: - url: https://github.com/cypherstack/socks_socket.git - ref: master bip340: ^0.2.0 # tezart: ^2.0.5 tezart: From 414760cc54ec085915fcccd79591fba55cfab827 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Apr 2024 13:57:39 -0500 Subject: [PATCH 157/272] pubspec todo nit --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2c432f6e0..832b24fdd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -157,7 +157,7 @@ dependencies: nanodart: ^2.0.0 basic_utils: ^5.5.4 stellar_flutter_sdk: # ^1.5.3 - git: # TODO Revert to official package once Tor support is merged upstream. + git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. url: https://github.com/cypherstack/stellar_flutter_sdk.git ref: eca1d730e952cf6a6d64502f977cfc03876b75d4 # tor-backport branch (based on 1.5.3). socks_socket: From e4d8a4af3651e437b676a2462d323be6012866f1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Apr 2024 14:00:44 -0500 Subject: [PATCH 158/272] solana has tor support --- lib/wallets/crypto_currency/coins/solana.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart index 632e0f977..193e48541 100644 --- a/lib/wallets/crypto_currency/coins/solana.dart +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -1,9 +1,9 @@ import 'package:solana/solana.dart'; import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; class Solana extends Bip39Currency { Solana(super.network) { @@ -20,7 +20,8 @@ class Solana extends Bip39Currency { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( - host: "https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one + host: + "https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one port: 443, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(Coin.solana), @@ -38,11 +39,15 @@ class Solana extends Bip39Currency { @override int get minConfirms => 21; + @override + bool get torSupport => true; + @override bool validateAddress(String address) { - return isPointOnEd25519Curve(Ed25519HDPublicKey.fromBase58(address).toByteArray()); + return isPointOnEd25519Curve( + Ed25519HDPublicKey.fromBase58(address).toByteArray()); } @override String get genesisHash => throw UnimplementedError(); -} \ No newline at end of file +} From 4f55765a53258d218f0efa47fcc2412f62faf2d1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Apr 2024 14:01:28 -0500 Subject: [PATCH 159/272] stellar has tor support --- lib/wallets/crypto_currency/coins/stellar.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/wallets/crypto_currency/coins/stellar.dart b/lib/wallets/crypto_currency/coins/stellar.dart index aa5f9ce4d..e9a7e605b 100644 --- a/lib/wallets/crypto_currency/coins/stellar.dart +++ b/lib/wallets/crypto_currency/coins/stellar.dart @@ -19,6 +19,9 @@ class Stellar extends Bip39Currency { @override int get minConfirms => 1; + @override + bool get torSupport => true; + @override String get genesisHash => throw UnimplementedError( "Not used for stellar", From 9e26913fc4d107a5651a80f484f56150ef4778b1 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Apr 2024 14:03:28 -0500 Subject: [PATCH 160/272] tezos has tor support --- lib/wallets/crypto_currency/coins/tezos.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart index ef0937da6..efb982867 100644 --- a/lib/wallets/crypto_currency/coins/tezos.dart +++ b/lib/wallets/crypto_currency/coins/tezos.dart @@ -70,6 +70,9 @@ class Tezos extends Bip39Currency { @override int get minConfirms => 1; + @override + bool get torSupport => true; + @override bool validateAddress(String address) { return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); From 81fd642735614f9a608d8721bc27e7b9940a443e Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Apr 2024 14:57:42 -0500 Subject: [PATCH 161/272] do not show multiple warnings when entering wallet remove WIP/original TorWarningDialog --- lib/widgets/wallet_card.dart | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 31a29fb68..bc1c80aa4 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -30,7 +30,6 @@ import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; @@ -95,37 +94,6 @@ class SimpleWalletCard extends ConsumerWidget { final wallet = ref.read(pWallets).getWallet(walletId); - // If Tor enabled, show a warning if opening a wallet incompatible with Tor. - if (ref.read(prefsChangeNotifierProvider).useTor) { - if (!wallet.cryptoCurrency.torSupport) { - final shouldContinue = await showDialog( - context: context, - builder: (context) => BasicDialog( - title: "Warning! Tor not supported.", - message: "Stacky is not compatible with Tor." - "\n\nBy using it, you will leak your IP address. Are you sure you " - "want to continue?", - // A PrimaryButton widget: - leftButton: PrimaryButton( - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: SecondaryButton( - label: "Continue", - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - )) ?? - false; - if (!shouldContinue) { - return; - } - } - } - if (context.mounted) { final Future loadFuture; if (wallet is CwBasedInterface) { From 01de393521f3eed4c6143e6c901312fc745d6f84 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 25 Apr 2024 15:35:04 -0500 Subject: [PATCH 162/272] fetch paynym bot image over Tor hard to test, as I cannot claim/generate nor detect a previously-claimed/generated nym over Tor due to ``` flutter: Log: [Info][2024-04-25 20:28:22.609Z]: HTTP.post() rethrew: Exception: Command handling failed. With error: serverError flutter: #0 SocksSocket._handleCommandResponse (package:socks5_proxy/src/client/socks_client.dart:196:7) flutter: flutter: #1 SocksSocket.initialize (package:socks5_proxy/src/client/socks_client.dart:74:22) flutter: flutter: #2 SocksTCPClient.connect (package:socks5_proxy/src/client/socks_tcp_client.dart:70:20) flutter: flutter: #3 SocksTCPClient.assignToHttpClientWithSecureOptions. (package:socks5_proxy/src/client/socks_tcp_client.dart:46:40) flutter: flutter: #4 _ConnectionTarget.connect. (dart:_http/http_impl.dart:2490:32) flutter: flutter: #5 _HttpClient._openUrl. (dart:_http/http_impl.dart:2787:15) flutter: flutter: #6 HTTP.post (package:stackwallet/networking/http.dart:85:41) flutter: flutter: #7 PaynymIsApi._post (package:stackwallet/utilities/paynym_is_api.dart:54:22) flutter: flutter: #8 PaynymIsApi.nym (package:stackwallet/utilities/paynym_is_api.dart:267:22) flutter: flutter: #9 Wallet.refresh (package:stackwallet/wallets/wallet/wallet.dart:511:21) flutter: ``` --- lib/pages/paynym/subwidgets/paynym_bot.dart | 43 ++++++++++++++++----- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/lib/pages/paynym/subwidgets/paynym_bot.dart b/lib/pages/paynym/subwidgets/paynym_bot.dart index d8f645da3..082f7034e 100644 --- a/lib/pages/paynym/subwidgets/paynym_bot.dart +++ b/lib/pages/paynym/subwidgets/paynym_bot.dart @@ -8,8 +8,12 @@ * */ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/networking/http.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/prefs.dart'; class PayNymBot extends StatelessWidget { const PayNymBot({ @@ -28,16 +32,37 @@ class PayNymBot extends StatelessWidget { child: SizedBox( width: size, height: size, - child: Image.network( - "https://paynym.is/$paymentCodeString/avatar", - loadingBuilder: (context, child, loadingProgress) => - loadingProgress == null - ? child - : const Center( - child: LoadingIndicator(), - ), + child: FutureBuilder( + future: _fetchImage(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Image.memory(snapshot.data!); + } else if (snapshot.hasError) { + return const Center(child: Icon(Icons.error)); + } else { + return const Center(); // TODO [prio=low]: Make better loading indicator. + } + }, ), ), ); } + + Future _fetchImage() async { + final HTTP client = HTTP(); + final Uri uri = Uri.parse("https://paynym.is/$paymentCodeString/avatar"); + + final response = await client.get( + url: uri, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + if (response.code == 200) { + return Uint8List.fromList(response.bodyBytes); + } else { + throw Exception('Failed to load image'); + } + } } From f279bf1f3b4bd2133816278114299a3ab0d125ff Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:19:01 +0300 Subject: [PATCH 163/272] fix deprecated fee func and null fees. --- lib/wallets/wallet/impl/solana_wallet.dart | 47 +++++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index b9b068ee4..39b8b7d77 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -1,9 +1,11 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:isar/isar.dart'; import 'package:socks5_proxy/socks_client.dart'; import 'package:solana/dto.dart'; +import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' @@ -53,6 +55,21 @@ class SolanaWallet extends Bip39Wallet { final balance = await _rpcClient?.getBalance((await _getKeyPair()).address); return balance!.value; } + + Future _getEstimatedNetworkFee(Amount transferAmount) async { + final latestBlockhash = await _rpcClient?.getLatestBlockhash(); + + final compiledMessage = Message(instructions: [ + SystemInstruction.transfer( + fundingAccount: (await _getKeyPair()).publicKey, + recipientAccount: (await _getKeyPair()).publicKey, + lamports: transferAmount.raw.toInt()), + ]).compile(recentBlockhash: latestBlockhash!.value.blockhash, feePayer: (await _getKeyPair()).publicKey); + + return await _rpcClient?.getFeeForMessage( + base64Encode(compiledMessage.toByteArray().toList()), + ); + } @override FilterOperation? get changeAddressFilterOperation => @@ -184,12 +201,14 @@ class SolanaWallet extends Bip39Wallet { fractionDigits: cryptoCurrency.fractionDigits, ); } - - final fee = await _rpcClient?.getFees(); - // TODO [prio=low]: handle null fee. - + + final fee = await _getEstimatedNetworkFee(amount); + if (fee == null) { + throw Exception("Failed to get fees, please check your node connection."); + } + return Amount( - rawValue: BigInt.from(fee!.value.feeCalculator.lamportsPerSignature), + rawValue: BigInt.from(fee as num), fractionDigits: cryptoCurrency.fractionDigits, ); } @@ -197,16 +216,22 @@ class SolanaWallet extends Bip39Wallet { @override Future get fees async { _checkClient(); - - final fees = await _rpcClient?.getFees(); - // TODO [prio=low]: handle null fees. + + final fee = await _getEstimatedNetworkFee(Amount( + rawValue: BigInt.from(1000000000), // 1 SOL + fractionDigits: cryptoCurrency.fractionDigits, + )); + if (fee == null) { + throw Exception("Failed to get fees, please check your node connection."); + } + return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 1, numberOfBlocksSlow: 1, - fast: fees!.value.feeCalculator.lamportsPerSignature, - medium: fees!.value.feeCalculator.lamportsPerSignature, - slow: fees!.value.feeCalculator.lamportsPerSignature); + fast: fee, + medium: fee, + slow: fee); } @override From cfe06f5b131a3446ffeb5f0938cecc13eb74f46c Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 11:01:26 -0600 Subject: [PATCH 164/272] clean up and optimize --- lib/wallets/wallet/impl/solana_wallet.dart | 37 +++++++++++++--------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 39b8b7d77..c9343ddfc 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:decimal/decimal.dart'; import 'package:isar/isar.dart'; import 'package:socks5_proxy/socks_client.dart'; import 'package:solana/dto.dart'; -import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' @@ -55,16 +55,21 @@ class SolanaWallet extends Bip39Wallet { final balance = await _rpcClient?.getBalance((await _getKeyPair()).address); return balance!.value; } - + Future _getEstimatedNetworkFee(Amount transferAmount) async { final latestBlockhash = await _rpcClient?.getLatestBlockhash(); + final pubKey = (await _getKeyPair()).publicKey; final compiledMessage = Message(instructions: [ SystemInstruction.transfer( - fundingAccount: (await _getKeyPair()).publicKey, - recipientAccount: (await _getKeyPair()).publicKey, - lamports: transferAmount.raw.toInt()), - ]).compile(recentBlockhash: latestBlockhash!.value.blockhash, feePayer: (await _getKeyPair()).publicKey); + fundingAccount: pubKey, + recipientAccount: pubKey, + lamports: transferAmount.raw.toInt(), + ), + ]).compile( + recentBlockhash: latestBlockhash!.value.blockhash, + feePayer: pubKey, + ); return await _rpcClient?.getFeeForMessage( base64Encode(compiledMessage.toByteArray().toList()), @@ -201,14 +206,14 @@ class SolanaWallet extends Bip39Wallet { fractionDigits: cryptoCurrency.fractionDigits, ); } - + final fee = await _getEstimatedNetworkFee(amount); if (fee == null) { throw Exception("Failed to get fees, please check your node connection."); } - + return Amount( - rawValue: BigInt.from(fee as num), + rawValue: BigInt.from(fee), fractionDigits: cryptoCurrency.fractionDigits, ); } @@ -216,15 +221,17 @@ class SolanaWallet extends Bip39Wallet { @override Future get fees async { _checkClient(); - - final fee = await _getEstimatedNetworkFee(Amount( - rawValue: BigInt.from(1000000000), // 1 SOL - fractionDigits: cryptoCurrency.fractionDigits, - )); + + final fee = await _getEstimatedNetworkFee( + Amount.fromDecimal( + Decimal.one, // 1 SOL + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); if (fee == null) { throw Exception("Failed to get fees, please check your node connection."); } - + return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 1, From bf3667da85dea8f464c80eb6abe7f384dbbcc732 Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Mon, 29 Apr 2024 20:19:09 +0300 Subject: [PATCH 165/272] solana fee estimation fixes --- lib/wallets/wallet/impl/solana_wallet.dart | 24 ++++++---------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index c9343ddfc..7a4aab388 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -18,7 +18,6 @@ import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; @@ -66,10 +65,8 @@ class SolanaWallet extends Bip39Wallet { recipientAccount: pubKey, lamports: transferAmount.raw.toInt(), ), - ]).compile( - recentBlockhash: latestBlockhash!.value.blockhash, - feePayer: pubKey, - ); + ComputeBudgetInstruction.setComputeUnitPrice(microLamports: 6000), + ]).compile(recentBlockhash: latestBlockhash!.value.blockhash, feePayer: (await _getKeyPair()).publicKey); return await _rpcClient?.getFeeForMessage( base64Encode(compiledMessage.toByteArray().toList()), @@ -118,19 +115,10 @@ class SolanaWallet extends Bip39Wallet { throw Exception("Insufficient available balance"); } - int feeAmount; - final currentFees = await fees; - switch (txData.feeRateType) { - case FeeRateType.fast: - feeAmount = currentFees.fast; - break; - case FeeRateType.slow: - feeAmount = currentFees.slow; - break; - case FeeRateType.average: - default: - feeAmount = currentFees.medium; - break; + final feeAmount = await _getEstimatedNetworkFee(sendAmount); + if (feeAmount == null) { + throw Exception( + "Failed to get fees, please check your node connection."); } // Rent exemption of Solana From 87a96b9b09335b64d983736c8aa501b8f8dbf0e8 Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Tue, 30 Apr 2024 00:18:54 +0300 Subject: [PATCH 166/272] sol: fee set fix and sent to self fix --- lib/wallets/wallet/impl/solana_wallet.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 7a4aab388..e7d93ed0f 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -64,8 +64,7 @@ class SolanaWallet extends Bip39Wallet { fundingAccount: pubKey, recipientAccount: pubKey, lamports: transferAmount.raw.toInt(), - ), - ComputeBudgetInstruction.setComputeUnitPrice(microLamports: 6000), + ) ]).compile(recentBlockhash: latestBlockhash!.value.blockhash, feePayer: (await _getKeyPair()).publicKey); return await _rpcClient?.getFeeForMessage( @@ -167,7 +166,12 @@ class SolanaWallet extends Bip39Wallet { recipientAccount: recipientPubKey, lamports: txData.amount!.raw.toInt()), ComputeBudgetInstruction.setComputeUnitPrice( - microLamports: txData.fee!.raw.toInt()), + microLamports: txData.fee!.raw.toInt() - 5000), + // 5000 lamports is the base fee for a transaction. This instruction adds the necessary fee on top of base fee if it is needed. + ComputeBudgetInstruction.setComputeUnitLimit(units: 1000000), + // 1000000 is the multiplication number to turn the compute unit price of microLamports to lamports. + // These instructions also help the user to not pay more than the shown fee. + // See: https://solanacookbook.com/references/basic-transactions.html#how-to-change-compute-budget-fee-priority-for-a-transaction ], ); @@ -367,7 +371,7 @@ class SolanaWallet extends Bip39Wallet { for (final tx in transactionsList!) { final senderAddress = (tx.transaction as ParsedTransaction).message.accountKeys[0].pubkey; - final receiverAddress = + var receiverAddress = (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; var txType = isar.TransactionType.unknown; final txAmount = Amount( @@ -376,9 +380,10 @@ class SolanaWallet extends Bip39Wallet { fractionDigits: cryptoCurrency.fractionDigits, ); - if ((senderAddress == (await _getKeyPair()).address) && - (receiverAddress == (await _getKeyPair()).address)) { + if ((senderAddress == (await _getKeyPair()).address) && (receiverAddress == "11111111111111111111111111111111")) { + // The account that is only 1's are System Program accounts which means there is no receiver except the sender, see: https://explorer.solana.com/address/11111111111111111111111111111111 txType = isar.TransactionType.sentToSelf; + receiverAddress = senderAddress; } else if (senderAddress == (await _getKeyPair()).address) { txType = isar.TransactionType.outgoing; } else if (receiverAddress == (await _getKeyPair()).address) { From 1101b8c9326a23249228c1c8b99b79fd326ea37e Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 May 2024 09:33:59 -0600 Subject: [PATCH 167/272] clean up sol --- lib/wallets/crypto_currency/coins/solana.dart | 18 +++- lib/wallets/wallet/impl/solana_wallet.dart | 97 +++++++++++-------- 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart index 632e0f977..0307d6bde 100644 --- a/lib/wallets/crypto_currency/coins/solana.dart +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -1,9 +1,9 @@ import 'package:solana/solana.dart'; import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; class Solana extends Bip39Currency { Solana(super.network) { @@ -20,7 +20,8 @@ class Solana extends Bip39Currency { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( - host: "https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one + host: + "https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one port: 443, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(Coin.solana), @@ -40,9 +41,18 @@ class Solana extends Bip39Currency { @override bool validateAddress(String address) { - return isPointOnEd25519Curve(Ed25519HDPublicKey.fromBase58(address).toByteArray()); + return isPointOnEd25519Curve( + Ed25519HDPublicKey.fromBase58(address).toByteArray()); } @override String get genesisHash => throw UnimplementedError(); -} \ No newline at end of file + + @override + bool operator ==(Object other) { + return other is Solana && other.network == network; + } + + @override + int get hashCode => Object.hash(Solana, network); +} diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index e7d93ed0f..d2f3582c9 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -28,24 +28,30 @@ import 'package:tuple/tuple.dart'; class SolanaWallet extends Bip39Wallet { SolanaWallet(CryptoCurrencyNetwork network) : super(Solana(network)); + static const String _addressDerivationPath = "m/44'/501'/0'/0'"; + NodeModel? _solNode; RpcClient? _rpcClient; // The Solana RpcClient. Future _getKeyPair() async { - return Ed25519HDKeyPair.fromMnemonic(await getMnemonic(), - account: 0, change: 0); + return Ed25519HDKeyPair.fromMnemonic( + await getMnemonic(), + account: 0, + change: 0, + ); } - Future
_getCurrentAddress() async { + Future
_generateAddress() async { final addressStruct = Address( - walletId: walletId, - value: (await _getKeyPair()).address, - publicKey: List.empty(), - derivationIndex: 0, - derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'", - type: cryptoCurrency.coin.primaryAddressType, - subType: AddressSubType.unknown); + walletId: walletId, + value: (await _getKeyPair()).address, + publicKey: List.empty(), + derivationIndex: 0, + derivationPath: DerivationPath()..value = _addressDerivationPath, + type: cryptoCurrency.coin.primaryAddressType, + subType: AddressSubType.receiving, + ); return addressStruct; } @@ -65,7 +71,10 @@ class SolanaWallet extends Bip39Wallet { recipientAccount: pubKey, lamports: transferAmount.raw.toInt(), ) - ]).compile(recentBlockhash: latestBlockhash!.value.blockhash, feePayer: (await _getKeyPair()).publicKey); + ]).compile( + recentBlockhash: latestBlockhash!.value.blockhash, + feePayer: pubKey, + ); return await _rpcClient?.getFeeForMessage( base64Encode(compiledMessage.toByteArray().toList()), @@ -79,18 +88,13 @@ class SolanaWallet extends Bip39Wallet { @override Future checkSaveInitialReceivingAddress() async { try { - final address = (await _getKeyPair()).address; + Address? address = await getCurrentReceivingAddress(); - await mainDB.updateOrPutAddresses([ - Address( - walletId: walletId, - value: address, - publicKey: List.empty(), - derivationIndex: 0, - derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'", - type: cryptoCurrency.coin.primaryAddressType, - subType: AddressSubType.unknown) - ]); + if (address == null) { + address = await _generateAddress(); + + await mainDB.updateOrPutAddresses([address]); + } } catch (e, s) { Logging.instance.log( "$runtimeType checkSaveInitialReceivingAddress() failed: $e\n$s", @@ -120,9 +124,10 @@ class SolanaWallet extends Bip39Wallet { "Failed to get fees, please check your node connection."); } + final address = await getCurrentReceivingAddress(); + // Rent exemption of Solana - final accInfo = - await _rpcClient?.getAccountInfo((await _getKeyPair()).address); + final accInfo = await _rpcClient?.getAccountInfo(address!.value); final int minimumRent = await _rpcClient?.getMinimumBalanceForRentExemption( accInfo!.value!.data.toString().length) ?? @@ -132,7 +137,9 @@ class SolanaWallet extends Bip39Wallet { txData.amount!.raw.toInt() - feeAmount)) { throw Exception( - "Insufficient remaining balance for rent exemption, minimum rent: ${minimumRent / pow(10, cryptoCurrency.fractionDigits)}"); + "Insufficient remaining balance for rent exemption, minimum rent: " + "${minimumRent / pow(10, cryptoCurrency.fractionDigits)}", + ); } return txData.copyWith( @@ -255,7 +262,7 @@ class SolanaWallet extends Bip39Wallet { @override Future recover({required bool isRescan}) async { await refreshMutex.protect(() async { - final addressStruct = await _getCurrentAddress(); + final addressStruct = await _generateAddress(); await mainDB.updateOrPutAddresses([addressStruct]); @@ -277,13 +284,13 @@ class SolanaWallet extends Bip39Wallet { @override Future updateBalance() async { try { + final address = await getCurrentReceivingAddress(); _checkClient(); - final balance = await _rpcClient?.getBalance(info.cachedReceivingAddress); + final balance = await _rpcClient?.getBalance(address!.value); // Rent exemption of Solana - final accInfo = - await _rpcClient?.getAccountInfo((await _getKeyPair()).address); + final accInfo = await _rpcClient?.getAccountInfo(address!.value); // TODO [prio=low]: handle null account info. final int minimumRent = await _rpcClient?.getMinimumBalanceForRentExemption( @@ -366,6 +373,8 @@ class SolanaWallet extends Bip39Wallet { final txsList = List>.empty(growable: true); + final myAddress = (await getCurrentReceivingAddress())!; + // TODO [prio=low]: Revisit null assertion below. for (final tx in transactionsList!) { @@ -380,13 +389,16 @@ class SolanaWallet extends Bip39Wallet { fractionDigits: cryptoCurrency.fractionDigits, ); - if ((senderAddress == (await _getKeyPair()).address) && (receiverAddress == "11111111111111111111111111111111")) { - // The account that is only 1's are System Program accounts which means there is no receiver except the sender, see: https://explorer.solana.com/address/11111111111111111111111111111111 + if ((senderAddress == myAddress.value) && + (receiverAddress == "11111111111111111111111111111111")) { + // The account that is only 1's are System Program accounts which + // means there is no receiver except the sender, + // see: https://explorer.solana.com/address/11111111111111111111111111111111 txType = isar.TransactionType.sentToSelf; receiverAddress = senderAddress; - } else if (senderAddress == (await _getKeyPair()).address) { + } else if (senderAddress == myAddress.value) { txType = isar.TransactionType.outgoing; - } else if (receiverAddress == (await _getKeyPair()).address) { + } else if (receiverAddress == myAddress.value) { txType = isar.TransactionType.incoming; } @@ -411,15 +423,16 @@ class SolanaWallet extends Bip39Wallet { ); final txAddress = Address( - walletId: walletId, - value: receiverAddress, - publicKey: List.empty(), - derivationIndex: 0, - derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'", - type: AddressType.solana, - subType: txType == isar.TransactionType.outgoing - ? AddressSubType.unknown - : AddressSubType.receiving); + walletId: walletId, + value: receiverAddress, + publicKey: List.empty(), + derivationIndex: 0, + derivationPath: DerivationPath()..value = _addressDerivationPath, + type: AddressType.solana, + subType: txType == isar.TransactionType.outgoing + ? AddressSubType.unknown + : AddressSubType.receiving, + ); txsList.add(Tuple2(transaction, txAddress)); } From d145ec9546bcd9fc85a0cbcbbeb4a34aa379c401 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 25 Apr 2024 08:51:54 -0600 Subject: [PATCH 168/272] move widget to widgets directory --- ...firm_new_frost_ms_wallet_creation_view.dart | 8 ++++---- .../new/create_new_frost_ms_wallet_view.dart | 5 +++-- .../new/frost_share_commitments_view.dart | 5 +++-- .../frost_ms/new/frost_share_shares_view.dart | 5 +++-- .../new/import_new_frost_ms_wallet_view.dart | 6 +++--- .../new/share_new_multisig_config_view.dart | 5 +++-- .../restore/restore_frost_ms_wallet_view.dart | 18 ++++++++---------- .../resharing/finish_resharing_view.dart | 6 +++--- .../new/new_continue_sharing_view.dart | 6 +++--- .../new/new_import_resharer_config_view.dart | 7 +++---- .../new/new_start_resharing_view.dart | 6 +++--- lib/{pages => widgets}/frost_mascot.dart | 12 ++++++++---- 12 files changed, 47 insertions(+), 42 deletions(-) rename lib/{pages => widgets}/frost_mascot.dart (81%) diff --git a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart index ffd1127b6..fad725653 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/frost_mascot.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_desktop_specific/desktop_home_view.dart'; @@ -21,6 +20,7 @@ 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/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'; @@ -32,10 +32,9 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/frost_mascot.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; -import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; - class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget { const ConfirmNewFrostMSWalletCreationView({ super.key, @@ -105,7 +104,8 @@ class _ConfirmNewFrostMSWalletCreationViewState ), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index 9f42a3fa6..c9995dfde 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; @@ -15,6 +14,7 @@ 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/stack_dialog.dart'; class CreateNewFrostMsWalletView extends ConsumerStatefulWidget { @@ -123,7 +123,8 @@ class _NewFrostMsWalletViewState leading: AppBarBackButton(), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart index 1234dbf8a..3e8d28518 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; @@ -25,6 +24,7 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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'; @@ -123,7 +123,8 @@ class _FrostShareCommitmentsViewState ), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart index 20ac39c03..97e9898bd 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; @@ -25,6 +24,7 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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'; @@ -122,7 +122,8 @@ class _FrostShareSharesViewState extends ConsumerState { ), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart index 4eeb3a045..ef1af8180 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart @@ -17,6 +17,7 @@ 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'; @@ -24,8 +25,6 @@ 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:stackwallet/pages/frost_mascot.dart'; - class ImportNewFrostMsWalletView extends ConsumerStatefulWidget { const ImportNewFrostMsWalletView({ super.key, @@ -79,7 +78,8 @@ class _ImportNewFrostMsWalletViewState leading: AppBarBackButton(), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart index 7d463c4ca..348792306 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/frost_mascot.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; @@ -18,6 +17,7 @@ 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/detail_item.dart'; +import 'package:stackwallet/widgets/frost_mascot.dart'; class ShareNewMultisigConfigView extends ConsumerStatefulWidget { const ShareNewMultisigConfigView({ @@ -49,7 +49,8 @@ class _ShareNewMultisigConfigViewState leading: AppBarBackButton(), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 08f36ebde..118ff60bb 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -8,7 +8,6 @@ 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/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/global/node_service_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; @@ -33,6 +32,7 @@ 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'; @@ -40,8 +40,6 @@ 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:stackwallet/pages/frost_mascot.dart'; - class RestoreFrostMsWalletView extends ConsumerStatefulWidget { const RestoreFrostMsWalletView({ super.key, @@ -215,13 +213,13 @@ class _RestoreFrostMsWalletViewState builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ) - ), + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: FrostMascot( + title: 'Lorem ipsum', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + )), body: SizedBox( width: 480, child: child, diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart index 5ff5c815d..a0ba76770 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart @@ -26,6 +26,7 @@ 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/detail_item.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'; @@ -33,8 +34,6 @@ 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:stackwallet/pages/frost_mascot.dart'; - class FinishResharingView extends ConsumerStatefulWidget { const FinishResharingView({ super.key, @@ -186,7 +185,8 @@ class _FinishResharingViewState extends ConsumerState { leading: AppBarBackButton(), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart index 5e4ed4762..f365667a2 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart @@ -18,8 +18,7 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; - -import 'package:stackwallet/pages/frost_mascot.dart'; +import 'package:stackwallet/widgets/frost_mascot.dart'; class NewContinueSharingView extends ConsumerStatefulWidget { const NewContinueSharingView({ @@ -71,7 +70,8 @@ class _NewContinueSharingViewState ), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart index 698363923..fd96236cb 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -20,6 +19,7 @@ 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'; @@ -27,8 +27,6 @@ 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:stackwallet/pages/frost_mascot.dart'; - class NewImportResharerConfigView extends ConsumerStatefulWidget { const NewImportResharerConfigView({ super.key, @@ -96,7 +94,8 @@ class _NewImportResharerConfigViewState leading: AppBarBackButton(), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart index 7173eff3d..5feda5742 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart @@ -19,6 +19,7 @@ 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/frost_interruption_dialog.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'; @@ -26,8 +27,6 @@ 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:stackwallet/pages/frost_mascot.dart'; - class NewStartResharingView extends ConsumerStatefulWidget { const NewStartResharingView({ super.key, @@ -149,7 +148,8 @@ class _NewStartResharingViewState extends ConsumerState { ), trailing: FrostMascot( title: 'Lorem ipsum', - body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', ), ), body: SizedBox( diff --git a/lib/pages/frost_mascot.dart b/lib/widgets/frost_mascot.dart similarity index 81% rename from lib/pages/frost_mascot.dart rename to lib/widgets/frost_mascot.dart index 3f6c0562d..6be496272 100644 --- a/lib/pages/frost_mascot.dart +++ b/lib/widgets/frost_mascot.dart @@ -15,9 +15,11 @@ import 'package:stackwallet/utilities/assets.dart'; class FrostMascot extends StatelessWidget { final String title; final String body; - FrostMascot({ + const FrostMascot({ super.key, - this.onPressed, required this.title, required this.body, + this.onPressed, + required this.title, + required this.body, }); final VoidCallback? onPressed; @@ -32,8 +34,10 @@ class FrostMascot extends StatelessWidget { onTap: () async { await showDialog( context: context, - builder: (context) => - FrostStepExplanationDialog(title: title, body: body), + builder: (context) => FrostStepExplanationDialog( + title: title, + body: body, + ), ); }, child: Image( From 4df2a7d2147a46ef4d909f96aa91a1d738d16fb0 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 25 Apr 2024 09:16:43 -0600 Subject: [PATCH 169/272] remove unused frost wallet toggle --- .../add_wallet_view/add_wallet_view.dart | 11 +--------- lib/utilities/prefs.dart | 22 ------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index d84bfc708..6d0582c5c 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -46,7 +46,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,11 +134,6 @@ class _AddWalletViewState extends ConsumerState { _coins.remove(Coin.wownero); } - // Remove FROST from the list of coins based on our frostEnabled preference. - if (!ref.read(prefsChangeNotifierProvider).frostEnabled) { - _coins.remove(Coin.bitcoinFrost); - } - // Remove Solana from the list of coins based on our frostEnabled preference. if (!ref.read(prefsChangeNotifierProvider).solanaEnabled) { _coins.remove(Coin.solana); @@ -147,10 +142,6 @@ class _AddWalletViewState extends ConsumerState { coinEntities.addAll(_coins.map((e) => CoinEntity(e))); if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) { - if (!ref.read(prefsChangeNotifierProvider).frostEnabled) { - _coinsTestnet.remove(Coin.bitcoinFrostTestNet); - } - coinEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e))); } diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index d380901c3..fc3d29e71 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -69,7 +69,6 @@ class Prefs extends ChangeNotifier { _useTor = await _getUseTor(); _fusionServerInfo = await _getFusionServerInfo(); _solanaEnabled = await _getSolanaEnabled(); - _frostEnabled = await _getFrostEnabled(); _initialized = true; } @@ -1031,25 +1030,4 @@ class Prefs extends ChangeNotifier { boxName: DB.boxNamePrefs, key: "solanaEnabled") as bool? ?? false; } - - // FROST multisig - - bool _frostEnabled = false; - - bool get frostEnabled => _frostEnabled; - - set frostEnabled(bool frostEnabled) { - if (_frostEnabled != frostEnabled) { - DB.instance.put( - boxName: DB.boxNamePrefs, key: "frostEnabled", value: frostEnabled); - _frostEnabled = frostEnabled; - notifyListeners(); - } - } - - Future _getFrostEnabled() async { - return await DB.instance.get( - boxName: DB.boxNamePrefs, key: "frostEnabled") as bool? ?? - false; - } } From 292b8a03c032c856eb3e4a1ebda1ee5fd8866c77 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 25 Apr 2024 11:44:59 -0600 Subject: [PATCH 170/272] prep frost ui refactor --- ...irm_new_frost_ms_wallet_creation_view.dart | 11 +-- .../new/create_new_frost_ms_wallet_view.dart | 11 +-- .../new/frost_share_commitments_view.dart | 11 +-- .../frost_ms/new/frost_share_shares_view.dart | 11 +-- .../new/import_new_frost_ms_wallet_view.dart | 11 +-- .../new/share_new_multisig_config_view.dart | 11 +-- .../restore/restore_frost_ms_wallet_view.dart | 26 +++---- .../name_your_wallet_view.dart | 71 ++++++++++++------- .../new/new_import_resharer_config_view.dart | 15 ++-- lib/route_generator.dart | 34 ++++----- lib/utilities/text_styles.dart | 22 ++++++ .../crypto_currency/coins/bitcoin_frost.dart | 2 +- ..._key_currency.dart => frost_currency.dart} | 0 .../wallet/impl/bitcoin_frost_wallet.dart | 2 +- 14 files changed, 145 insertions(+), 93 deletions(-) rename lib/wallets/crypto_currency/intermediate/{private_key_currency.dart => frost_currency.dart} (100%) diff --git a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart index fad725653..184dd91d1 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart @@ -16,10 +16,10 @@ 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/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/wallets/crypto_currency/intermediate/frost_currency.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'; @@ -39,13 +39,13 @@ class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget { const ConfirmNewFrostMSWalletCreationView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/confirmNewFrostMSWalletCreationView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -102,7 +102,8 @@ class _ConfirmNewFrostMSWalletCreationViewState ); }, ), - trailing: FrostMascot( + // TODO: [prio=high] get rid of placeholder text?? + trailing: const FrostMascot( title: 'Lorem ipsum', body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', @@ -230,7 +231,7 @@ class _ConfirmNewFrostMSWalletCreationViewState ); final info = WalletInfo.createNew( - coin: widget.coin, + coin: widget.frostCurrency.coin, name: widget.walletName, ); diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index c9995dfde..9ea89de44 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -5,9 +5,9 @@ import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multis 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/enums/coin_enum.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'; @@ -21,13 +21,13 @@ class CreateNewFrostMsWalletView extends ConsumerStatefulWidget { const CreateNewFrostMsWalletView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/createNewFrostMsWalletView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -118,9 +118,10 @@ class _NewFrostMsWalletViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( + appBar: const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? trailing: FrostMascot( title: 'Lorem ipsum', body: @@ -276,7 +277,7 @@ class _NewFrostMsWalletViewState ShareNewMultisigConfigView.routeName, arguments: ( walletName: widget.walletName, - coin: widget.coin, + frostCurrency: widget.frostCurrency, ), ); }, diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart index 3e8d28518..e754db2bd 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart @@ -11,10 +11,10 @@ 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/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/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'; @@ -36,13 +36,13 @@ class FrostShareCommitmentsView extends ConsumerStatefulWidget { const FrostShareCommitmentsView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/frostShareCommitmentsView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -121,7 +121,8 @@ class _FrostShareCommitmentsViewState ); }, ), - trailing: FrostMascot( + // TODO: [prio=high] get rid of placeholder text?? + trailing: const FrostMascot( title: 'Lorem ipsum', body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', @@ -397,7 +398,7 @@ class _FrostShareCommitmentsViewState FrostShareSharesView.routeName, arguments: ( walletName: widget.walletName, - coin: widget.coin, + frostCurrency: widget.frostCurrency, ), ); } catch (e, s) { diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart index 97e9898bd..ef5927b9e 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart @@ -11,10 +11,10 @@ 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/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/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'; @@ -36,13 +36,13 @@ class FrostShareSharesView extends ConsumerStatefulWidget { const FrostShareSharesView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/frostShareSharesView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -120,7 +120,8 @@ class _FrostShareSharesViewState extends ConsumerState { ); }, ), - trailing: FrostMascot( + // TODO: [prio=high] get rid of placeholder text?? + trailing: const FrostMascot( title: 'Lorem ipsum', body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', @@ -364,7 +365,7 @@ class _FrostShareSharesViewState extends ConsumerState { ConfirmNewFrostMSWalletCreationView.routeName, arguments: ( walletName: widget.walletName, - coin: widget.coin, + frostCurrency: widget.frostCurrency, ), ); } catch (e, s) { diff --git a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart index ef1af8180..bd9724984 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart @@ -7,10 +7,10 @@ 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/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/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'; @@ -29,13 +29,13 @@ class ImportNewFrostMsWalletView extends ConsumerStatefulWidget { const ImportNewFrostMsWalletView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/importNewFrostMsWalletView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -73,9 +73,10 @@ class _ImportNewFrostMsWalletViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( + appBar: const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? trailing: FrostMascot( title: 'Lorem ipsum', body: @@ -377,7 +378,7 @@ class _ImportNewFrostMsWalletViewState FrostShareCommitmentsView.routeName, arguments: ( walletName: widget.walletName, - coin: widget.coin, + frostCurrency: widget.frostCurrency, ), ); }, diff --git a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart index 348792306..9f8862070 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart @@ -6,9 +6,9 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_deta 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/enums/coin_enum.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'; @@ -23,13 +23,13 @@ class ShareNewMultisigConfigView extends ConsumerStatefulWidget { const ShareNewMultisigConfigView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/shareNewMultisigConfigView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -44,9 +44,10 @@ class _ShareNewMultisigConfigViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( + appBar: const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? trailing: FrostMascot( title: 'Lorem ipsum', body: @@ -153,7 +154,7 @@ class _ShareNewMultisigConfigViewState FrostShareCommitmentsView.routeName, arguments: ( walletName: widget.walletName, - coin: widget.coin, + frostCurrency: widget.frostCurrency, ), ); }, diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 118ff60bb..2d4846289 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -17,11 +17,11 @@ 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/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/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'; @@ -44,13 +44,13 @@ class RestoreFrostMsWalletView extends ConsumerStatefulWidget { const RestoreFrostMsWalletView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/restoreFrostMsWalletView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -75,7 +75,7 @@ class _RestoreFrostMsWalletViewState final myName = participants[myNameIndex]; final info = WalletInfo.createNew( - coin: widget.coin, + coin: widget.frostCurrency.coin, name: widget.walletName, ); @@ -212,14 +212,16 @@ class _RestoreFrostMsWalletViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - )), + 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, diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index 7ddaaba3a..b500ba32b 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -15,12 +15,11 @@ 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/import_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'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_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/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart'; @@ -31,6 +30,8 @@ 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'; @@ -243,7 +244,7 @@ class _NameYourWalletViewState extends ConsumerState { height: isDesktop ? 0 : 16, ), Text( - "Name your ${coin.prettyName} wallet", + "Name your ${coin.prettyName} ${coin.isFrost ? "multisig " : ""}wallet", textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopH2(context) @@ -253,7 +254,7 @@ class _NameYourWalletViewState extends ConsumerState { height: isDesktop ? 16 : 8, ), Text( - "Enter a label for your wallet (e.g. Savings)", + "Enter a label for your wallet (e.g. ${coin.isFrost ? "Multisig" : "Savings"})", textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopSubtitleH2(context) @@ -403,7 +404,7 @@ class _NameYourWalletViewState extends ConsumerState { Column( children: [ PrimaryButton( - label: "Create config", + label: "Create new group", enabled: _nextEnabled, onPressed: () async { final name = textEditingController.text; @@ -421,38 +422,56 @@ class _NameYourWalletViewState extends ConsumerState { height: 12, ), SecondaryButton( - label: "Import multisig config", + label: "Join group", enabled: _nextEnabled, onPressed: () async { final name = textEditingController.text; await Navigator.of(context).pushNamed( - ImportNewFrostMsWalletView.routeName, + SelectNewFrostImportTypeView.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, + // 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, + // ), + // ); + // }, + // ), ], ), if (!widget.coin.isFrost) diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart index fd96236cb..ec3f00408 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart @@ -6,11 +6,11 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/r import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.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/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/wallet_info.dart'; import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -31,13 +31,13 @@ class NewImportResharerConfigView extends ConsumerStatefulWidget { const NewImportResharerConfigView({ super.key, required this.walletName, - required this.coin, + required this.frostCurrency, }); static const String routeName = "/newImportResharerConfigView"; final String walletName; - final Coin coin; + final FrostCurrency frostCurrency; @override ConsumerState createState() => @@ -56,7 +56,7 @@ class _NewImportResharerConfigViewState Future _createWallet() async { final info = WalletInfo.createNew( name: widget.walletName, - coin: widget.coin, + coin: widget.frostCurrency.coin, ); final wallet = IncompleteFrostWallet(); @@ -89,9 +89,10 @@ class _NewImportResharerConfigViewState condition: Util.isDesktop, builder: (child) => DesktopScaffold( background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( + appBar: const DesktopAppBar( isCompactHeight: false, leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? trailing: FrostMascot( title: 'Lorem ipsum', body: @@ -395,7 +396,7 @@ class _NewImportResharerConfigViewState throw ex!; } - if (mounted) { + if (context.mounted) { ref.read(pFrostResharingData).incompleteWallet = wallet!; await Navigator.of(context).pushNamed( NewStartResharingView.routeName, @@ -408,7 +409,7 @@ class _NewImportResharerConfigViewState level: LogLevel.Fatal, ); - if (mounted) { + if (context.mounted) { await showDialog( context: context, builder: (_) => StackOkDialog( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index e4d188cdf..ace0e7c88 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -31,6 +31,7 @@ import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_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/new/share_new_multisig_config_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; @@ -205,6 +206,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/widgets/choose_coin_view.dart'; @@ -450,13 +452,13 @@ class RouteGenerator { case CreateNewFrostMsWalletView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => CreateNewFrostMsWalletView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -468,13 +470,13 @@ class RouteGenerator { case RestoreFrostMsWalletView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => RestoreFrostMsWalletView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -486,13 +488,13 @@ class RouteGenerator { case ShareNewMultisigConfigView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => ShareNewMultisigConfigView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -504,13 +506,13 @@ class RouteGenerator { case ImportNewFrostMsWalletView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => ImportNewFrostMsWalletView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -522,13 +524,13 @@ class RouteGenerator { case NewImportResharerConfigView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => NewImportResharerConfigView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -568,13 +570,13 @@ class RouteGenerator { case FrostShareCommitmentsView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => FrostShareCommitmentsView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -586,13 +588,13 @@ class RouteGenerator { case FrostShareSharesView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => FrostShareSharesView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, @@ -604,13 +606,13 @@ class RouteGenerator { case ConfirmNewFrostMSWalletCreationView.routeName: if (args is ({ String walletName, - Coin coin, + FrostCurrency frostCurrency, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => ConfirmNewFrostMSWalletCreationView( walletName: args.walletName, - coin: args.coin, + frostCurrency: args.frostCurrency, ), settings: RouteSettings( name: settings.name, diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 21089d30d..0a7383e9f 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -383,6 +383,28 @@ class STextStyles { } } + static TextStyle w400_16(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); + } + } + + static TextStyle w400_14(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 14, + ); + } + } + static TextStyle w600_20(BuildContext context) { switch (_theme(context).themeId) { default: diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index 38530f056..4bd1a6af9 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -6,7 +6,7 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; -import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; class BitcoinFrost extends FrostCurrency { BitcoinFrost(super.network) { diff --git a/lib/wallets/crypto_currency/intermediate/private_key_currency.dart b/lib/wallets/crypto_currency/intermediate/frost_currency.dart similarity index 100% rename from lib/wallets/crypto_currency/intermediate/private_key_currency.dart rename to lib/wallets/crypto_currency/intermediate/frost_currency.dart diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index d4cd2b1e2..734fe2bbc 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -23,7 +23,7 @@ import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; -import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; From 1f75c6b6e792fc97a56cdf1ba74309571c0f5164 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 25 Apr 2024 11:45:28 -0600 Subject: [PATCH 171/272] select frost import config type selection screen --- .../select_new_frost_import_type_view.dart | 394 ++++++++++++++++++ lib/route_generator.dart | 18 + 2 files changed, 412 insertions(+) create mode 100644 lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart new file mode 100644 index 000000000..80205afd9 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -0,0 +1,394 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.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/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class SelectNewFrostImportTypeView extends StatefulWidget { + const SelectNewFrostImportTypeView({ + super.key, + required this.walletName, + required this.frostCurrency, + }); + + static const String routeName = "/selectNewFrostImportTypeView"; + + final String walletName; + final FrostCurrency frostCurrency; + + @override + State createState() => + _SelectNewFrostImportTypeViewState(); +} + +class _SelectNewFrostImportTypeViewState + extends State { + _ImportOption _selectedOption = _ImportOption.multisigNew; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (content) => DesktopScaffold( + appBar: const DesktopAppBar( + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + isCompactHeight: false, + ), + body: SizedBox( + width: 480, + child: content, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (content) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + colorFilter: ColorFilter.mode( + Theme.of(context) + .extension()! + .topNavIconPrimary, + BlendMode.srcIn, + ), + ), + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const _FrostJoinInfoDialog(), + ); + }, + ), + ), + ], + ), + body: Container( + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (ctx, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: content, + ), + ), + ); + }, + ), + ), + ), + ), + ), + child: Column( + children: [ + ..._ImportOption.values.map( + (e) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _ImportOptionCard( + onPressed: () => setState(() => _selectedOption = e), + title: e.info, + description: e.description, + value: e, + groupValue: _selectedOption, + ), + ), + ), + const Spacer(), + PrimaryButton( + label: "Continue", + onPressed: () async { + final String route; + switch (_selectedOption) { + case _ImportOption.multisigNew: + route = ImportNewFrostMsWalletView.routeName; + case _ImportOption.resharerExisting: + route = NewImportResharerConfigView.routeName; + } + + await Navigator.of(context).pushNamed( + route, + arguments: ( + walletName: widget.walletName, + frostCurrency: widget.frostCurrency, + ), + ); + }, + ) + ], + ), + ), + ); + } +} + +enum _ImportOption { + multisigNew, + resharerExisting; + + String get info { + switch (this) { + case _ImportOption.multisigNew: + return "I want to join a new group"; + case _ImportOption.resharerExisting: + return "I want to join an existing group"; + } + } + + String get description { + switch (this) { + case _ImportOption.multisigNew: + return "You are currently participating in the process of creating a new group"; + case _ImportOption.resharerExisting: + return "You are joining an existing group through the process of resharing"; + } + } +} + +class _ImportOptionCard extends StatefulWidget { + const _ImportOptionCard({ + super.key, + required this.onPressed, + required this.title, + required this.description, + required this.value, + required this.groupValue, + }); + + final VoidCallback onPressed; + final String title; + final String description; + final _ImportOption value; + final _ImportOption groupValue; + + @override + State<_ImportOptionCard> createState() => _ImportOptionCardState(); +} + +class _ImportOptionCardState extends State<_ImportOptionCard> { + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + onPressed: widget.onPressed, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Radio( + value: widget.value, + groupValue: widget.groupValue, + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + onChanged: (_) => widget.onPressed(), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 12.0, + right: 12.0, + bottom: 12.0, + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + widget.title, + style: STextStyles.w600_16(context), + ), + ), + ], + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + Expanded( + child: Text( + widget.description, + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _FrostJoinInfoDialog extends StatelessWidget { + const _FrostJoinInfoDialog({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.all(16), + child: Material( + borderRadius: BorderRadius.circular( + 20, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // TODO: [prio=high] need text from designers! + Text( + "Join a group", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + Text( + "Text here", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "What is resharing?", + style: STextStyles.w600_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "In cryptocurrency, you are your own bank." + " Imagine keeping cash at home. If that cash" + " burns down or gets stolen, you lose it and" + " nobody will help you get your money back.", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Since cryptocurrency is digital money, your " + "wallet key is like that “cash” you keep at " + "home. If you lose your phone or if you " + "forget your wallet PIN, but you have your " + "wallet key, your crypto money will be safe. " + "That is why you should keep your wallet key " + "safe.", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 12, + ), + Text( + "Why write it down?", + style: STextStyles.w600_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You do not put your cash on display, do you?" + " Keeping your wallet key on a digital device" + " is like having it on display for thieves - " + "malicious software and hackers. Write your " + "wallet key down on paper in multiple copies " + "and keep them in a real, physical safe.", + style: STextStyles.w400_16(context), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: SecondaryButton( + label: "Close", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index ace0e7c88..70b4773fa 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -503,6 +503,24 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SelectNewFrostImportTypeView.routeName: + if (args is ({ + String walletName, + FrostCurrency frostCurrency, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SelectNewFrostImportTypeView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ImportNewFrostMsWalletView.routeName: if (args is ({ String walletName, From 3e74683d6c231f5683eb50f4985c00699dc78487 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 25 Apr 2024 15:49:48 -0600 Subject: [PATCH 172/272] add newly designed simple mobile dialog --- lib/widgets/dialogs/simple_mobile_dialog.dart | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 lib/widgets/dialogs/simple_mobile_dialog.dart diff --git a/lib/widgets/dialogs/simple_mobile_dialog.dart b/lib/widgets/dialogs/simple_mobile_dialog.dart new file mode 100644 index 000000000..1e07e22ae --- /dev/null +++ b/lib/widgets/dialogs/simple_mobile_dialog.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class SimpleMobileDialog extends StatelessWidget { + const SimpleMobileDialog({ + super.key, + required this.child, + this.showCloseButton = true, + this.padding, + }); + + final Widget child; + final bool showCloseButton; + final EdgeInsets? padding; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.all(16), + child: Material( + borderRadius: BorderRadius.circular( + 20, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: Padding( + padding: padding ?? const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: child, + ), + ), + if (showCloseButton) + const SizedBox( + height: 16, + ), + if (showCloseButton) + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: SecondaryButton( + label: "Close", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} From 3c76cc115ca9d01403dedf27554c47f60b4b488b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 25 Apr 2024 15:54:40 -0600 Subject: [PATCH 173/272] rough gui refactor for frost wallet creation on mobile --- lib/electrumx_rpc/client_manager.dart | 4 +- .../frost_ms/frost_scaffold.dart | 177 +++++++ ...irm_new_frost_ms_wallet_creation_view.dart | 337 -------------- .../new/create_new_frost_ms_wallet_view.dart | 172 ++++++- .../new/frost_share_commitments_view.dart | 438 ------------------ .../frost_ms/new/frost_share_shares_view.dart | 404 ---------------- .../new/import_new_frost_ms_wallet_view.dart | 50 +- .../select_new_frost_import_type_view.dart | 180 +++---- .../new/share_new_multisig_config_view.dart | 266 ++++++++++- .../new/steps/frost_create_step_1.dart | 370 +++++++++++++++ .../new/steps/frost_create_step_2.dart | 320 +++++++++++++ .../new/steps/frost_create_step_3.dart | 75 +++ .../new/steps/frost_create_step_4.dart | 231 +++++++++ .../new/steps/frost_route_generator.dart | 78 ++++ .../name_your_wallet_view.dart | 5 +- .../frost_continue_sign_config_view.dart | 2 +- .../involved/step_2/begin_resharing_view.dart | 2 +- .../step_2/continue_resharing_view.dart | 2 +- .../new/new_continue_sharing_view.dart | 2 +- .../new/new_start_resharing_view.dart | 2 +- .../resharing/verify_updated_wallet_view.dart | 2 +- lib/route_generator.dart | 65 +-- .../crypto_currency/coins/bitcoin_frost.dart | 4 +- .../wallet/impl/bitcoin_frost_wallet.dart | 9 - .../frost_interruption_dialog.dart | 0 .../frost}/frost_step_explanation_dialog.dart | 0 .../dialogs/frost/frost_step_qr_dialog.dart | 199 ++++++++ lib/widgets/frost_mascot.dart | 2 +- lib/widgets/frost_step_user_steps.dart | 38 ++ 29 files changed, 2030 insertions(+), 1406 deletions(-) create mode 100644 lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart delete mode 100644 lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart delete mode 100644 lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart delete mode 100644 lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart rename lib/widgets/dialogs/{ => frost}/frost_interruption_dialog.dart (100%) rename lib/{pages/add_wallet_views/frost_ms => widgets/dialogs/frost}/frost_step_explanation_dialog.dart (100%) create mode 100644 lib/widgets/dialogs/frost/frost_step_qr_dialog.dart create mode 100644 lib/widgets/frost_step_user_steps.dart diff --git a/lib/electrumx_rpc/client_manager.dart b/lib/electrumx_rpc/client_manager.dart index 8e580ec78..26db04b4b 100644 --- a/lib/electrumx_rpc/client_manager.dart +++ b/lib/electrumx_rpc/client_manager.dart @@ -51,12 +51,12 @@ class ClientManager { if (_map[key] == null) { throw Exception( - "No managed ElectrumClient for $cryptoCurrency found.", + "No managed ElectrumClient for $key found.", ); } if (_heightCompleters[key] == null) { throw Exception( - "No managed _heightCompleters for $cryptoCurrency found.", + "No managed _heightCompleters for $key found.", ); } diff --git a/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart b/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart new file mode 100644 index 000000000..ab85dd776 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/progress_bar.dart'; + +class FrostStepScaffold extends ConsumerStatefulWidget { + const FrostStepScaffold({super.key}); + + static const String routeName = "/frostStepScaffold"; + + @override + ConsumerState createState() => _FrostScaffoldState(); +} + +class _FrostScaffoldState extends ConsumerState { + static const _titleTextSize = 18.0; + final _navigatorKey = GlobalKey(); + + late final List _routes; + + bool _requestPopLock = false; + Future _requestPop(BuildContext context) async { + if (_requestPopLock) { + return; + } + _requestPopLock = true; + + // TODO: dialog to confirm exit + + // make sure to at least delay some time otherwise flutter pops back more than a single route lol... + await Future.delayed(const Duration(milliseconds: 200)); + + if (context.mounted) { + Navigator.of(context).pop(); + ref.read(pFrostCreateNewArgs.state).state = null; + } + + _requestPopLock = false; + } + + @override + void initState() { + _routes = ref.read(pFrostCreateNewArgs)!.$2; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvoked: (_) => _requestPop(context), + child: Material( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => child, + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + body: SafeArea( + child: child, + ), + ), + ), + child: Column( + children: [ + // header + SizedBox( + height: 56, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + Text( + "${ref.watch(pFrostCreateCurrentStep)} / ${_routes.length}", + style: STextStyles.navBarTitle(context).copyWith( + fontSize: _titleTextSize, + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Text( + _routes[ref.watch(pFrostCreateCurrentStep) - 1] + .title, + style: STextStyles.navBarTitle(context).copyWith( + fontSize: _titleTextSize, + ), + ), + ), + const SizedBox( + width: 10, + ), + CustomTextButton( + text: "Exit", + textSize: _titleTextSize, + onTap: () => _requestPop(context), + ), + ], + ), + ), + ), + LayoutBuilder( + builder: (subContext, constraints) => ProgressBar( + width: constraints.maxWidth, + height: 3, + fillColor: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + backgroundColor: Theme.of(context) + .extension()! + .customTextButtonEnabledText + .withOpacity(0.1), + percent: + ref.watch(pFrostCreateCurrentStep) / _routes.length, + ), + ), + Expanded( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: SizedBox( + width: 500, + child: child, + ), + ) + ], + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ), + child: Navigator( + key: _navigatorKey, + initialRoute: _routes[0].routeName, + onGenerateRoute: FrostRouteGenerator.generateRoute, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart b/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart deleted file mode 100644 index 184dd91d1..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart +++ /dev/null @@ -1,337 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.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/transaction_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/crypto_currency/intermediate/frost_currency.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/custom_buttons/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; -import 'package:stackwallet/widgets/frost_mascot.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; - -class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget { - const ConfirmNewFrostMSWalletCreationView({ - super.key, - required this.walletName, - required this.frostCurrency, - }); - - static const String routeName = "/confirmNewFrostMSWalletCreationView"; - - final String walletName; - final FrostCurrency frostCurrency; - - @override - ConsumerState createState() => - _ConfirmNewFrostMSWalletCreationViewState(); -} - -class _ConfirmNewFrostMSWalletCreationViewState - extends ConsumerState { - late final String seed, recoveryString, serializedKeys, multisigConfig; - late final Uint8List multisigId; - - @override - void initState() { - seed = ref.read(pFrostStartKeyGenData.state).state!.seed; - serializedKeys = - ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys; - recoveryString = - ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString; - multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; - multisigConfig = ref.read(pFrostMultisigConfig.state).state!; - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - // TODO: [prio=high] get rid of placeholder text?? - trailing: const FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Finalize 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: [ - Text( - "Ensure your multisig ID matches that of each other participant", - style: STextStyles.pageTitleH2(context), - ), - const _Div(), - DetailItem( - title: "ID", - detail: multisigId.toString(), - button: Util.isDesktop - ? IconCopyButton( - data: multisigId.toString(), - ) - : SimpleCopyButton( - data: multisigId.toString(), - ), - ), - const _Div(), - const _Div(), - Text( - "Back up your keys and config", - style: STextStyles.pageTitleH2(context), - ), - const _Div(), - DetailItem( - title: "Multisig Config", - detail: multisigConfig, - button: Util.isDesktop - ? IconCopyButton( - data: multisigConfig, - ) - : SimpleCopyButton( - data: multisigConfig, - ), - ), - const _Div(), - DetailItem( - title: "Keys", - detail: serializedKeys, - button: Util.isDesktop - ? IconCopyButton( - data: serializedKeys, - ) - : SimpleCopyButton( - data: serializedKeys, - ), - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Confirm", - onPressed: () async { - bool progressPopped = false; - try { - unawaited( - showDialog( - context: context, - barrierDismissible: false, - useSafeArea: true, - builder: (ctx) { - return const Center( - child: LoadingIndicator( - width: 50, - height: 50, - ), - ); - }, - ), - ); - - final 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), - ); - - await (wallet as BitcoinFrostWallet).initializeNewFrost( - mnemonic: seed, - 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 (mounted) { - Navigator.pop(context); - progressPopped = true; - } - - if (mounted) { - if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); - } else { - unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), - ); - } - - ref.read(pFrostMultisigConfig.state).state = null; - ref.read(pFrostStartKeyGenData.state).state = null; - ref.read(pFrostSecretSharesData.state).state = null; - - 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, - ); - - // pop progress dialog - if (mounted && !progressPopped) { - Navigator.pop(context); - progressPopped = true; - } - // TODO: handle gracefully - rethrow; - } - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index 9ea89de44..533d23e02 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -11,10 +11,13 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency. 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/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class CreateNewFrostMsWalletView extends ConsumerStatefulWidget { @@ -67,13 +70,14 @@ class _NewFrostMsWalletViewState } final hasEmptyParticipants = controllers - .map((e) => e.text.isEmpty) + .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).toSet().length) { + if (controllers.length != + controllers.map((e) => e.text.trim()).toSet().length) { return "Duplicate participant name found"; } @@ -102,6 +106,31 @@ class _NewFrostMsWalletViewState } } + void _showWhatIsThresholdDialog() { + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // TODO: [prio=high] need text from designers! + Text( + "What is a threshold?", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + Text( + "Text here", + style: STextStyles.w400_16(context), + ), + ], + ), + ), + ); + } + @override void dispose() { _thresholdController.dispose(); @@ -146,7 +175,7 @@ class _NewFrostMsWalletViewState }, ), title: Text( - "New FROST multisig config", + "Create new group", style: STextStyles.navBarTitle(context), ), ), @@ -174,9 +203,21 @@ class _NewFrostMsWalletViewState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Threshold", - style: STextStyles.label(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Threshold", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + CustomTextButton( + text: "What is a threshold?", + onTap: _showWhatIsThresholdDialog, + ), + ], ), const SizedBox( height: 10, @@ -185,22 +226,53 @@ class _NewFrostMsWalletViewState 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.label(context), + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), ), const SizedBox( height: 10, ), - TextField( - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - controller: _participantsController, - onChanged: _participantsCountChanged, + 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, @@ -208,24 +280,75 @@ class _NewFrostMsWalletViewState if (controllers.isNotEmpty) Text( "My name", - style: STextStyles.label(context), + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), ), if (controllers.isNotEmpty) const SizedBox( height: 10, ), if (controllers.isNotEmpty) - TextField( - controller: controllers.first, + 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) - Text( - "Remaining participants", - style: STextStyles.label(context), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Remaining participants", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Type each name in one word without spaces", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + ], ), if (controllers.length > 1) Column( @@ -237,6 +360,10 @@ class _NewFrostMsWalletViewState ), child: TextField( controller: controllers[i], + decoration: InputDecoration( + hintText: "Enter name", + hintStyle: STextStyles.fieldLabel(context), + ), ), ), ], @@ -246,7 +373,7 @@ class _NewFrostMsWalletViewState height: 16, ), PrimaryButton( - label: "Generate", + label: "Create new group", onPressed: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -265,12 +392,13 @@ class _NewFrostMsWalletViewState } final config = Frost.createMultisigConfig( - name: controllers.first.text, + name: controllers.first.text.trim(), threshold: int.parse(_thresholdController.text), - participants: controllers.map((e) => e.text).toList(), + participants: controllers.map((e) => e.text.trim()).toList(), ); - ref.read(pFrostMyName.notifier).state = controllers.first.text; + ref.read(pFrostMyName.notifier).state = + controllers.first.text.trim(); ref.read(pFrostMultisigConfig.notifier).state = config; await Navigator.of(context).pushNamed( diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart deleted file mode 100644 index e754db2bd..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart +++ /dev/null @@ -1,438 +0,0 @@ -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:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.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_desktop_specific/desktop_home_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/constants.dart'; -import 'package:stackwallet/utilities/logger.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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 FrostShareCommitmentsView extends ConsumerStatefulWidget { - const FrostShareCommitmentsView({ - super.key, - required this.walletName, - required this.frostCurrency, - }); - - static const String routeName = "/frostShareCommitmentsView"; - - final String walletName; - final FrostCurrency frostCurrency; - - @override - ConsumerState createState() => - _FrostShareCommitmentsViewState(); -} - -class _FrostShareCommitmentsViewState - extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List participants; - late final String myCommitment; - late final int myIndex; - - final List fieldIsEmptyFlags = []; - - @override - void initState() { - participants = Frost.getParticipants( - multisigConfig: ref.read(pFrostMultisigConfig.state).state!, - ); - myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); - 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 WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - // TODO: [prio=high] get rid of placeholder text?? - trailing: const FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Commitments", - 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: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myCommitment, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My name", - detail: ref.watch(pFrostMyName.state).state!, - ), - const _Div(), - DetailItem( - title: "My commitment", - detail: myCommitment, - button: Util.isDesktop - ? IconCopyButton( - data: myCommitment, - ) - : SimpleCopyButton( - data: myCommitment, - ), - ), - const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < participants.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostCommitmentsTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter ${participants[i]}'s commitment", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Commitment Field Input.", - key: Key( - "frostCommitmentsClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Commitment Field Input.", - key: Key( - "frostCommitmentsPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .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(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Generate shares", - enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: () async { - // check for empty commitments - if (controllers - .map((e) => e.text.isEmpty) - .reduce((value, element) => value |= element)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Missing commitments", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - // collect commitment strings and insert my own at the correct index - final commitments = controllers.map((e) => e.text).toList(); - commitments.insert(myIndex, myCommitment); - - try { - ref.read(pFrostSecretSharesData.notifier).state = - Frost.generateSecretShares( - multisigConfigWithNamePtr: ref - .read(pFrostStartKeyGenData.state) - .state! - .multisigConfigWithNamePtr, - mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed, - secretShareMachineWrapperPtr: ref - .read(pFrostStartKeyGenData.state) - .state! - .secretShareMachineWrapperPtr, - commitments: commitments, - ); - - await Navigator.of(context).pushNamed( - FrostShareSharesView.routeName, - arguments: ( - walletName: widget.walletName, - frostCurrency: widget.frostCurrency, - ), - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to generate shares", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart b/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart deleted file mode 100644 index ef5927b9e..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart +++ /dev/null @@ -1,404 +0,0 @@ -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:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.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_desktop_specific/desktop_home_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/constants.dart'; -import 'package:stackwallet/utilities/logger.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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.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 FrostShareSharesView extends ConsumerStatefulWidget { - const FrostShareSharesView({ - super.key, - required this.walletName, - required this.frostCurrency, - }); - - static const String routeName = "/frostShareSharesView"; - - final String walletName; - final FrostCurrency frostCurrency; - - @override - ConsumerState createState() => - _FrostShareSharesViewState(); -} - -class _FrostShareSharesViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List participants; - late final String myShare; - late final int myIndex; - - final List fieldIsEmptyFlags = []; - - @override - void initState() { - participants = Frost.getParticipants( - multisigConfig: ref.read(pFrostMultisigConfig.state).state!, - ); - myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); - myShare = ref.read(pFrostSecretSharesData.state).state!.share; - - // temporarily remove my name. Added back later - participants.removeAt(myIndex); - - for (int i = 0; i < participants.length; i++) { - controllers.add(TextEditingController()); - focusNodes.add(FocusNode()); - fieldIsEmptyFlags.add(true); - } - super.initState(); - } - - @override - void dispose() { - for (int i = 0; i < controllers.length; i++) { - controllers[i].dispose(); - } - for (int i = 0; i < focusNodes.length; i++) { - focusNodes[i].dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - // TODO: [prio=high] get rid of placeholder text?? - trailing: const FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.walletCreation, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Generate shares", - 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: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myShare, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My name", - detail: ref.watch(pFrostMyName.state).state!, - ), - const _Div(), - DetailItem( - title: "My share", - detail: myShare, - button: Util.isDesktop - ? IconCopyButton( - data: myShare, - ) - : SimpleCopyButton( - data: myShare, - ), - ), - const _Div(), - for (int i = 0; i < participants.length; i++) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frSharesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${participants[i]}'s share", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Share Field Input.", - key: Key("frSharesClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Share Field Input.", - key: Key("frSharesPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: Key("frSharesScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Generate", - onPressed: () async { - // check for empty commitments - if (controllers - .map((e) => e.text.isEmpty) - .reduce((value, element) => value |= element)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Missing shares", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - // collect commitment strings and insert my own at the correct index - final shares = controllers.map((e) => e.text).toList(); - shares.insert(myIndex, myShare); - - try { - ref.read(pFrostCompletedKeyGenData.notifier).state = - Frost.completeKeyGeneration( - multisigConfigWithNamePtr: ref - .read(pFrostStartKeyGenData.state) - .state! - .multisigConfigWithNamePtr, - secretSharesResPtr: ref - .read(pFrostSecretSharesData.state) - .state! - .secretSharesResPtr, - shares: shares, - ); - await Navigator.of(context).pushNamed( - ConfirmNewFrostMSWalletCreationView.routeName, - arguments: ( - walletName: widget.walletName, - frostCurrency: widget.frostCurrency, - ), - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to complete key generation", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart index bd9724984..531d7971f 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart @@ -1,11 +1,18 @@ +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:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_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/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -374,12 +381,47 @@ class _ImportNewFrostMsWalletViewState myName: ref.read(pFrostMyName.state).state!, ); - await Navigator.of(context).pushNamed( - FrostShareCommitmentsView.routeName, - arguments: ( + ref.read(pFrostCreateNewArgs.state).state = ( + ( walletName: widget.walletName, frostCurrency: widget.frostCurrency, ), + FrostRouteGenerator.createNewConfigStepRoutes, + () { + // successful completion of steps + if (Util.isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).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(pFrostCreateNewArgs.state).state = null; + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Your wallet is set up.", + iconAsset: Assets.svg.check, + context: context, + ), + ); + } + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, ); }, ), diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index 80205afd9..e35822cac 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -14,7 +14,7 @@ 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/dialogs/simple_mobile_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class SelectNewFrostImportTypeView extends StatefulWidget { @@ -268,124 +268,70 @@ class _FrostJoinInfoDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( + return SimpleMobileDialog( child: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Flexible( - child: Padding( - padding: const EdgeInsets.all(16), - child: Material( - borderRadius: BorderRadius.circular( - 20, - ), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - 20, - ), - ), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // TODO: [prio=high] need text from designers! - Text( - "Join a group", - style: STextStyles.w600_20(context), - ), - const SizedBox( - height: 12, - ), - Text( - "Text here", - style: STextStyles.w400_16(context), - ), - const SizedBox( - height: 8, - ), - Text( - "What is resharing?", - style: STextStyles.w600_16(context), - ), - const SizedBox( - height: 8, - ), - Text( - "In cryptocurrency, you are your own bank." - " Imagine keeping cash at home. If that cash" - " burns down or gets stolen, you lose it and" - " nobody will help you get your money back.", - style: STextStyles.w400_16(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Since cryptocurrency is digital money, your " - "wallet key is like that “cash” you keep at " - "home. If you lose your phone or if you " - "forget your wallet PIN, but you have your " - "wallet key, your crypto money will be safe. " - "That is why you should keep your wallet key " - "safe.", - style: STextStyles.w400_16(context), - ), - const SizedBox( - height: 12, - ), - Text( - "Why write it down?", - style: STextStyles.w600_16(context), - ), - const SizedBox( - height: 8, - ), - Text( - "You do not put your cash on display, do you?" - " Keeping your wallet key on a digital device" - " is like having it on display for thieves - " - "malicious software and hackers. Write your " - "wallet key down on paper in multiple copies " - "and keep them in a real, physical safe.", - style: STextStyles.w400_16(context), - ), - ], - ), - ), - ), - const SizedBox( - height: 16, - ), - Row( - children: [ - const Spacer(), - const SizedBox( - width: 16, - ), - Expanded( - child: SecondaryButton( - label: "Close", - onPressed: Navigator.of(context).pop, - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), + // TODO: [prio=high] need text from designers! + Text( + "Join a group", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + Text( + "Text here", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "What is resharing?", + style: STextStyles.w600_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "In cryptocurrency, you are your own bank." + " Imagine keeping cash at home. If that cash" + " burns down or gets stolen, you lose it and" + " nobody will help you get your money back.", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "Since cryptocurrency is digital money, your " + "wallet key is like that “cash” you keep at " + "home. If you lose your phone or if you " + "forget your wallet PIN, but you have your " + "wallet key, your crypto money will be safe. " + "That is why you should keep your wallet key " + "safe.", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 12, + ), + Text( + "Why write it down?", + style: STextStyles.w600_16(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You do not put your cash on display, do you?" + " Keeping your wallet key on a digital device" + " is like having it on display for thieves - " + "malicious software and hackers. Write your " + "wallet key down on paper in multiple copies " + "and keep them in a real, physical safe.", + style: STextStyles.w400_16(context), ), ], ), diff --git a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart index 9f8862070..30c7a5c23 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart @@ -1,11 +1,19 @@ +import 'dart:async'; + 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/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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_desktop_specific/desktop_home_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/wallets/crypto_currency/intermediate/frost_currency.dart'; @@ -16,8 +24,11 @@ import 'package:stackwallet/widgets/custom_buttons/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; import 'package:stackwallet/widgets/frost_mascot.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class ShareNewMultisigConfigView extends ConsumerStatefulWidget { const ShareNewMultisigConfigView({ @@ -38,6 +49,114 @@ class ShareNewMultisigConfigView extends ConsumerStatefulWidget { class _ShareNewMultisigConfigViewState extends ConsumerState { + bool _userVerifyContinue = false; + + void _showParticipantsDialog() { + final participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + showCloseButton: false, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "Group participants", + style: STextStyles.w600_20(context), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "The names are case-sensitive and must be entered exactly.", + style: STextStyles.w400_16(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + const SizedBox( + height: 12, + ), + for (final participant in participants) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.background, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + participant, + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: participant, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return ConditionalParent( @@ -72,7 +191,7 @@ class _ShareNewMultisigConfigViewState }, ), title: Text( - "Multisig config", + "Share multisig group info", style: STextStyles.navBarTitle(context), ), ), @@ -99,7 +218,10 @@ class _ShareNewMultisigConfigViewState ), child: Column( children: [ - if (!Util.isDesktop) const Spacer(), + const _SharingStepsInfo(), + const SizedBox( + height: 20, + ), SizedBox( height: 220, child: Row( @@ -119,7 +241,7 @@ class _ShareNewMultisigConfigViewState ), ), const SizedBox( - height: 32, + height: 20, ), DetailItem( title: "Encoded config", @@ -137,12 +259,64 @@ class _ShareNewMultisigConfigViewState 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, + ), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has joined the group", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), PrimaryButton( label: "Start key generation", + enabled: _userVerifyContinue, onPressed: () async { ref.read(pFrostStartKeyGenData.notifier).state = Frost.startKeyGeneration( @@ -150,12 +324,48 @@ class _ShareNewMultisigConfigViewState myName: ref.read(pFrostMyName.state).state!, ); - await Navigator.of(context).pushNamed( - FrostShareCommitmentsView.routeName, - arguments: ( + ref.read(pFrostCreateNewArgs.state).state = ( + ( walletName: widget.walletName, frostCurrency: widget.frostCurrency, ), + FrostRouteGenerator.createNewConfigStepRoutes, + () { + // successful completion of steps + if (Util.isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).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(pFrostCreateNewArgs.state).state = null; + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Your wallet is set up.", + iconAsset: Assets.svg.check, + context: context, + ), + ); + } + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + // FrostShareCommitmentsView.routeName, ); }, ), @@ -165,3 +375,45 @@ class _ShareNewMultisigConfigViewState ); } } + +class _SharingStepsInfo extends StatelessWidget { + const _SharingStepsInfo({super.key}); + + static const steps = [ + "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”.", + ]; + + @override + Widget build(BuildContext context) { + final style = STextStyles.w500_12(context); + return RoundedWhiteContainer( + child: Column( + children: [ + for (int i = 0; i < steps.length; i++) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${i + 1}.", + style: style, + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + steps[i], + style: style, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart new file mode 100644 index 000000000..143804e95 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart @@ -0,0 +1,370 @@ +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:flutter_svg/flutter_svg.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_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/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/text_styles.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/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.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 FrostCreateStep1 extends ConsumerStatefulWidget { + const FrostCreateStep1({ + super.key, + }); + + static const String routeName = "/frostCreateStep1"; + static const String title = "Commitments"; + + @override + ConsumerState createState() => _FrostCreateStep1State(); +} + +class _FrostCreateStep1State extends ConsumerState { + static const info = [ + "Share your commitment with other group members.", + "Enter their commitments into the corresponding fields.", + ]; + + final List controllers = []; + final List focusNodes = []; + + late final List participants; + late final String myCommitment; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + bool _userVerifyContinue = false; + + Future _showQrCodeDialog() async { + await showDialog( + context: context, + builder: (_) => FrostStepQrDialog( + myName: ref.read(pFrostMyName)!, + title: "Step 1 of 4 - ${FrostCreateStep1.title}", + data: myCommitment, + ), + ); + } + + @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), + SecondaryButton( + label: "View QR code", + icon: SvgPicture.asset( + Assets.svg.qrcode, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.buttonTextSecondary, + BlendMode.srcIn, + ), + ), + onPressed: _showQrCodeDialog, + ), + const SizedBox(height: 12), + for (int i = 0; i < participants.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 12, + ), + Text( + participants[i], + style: STextStyles.w500_14(context), + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostCommitmentsTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter ${participants[i]}'s commitment", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Commitment Field Input.", + key: Key( + "frostCommitmentsClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Commitment Field Input.", + key: Key( + "frostCommitmentsPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: + Key("frostCommitmentsScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has all commitments", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Generate shares", + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: () async { + // check for empty commitments + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing commitments", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect commitment strings and insert my own at the correct index + final commitments = controllers.map((e) => e.text).toList(); + commitments.insert(myIndex, myCommitment); + + try { + ref.read(pFrostSecretSharesData.notifier).state = + Frost.generateSecretShares( + multisigConfigWithNamePtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .multisigConfigWithNamePtr, + mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed, + secretShareMachineWrapperPtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .secretShareMachineWrapperPtr, + commitments: commitments, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + FrostCreateStep2.routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to generate shares", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart new file mode 100644 index 000000000..c6f1c7331 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -0,0 +1,320 @@ +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:flutter_svg/flutter_svg.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_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/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/text_styles.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/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.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 FrostCreateStep2 extends ConsumerStatefulWidget { + const FrostCreateStep2({super.key}); + + static const String routeName = "/frostCreateStep2"; + static const String title = "Shares"; + + @override + ConsumerState createState() => _FrostCreateStep2State(); +} + +class _FrostCreateStep2State extends ConsumerState { + static const info = [ + "Send your share to other group members.", + "Enter their shares into the corresponding fields.", + ]; + + final List controllers = []; + final List focusNodes = []; + + late final List participants; + late final String myShare; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + Future _showQrCodeDialog() async { + await showDialog( + context: context, + builder: (_) => FrostStepQrDialog( + myName: ref.read(pFrostMyName)!, + title: "Step 2 of 4 - ${FrostCreateStep2.title}", + data: myShare, + ), + ); + } + + @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), + SecondaryButton( + label: "View QR code", + icon: SvgPicture.asset( + Assets.svg.qrcode, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.buttonTextSecondary, + BlendMode.srcIn, + ), + ), + onPressed: _showQrCodeDialog, + ), + const SizedBox(height: 12), + for (int i = 0; i < participants.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 12, + ), + Text( + participants[i], + style: STextStyles.w500_14(context), + ), + const SizedBox( + height: 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frSharesTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${participants[i]}'s share", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Share Field Input.", + key: Key("frSharesClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Share Field Input.", + key: Key("frSharesPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: Key("frSharesScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ) + ], + ), + ), + ), + ), + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + PrimaryButton( + label: "Generate", + onPressed: () async { + // check for empty commitments + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing shares", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect commitment strings and insert my own at the correct index + final shares = controllers.map((e) => e.text).toList(); + shares.insert(myIndex, myShare); + + try { + ref.read(pFrostCompletedKeyGenData.notifier).state = + Frost.completeKeyGeneration( + multisigConfigWithNamePtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .multisigConfigWithNamePtr, + secretSharesResPtr: ref + .read(pFrostSecretSharesData.state) + .state! + .secretSharesResPtr, + shares: shares, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + FrostCreateStep3.routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to complete key generation", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart new file mode 100644 index 000000000..00b7f7d1a --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -0,0 +1,75 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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_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 FrostCreateStep3 extends ConsumerStatefulWidget { + const FrostCreateStep3({super.key}); + + static const String routeName = "/frostCreateStep3"; + static const String title = "Verify multisig ID"; + + @override + ConsumerState createState() => _FrostCreateStep3State(); +} + +class _FrostCreateStep3State extends ConsumerState { + static const info = [ + "Ensure your multisig ID matches that of each other participant.", + ]; + + late final Uint8List multisigId; + + @override + void initState() { + multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 12), + DetailItem( + title: "Multisig ID", + detail: multisigId.toString(), + button: Util.isDesktop + ? IconCopyButton( + data: multisigId.toString(), + ) + : SimpleCopyButton( + data: multisigId.toString(), + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + PrimaryButton( + label: "Confirm", + onPressed: () { + ref.read(pFrostCreateCurrentStep.state).state = 4; + Navigator.of(context).pushNamed( + FrostCreateStep4.routeName, + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart new file mode 100644 index 000000000..9cc8ef477 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart @@ -0,0 +1,231 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_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/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/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/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 FrostCreateStep4 extends ConsumerStatefulWidget { + const FrostCreateStep4({super.key}); + + static const String routeName = "/frostCreateStep4"; + static const String title = "Back up your keys"; + + @override + ConsumerState createState() => _FrostCreateStep4State(); +} + +class _FrostCreateStep4State extends ConsumerState { + static const _warning = "These are your private keys. Please back them up, " + "keep them safe and never share it with anyone. Your private keys are the" + " only way you can access your funds if you forget PIN, lose your phone, " + "etc. Stack Wallet does not keep nor is able to restore your private keys" + "."; + + late final String seed, recoveryString, serializedKeys, multisigConfig; + late final Uint8List multisigId; + + bool _userVerifyContinue = false; + + @override + void initState() { + seed = ref.read(pFrostStartKeyGenData.state).state!.seed; + serializedKeys = + ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys; + recoveryString = + ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString; + multisigConfig = ref.read(pFrostMultisigConfig.state).state!; + multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + RoundedContainer( + color: + Theme.of(context).extension()!.warningBackground, + child: Text( + _warning, + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .warningForeground, + ), + ), + ), + const SizedBox(height: 12), + DetailItem( + title: "Multisig Config", + detail: multisigConfig, + button: Util.isDesktop + ? IconCopyButton( + data: multisigConfig, + ) + : SimpleCopyButton( + data: multisigConfig, + ), + ), + const SizedBox(height: 12), + DetailItem( + title: "Keys", + detail: serializedKeys, + button: Util.isDesktop + ? IconCopyButton( + data: serializedKeys, + ) + : SimpleCopyButton( + data: serializedKeys, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have backed up my keys and the config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Continue", + enabled: _userVerifyContinue, + onPressed: () async { + bool progressPopped = false; + try { + unawaited( + showDialog( + context: context, + barrierDismissible: false, + useSafeArea: true, + builder: (ctx) { + return const Center( + child: LoadingIndicator( + width: 50, + height: 50, + ), + ); + }, + ), + ); + + final data = ref.read(pFrostCreateNewArgs)!; + + final info = WalletInfo.createNew( + coin: data.$1.frostCurrency.coin, + name: data.$1.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(pFrostCreateNewArgs)!.$3(); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + // pop progress dialog + if (context.mounted && !progressPopped) { + Navigator.pop(context); + progressPopped = true; + } + // TODO: handle gracefully + rethrow; + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart new file mode 100644 index 000000000..a5df36e67 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart @@ -0,0 +1,78 @@ +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_1.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/route_generator.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; + +typedef FrostStepRoute = ({String routeName, String title}); + +final pFrostCreateCurrentStep = StateProvider.autoDispose((ref) => 1); +final pFrostCreateNewArgs = StateProvider< + ( + ({String walletName, FrostCurrency frostCurrency}), + List, + VoidCallback, + )?>((ref) => null); + +abstract class FrostRouteGenerator { + static const bool useMaterialPageRoute = true; + + static const List createNewConfigStepRoutes = [ + (routeName: FrostCreateStep1.routeName, title: FrostCreateStep1.title), + (routeName: FrostCreateStep2.routeName, title: FrostCreateStep2.title), + (routeName: FrostCreateStep3.routeName, title: FrostCreateStep3.title), + (routeName: FrostCreateStep4.routeName, title: FrostCreateStep4.title), + ]; + + static Route generateRoute(RouteSettings settings) { + final args = settings.arguments; + + switch (settings.name) { + case FrostCreateStep1.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep1(), + 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, + ); + + default: + return _routeError(""); + } + } + + static Route _routeError(String message) { + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => Placeholder( + child: Center( + child: Text(message), + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index b500ba32b..f3867f74c 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -413,7 +413,10 @@ class _NameYourWalletViewState extends ConsumerState { CreateNewFrostMsWalletView.routeName, arguments: ( walletName: name, - coin: coin, + // TODO: [prio=med] this will cause issues if frost is ever applied to other coins + frostCurrency: coin.isTestNet + ? BitcoinFrost(CryptoCurrencyNetwork.test) + : BitcoinFrost(CryptoCurrencyNetwork.main), ), ); }, diff --git a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart index 732fb2f82..8bfa512ca 100644 --- a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart +++ b/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart @@ -24,7 +24,7 @@ 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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.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'; diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart index 90218529f..ebd92cc3c 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart @@ -24,7 +24,7 @@ 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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.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'; diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart index 75359d266..b70139e69 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart @@ -24,7 +24,7 @@ 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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.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'; diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart index f365667a2..622dace0b 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart @@ -17,7 +17,7 @@ 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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.dart'; import 'package:stackwallet/widgets/frost_mascot.dart'; class NewContinueSharingView extends ConsumerStatefulWidget { diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart index 5feda5742..8bf618235 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart @@ -18,7 +18,7 @@ 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/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.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'; diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart index 85d02c0ff..c2507a2fc 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart @@ -26,7 +26,7 @@ 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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class VerifyUpdatedWalletView extends ConsumerStatefulWidget { diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 70b4773fa..83118fff1 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -26,10 +26,8 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.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/frost_share_commitments_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_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/new/share_new_multisig_config_view.dart'; @@ -585,59 +583,14 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case FrostShareCommitmentsView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostShareCommitmentsView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FrostShareSharesView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostShareSharesView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case ConfirmNewFrostMSWalletCreationView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ConfirmNewFrostMSWalletCreationView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FrostStepScaffold.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostStepScaffold(), + settings: RouteSettings( + name: settings.name, + ), + ); case FrostMSWalletOptionsView.routeName: if (args is String) { diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index 4bd1a6af9..bd5216350 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -12,9 +12,9 @@ class BitcoinFrost extends FrostCurrency { BitcoinFrost(super.network) { switch (network) { case CryptoCurrencyNetwork.main: - coin = Coin.bitcoin; + coin = Coin.bitcoinFrost; case CryptoCurrencyNetwork.test: - coin = Coin.bitcoinTestNet; + coin = Coin.bitcoinFrostTestNet; default: throw Exception("Unsupported network: $network"); } diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 734fe2bbc..1798fd79f 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -41,7 +41,6 @@ class BitcoinFrostWallet extends Wallet { late CachedElectrumXClient electrumXCachedClient; Future initializeNewFrost({ - required String mnemonic, required String multisigConfig, required String recoveryString, required String serializedKeys, @@ -70,14 +69,6 @@ class BitcoinFrostWallet extends Wallet { threshold: threshold, ); - await secureStorageInterface.write( - key: Wallet.mnemonicKey(walletId: info.walletId), - value: mnemonic, - ); - await secureStorageInterface.write( - key: Wallet.mnemonicPassphraseKey(walletId: info.walletId), - value: "", - ); await _saveSerializedKeys(serializedKeys); await _saveRecoveryString(recoveryString); await _saveMultisigId(multisigId); diff --git a/lib/widgets/dialogs/frost_interruption_dialog.dart b/lib/widgets/dialogs/frost/frost_interruption_dialog.dart similarity index 100% rename from lib/widgets/dialogs/frost_interruption_dialog.dart rename to lib/widgets/dialogs/frost/frost_interruption_dialog.dart diff --git a/lib/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart b/lib/widgets/dialogs/frost/frost_step_explanation_dialog.dart similarity index 100% rename from lib/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart rename to lib/widgets/dialogs/frost/frost_step_explanation_dialog.dart diff --git a/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart b/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart new file mode 100644 index 000000000..acb028af1 --- /dev/null +++ b/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart @@ -0,0 +1,199 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostStepQrDialog extends StatefulWidget { + const FrostStepQrDialog({ + super.key, + required this.myName, + required this.title, + required this.data, + }); + + final String myName; + final String title; + final String data; + + @override + State createState() => _FrostStepQrDialogState(); +} + +class _FrostStepQrDialogState extends State { + final _qrKey = GlobalKey(); + + Future _capturePng(bool shouldSaveInsteadOfShare) async { + try { + final boundary = + _qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary; + final image = await boundary.toImage(); + final byteData = await image.toByteData(format: ImageByteFormat.png); + final pngBytes = byteData!.buffer.asUint8List(); + + if (shouldSaveInsteadOfShare) { + if (Util.isDesktop) { + final dir = Directory("${Platform.environment['HOME']}"); + if (!dir.existsSync()) { + throw Exception( + "Home dir not found while trying to open filepicker on QR image save"); + } + final path = await FilePicker.platform.saveFile( + fileName: "qrcode.png", + initialDirectory: dir.path, + ); + + if (path != null && context.mounted) { + final file = File(path); + if (file.existsSync()) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$path already exists!", + context: context, + ), + ); + } else { + await file.writeAsBytes(pngBytes); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "$path saved!", + context: context, + ), + ); + } + } + } else { + // await DocumentFileSavePlus.saveFile( + // pngBytes, + // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", + // "image/png"); + } + } else { + final tempDir = await getTemporaryDirectory(); + final file = await File("${tempDir.path}/qrcode.png").create(); + await file.writeAsBytes(pngBytes); + + await Share.shareFiles(["${tempDir.path}/qrcode.png"], + text: "Receive URI QR Code"); + } + } catch (e) { + //todo: comeback to this + debugPrint(e.toString()); + } + } + + @override + Widget build(BuildContext context) { + return SimpleMobileDialog( + showCloseButton: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RepaintBoundary( + key: _qrKey, + child: RoundedWhiteContainer( + boxShadow: [ + Theme.of(context).extension()!.standardBoxShadow + ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.myName, + style: STextStyles.w600_16(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + const SizedBox(height: 8), + Text( + widget.title, + style: STextStyles.w600_12(context), + ), + const SizedBox(height: 8), + RoundedContainer( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + radiusMultiplier: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: AspectRatio( + aspectRatio: 1, + child: QrImageView( + data: widget.data, + padding: EdgeInsets.zero, + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + ), + const SizedBox(height: 12), + SelectableText( + widget.data, + style: STextStyles.w500_10(context), + ), + ], + ), + ), + ], + ), + ), + ), + if (!Util.isDesktop) + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) + Row( + children: [ + const Spacer(), + const SizedBox(width: 16), + Expanded( + child: SecondaryButton( + label: "Share", + icon: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await _capturePng(false); + }, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/frost_mascot.dart b/lib/widgets/frost_mascot.dart index 6be496272..17743efcc 100644 --- a/lib/widgets/frost_mascot.dart +++ b/lib/widgets/frost_mascot.dart @@ -9,8 +9,8 @@ */ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_step_explanation_dialog.dart'; class FrostMascot extends StatelessWidget { final String title; diff --git a/lib/widgets/frost_step_user_steps.dart b/lib/widgets/frost_step_user_steps.dart new file mode 100644 index 000000000..6624ef45d --- /dev/null +++ b/lib/widgets/frost_step_user_steps.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostStepUserSteps extends StatelessWidget { + const FrostStepUserSteps({super.key, required this.userSteps}); + + final List userSteps; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + children: [ + for (int i = 0; i < userSteps.length; i++) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${i + 1}.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + userSteps[i], + style: STextStyles.w500_12(context), + ), + ), + ], + ), + ], + ), + ); + } +} From acccce62a8f8d1ec8ace267b67bbb9411b4b4328 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 09:51:39 -0600 Subject: [PATCH 174/272] frost wallet bottom nav bar ui refactor on mobile --- lib/pages/wallet_view/wallet_view.dart | 27 +++++++++++--- .../components/icons/frost_sign_nav_icon.dart | 36 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index cfa690f11..79046c600 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -84,6 +84,7 @@ 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'; @@ -356,7 +357,17 @@ class _WalletViewState extends ConsumerState { } } - void _onExchangePressed(BuildContext context) async { + Future _onFrostSignPressed(BuildContext context) async { + // TODO + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "TODO FROST SIGN TX", + ), + ); + } + + Future _onExchangePressed(BuildContext context) async { final Coin coin = ref.read(pWalletCoin(walletId)); if (coin.isTestNet) { @@ -401,7 +412,7 @@ class _WalletViewState extends ConsumerState { } } - void _onBuyPressed(BuildContext context) async { + Future _onBuyPressed(BuildContext context) async { final Coin coin = ref.read(pWalletCoin(walletId)); if (coin.isTestNet) { @@ -976,6 +987,12 @@ class _WalletViewState extends ConsumerState { } }, ), + if (ref.watch(pWalletCoin(walletId)).isFrost) + WalletNavigationBarItemData( + label: "Sign", + icon: const FrostSignNavIcon(), + onTap: () => _onFrostSignPressed(context), + ), WalletNavigationBarItemData( label: "Send", icon: const SendNavIcon(), @@ -1007,13 +1024,15 @@ class _WalletViewState extends ConsumerState { ); }, ), - 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(), diff --git a/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart new file mode 100644 index 000000000..a7a81c20f --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart @@ -0,0 +1,36 @@ +/* +* 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:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/themes/theme_providers.dart'; + +class FrostSignNavIcon extends ConsumerWidget { + const FrostSignNavIcon({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SvgPicture.file( + File( + ref.watch( + themeProvider.select( + // TODO: [prio=high] update themes with icon asset + (value) => value.assets.stackIcon, + ), + ), + ), + width: 24, + height: 24, + ); + } +} From 10b9e5433eb5588edd3a803a1a6c5f62bc322a2f Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 09:52:56 -0600 Subject: [PATCH 175/272] mobile frost wallet settings screen ui update --- assets/svg/swap2.svg | 10 ++ .../sub_widgets/settings_list_button.dart | 8 +- .../frost_ms/frost_ms_options_view.dart | 143 ++++++++---------- .../wallet_settings_view.dart | 31 ++-- lib/utilities/assets.dart | 1 + 5 files changed, 92 insertions(+), 101 deletions(-) create mode 100644 assets/svg/swap2.svg diff --git a/assets/svg/swap2.svg b/assets/svg/swap2.svg new file mode 100644 index 000000000..1c9ce8191 --- /dev/null +++ b/assets/svg/swap2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/pages/settings_views/sub_widgets/settings_list_button.dart b/lib/pages/settings_views/sub_widgets/settings_list_button.dart index 2e4c19d01..62b4a2aec 100644 --- a/lib/pages/settings_views/sub_widgets/settings_list_button.dart +++ b/lib/pages/settings_views/sub_widgets/settings_list_button.dart @@ -16,17 +16,19 @@ import 'package:stackwallet/utilities/text_styles.dart'; class SettingsListButton extends StatelessWidget { const SettingsListButton({ - Key? key, + super.key, required this.iconAssetName, required this.title, this.onPressed, this.iconSize = 20.0, - }) : super(key: key); + this.padding = const EdgeInsets.all(8.0), + }); final String iconAssetName; final String title; final VoidCallback? onPressed; final double iconSize; + final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { @@ -44,7 +46,7 @@ class SettingsListButton extends StatelessWidget { ), onPressed: onPressed, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: padding, child: Row( children: [ Container( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index 7fc7236a4..cfcb75a7e 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/resharing/involved/step_1a/begin_reshare_config_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart'; @@ -17,7 +18,7 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stac 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/constants.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'; @@ -30,14 +31,16 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; class FrostMSWalletOptionsView extends ConsumerWidget { const FrostMSWalletOptionsView({ - Key? key, + super.key, required this.walletId, - }) : super(key: key); + }); static const String routeName = "/frostMSWalletOptionsView"; final String walletId; + static const _padding = 12.0; + @override Widget build(BuildContext context, WidgetRef ref) { return ConditionalParent( @@ -83,56 +86,72 @@ class FrostMSWalletOptionsView extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _OptionButton( - label: "Show participants", - onPressed: () { - Navigator.of(context).pushNamed( - FrostParticipantsView.routeName, - arguments: walletId, - ); - }, + 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, ), - _OptionButton( - label: "Initiate resharing", - onPressed: () { - // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(walletId)!; + 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; + ref.read(pFrostMyName.state).state = frostInfo.myName; - Navigator.of(context).pushNamed( - BeginReshareConfigView.routeName, - arguments: walletId, - ); - }, + Navigator.of(context).pushNamed( + BeginReshareConfigView.routeName, + arguments: walletId, + ); + }, + ), ), const SizedBox( height: 8, ), - _OptionButton( - label: "Import reshare config", - onPressed: () { - // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(walletId)!; + 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; + ref.read(pFrostMyName.state).state = frostInfo.myName; - Navigator.of(context).pushNamed( - ImportReshareConfigView.routeName, - arguments: walletId, - ); - }, + Navigator.of(context).pushNamed( + ImportReshareConfigView.routeName, + arguments: walletId, + ); + }, + ), ), ], ), @@ -142,45 +161,3 @@ class FrostMSWalletOptionsView extends ConsumerWidget { ); } } - -class _OptionButton extends StatelessWidget { - const _OptionButton({ - super.key, - required this.label, - required this.onPressed, - }); - - final String label; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: onPressed, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - label, - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 04de48cb4..2eca78057 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -195,21 +195,6 @@ class _WalletSettingsViewState extends ConsumerState { padding: const EdgeInsets.all(4), child: Column( children: [ - if (coin == Coin.bitcoinFrost || - coin == Coin.bitcoinFrostTestNet) - if (coin == Coin.bitcoinFrost || - coin == Coin.bitcoinFrostTestNet) - SettingsListButton( - iconAssetName: Assets.svg.addressBook2, - iconSize: 16, - title: "FROST Multisig settings", - onPressed: () { - Navigator.of(context).pushNamed( - FrostMSWalletOptionsView.routeName, - arguments: walletId, - ); - }, - ), SettingsListButton( iconAssetName: Assets.svg.addressBook, iconSize: 16, @@ -221,6 +206,22 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), + if (coin.isFrost) + const SizedBox( + height: 8, + ), + if (coin.isFrost) + SettingsListButton( + iconAssetName: Assets.svg.addressBook2, + iconSize: 16, + title: "FROST Multisig settings", + onPressed: () { + Navigator.of(context).pushNamed( + FrostMSWalletOptionsView.routeName, + arguments: walletId, + ); + }, + ), const SizedBox( height: 8, ), diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index ff385a44d..9213fd1b2 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -143,6 +143,7 @@ class _SVG { String get chevronDown => "assets/svg/chevron-down.svg"; String get chevronUp => "assets/svg/chevron-up.svg"; String get swap => "assets/svg/swap.svg"; + String get swap2 => "assets/svg/swap2.svg"; String get downloadFolder => "assets/svg/folder-down.svg"; String get lock => "assets/svg/lock-keyhole.svg"; String get lockOpen => "assets/svg/lock-open.svg"; From 7250215edcdae6d2b9008e77361bf3f6ef1a3db5 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 10:55:46 -0600 Subject: [PATCH 176/272] mobile initiate resharing view updates --- .../frost_ms/frost_ms_options_view.dart | 4 +- ...view.dart => initiate_resharing_view.dart} | 131 +++++++++++++----- lib/route_generator.dart | 6 +- 3 files changed, 101 insertions(+), 40 deletions(-) rename lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/{begin_reshare_config_view.dart => initiate_resharing_view.dart} (54%) diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index cfcb75a7e..0a26cda75 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/resharing/involved/step_1a/begin_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_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'; @@ -120,7 +120,7 @@ class FrostMSWalletOptionsView extends ConsumerWidget { ref.read(pFrostMyName.state).state = frostInfo.myName; Navigator.of(context).pushNamed( - BeginReshareConfigView.routeName, + InitiateResharingView.routeName, arguments: walletId, ); }, diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart similarity index 54% rename from lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart rename to lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart index 94d2de0b2..32818982c 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart @@ -5,7 +5,6 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stac 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/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; @@ -15,9 +14,10 @@ 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 BeginReshareConfigView extends ConsumerStatefulWidget { - const BeginReshareConfigView({ +final class InitiateResharingView extends ConsumerStatefulWidget { + const InitiateResharingView({ super.key, required this.walletId, }); @@ -27,16 +27,18 @@ final class BeginReshareConfigView extends ConsumerStatefulWidget { final String walletId; @override - ConsumerState createState() => + ConsumerState createState() => _BeginReshareConfigViewState(); } class _BeginReshareConfigViewState - extends ConsumerState { + extends ConsumerState { + late final String myName; late final int currentThreshold; - late final List currentParticipants; + late final List originalParticipants; + late final List currentParticipantsWithoutMe; - final Map pFrostResharersMap = {}; + final Set selectedParticipants = {}; @override void initState() { @@ -50,7 +52,14 @@ class _BeginReshareConfigViewState .getByWalletIdSync(widget.walletId)!; currentThreshold = frostInfo.threshold; - currentParticipants = frostInfo.participants; + 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(); } @@ -83,10 +92,10 @@ class _BeginReshareConfigViewState Navigator.of(context).pop(); }, ), - // title: Text( - // "Modify Participants", - // style: STextStyles.navBarTitle(context), - // ), + title: Text( + "Initiate resharing", + style: STextStyles.navBarTitle(context), + ), ), body: SafeArea( child: LayoutBuilder( @@ -113,33 +122,48 @@ class _BeginReshareConfigViewState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Select participants for resharing", - style: STextStyles.label(context), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Select group members who will participate in resharing.", + style: STextStyles.w600_12(context), + ), + const SizedBox( + height: 10, + ), + Text( + "You must have the threshold number of members (including you) to initiate resharing.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), ), const SizedBox( height: 16, ), Column( children: [ - for (int i = 0; i < currentParticipants.length; i++) + for (int i = 0; i < currentParticipantsWithoutMe.length; i++) Padding( padding: const EdgeInsets.only( top: 10, ), - child: RawMaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), + child: RoundedWhiteContainer( + padding: EdgeInsets.zero, onPressed: () { - if (pFrostResharersMap[currentParticipants[i]] == - null) { - pFrostResharersMap[currentParticipants[i]] = i; + if (selectedParticipants + .contains(currentParticipantsWithoutMe[i])) { + selectedParticipants + .remove(currentParticipantsWithoutMe[i]); } else { - pFrostResharersMap.remove(currentParticipants[i]); + selectedParticipants + .add(currentParticipantsWithoutMe[i]); } setState(() {}); @@ -150,16 +174,15 @@ class _BeginReshareConfigViewState child: Row( children: [ Checkbox( - value: pFrostResharersMap[ - currentParticipants[i]] == - i, - onChanged: (bool? value) {}, + value: selectedParticipants + .contains(currentParticipantsWithoutMe[i]), + onChanged: (_) {}, ), const SizedBox( width: 10, ), Text( - currentParticipants[i], + currentParticipantsWithoutMe[i], style: STextStyles.itemSubtitle12(context), ), ], @@ -174,16 +197,54 @@ class _BeginReshareConfigViewState const SizedBox( height: 16, ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Required members", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + Text( + // +1 is included as the initiator who will also take part + "${selectedParticipants.length + 1} / $currentThreshold", + style: STextStyles.w500_14(context).copyWith( + color: selectedParticipants.length + 1 >= currentThreshold + ? Theme.of(context) + .extension()! + .accentColorGreen + : Theme.of(context) + .extension()! + .accentColorRed, + ), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), PrimaryButton( label: "Continue", - enabled: pFrostResharersMap.length >= currentThreshold, + // +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 List resharers = []; + + for (final name in selectedParticipants) { + resharers.add(originalParticipants.indexOf(name)); + } + await Navigator.of(context).pushNamed( CompleteReshareConfigView.routeName, arguments: ( walletId: widget.walletId, - resharers: - pFrostResharersMap.values.toList(growable: false), + resharers: resharers, ), ); }, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 83118fff1..a9fac8581 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -126,9 +126,9 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settin import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart'; @@ -634,11 +634,11 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case BeginReshareConfigView.routeName: + case InitiateResharingView.routeName: if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BeginReshareConfigView( + builder: (_) => InitiateResharingView( walletId: args, ), settings: RouteSettings( From 3bddec00f1d44ac801057b908df12e2b9c1751c3 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 12:25:24 -0600 Subject: [PATCH 177/272] fix frost restore routing error --- .../name_your_wallet_view/name_your_wallet_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index f3867f74c..3cfeb6bbd 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -395,7 +395,10 @@ class _NameYourWalletViewState extends ConsumerState { RestoreFrostMsWalletView.routeName, arguments: ( walletName: name, - coin: coin, + // TODO: [prio=med] this will cause issues if frost is ever applied to other coins + frostCurrency: coin.isTestNet + ? BitcoinFrost(CryptoCurrencyNetwork.test) + : BitcoinFrost(CryptoCurrencyNetwork.main), ), ); }, From a9449ace0d203c4b36e5d1cceac4c2591c359716 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 15:35:15 -0600 Subject: [PATCH 178/272] various frost settings view ui tweaks and fixes --- .../frost_ms/frost_participants_view.dart | 69 +++++-- .../step_1a/complete_reshare_config_view.dart | 169 ++++++++++++++-- .../step_1a/display_reshare_config_view.dart | 187 +++++++++++++++++- .../step_1b/import_reshare_config_view.dart | 73 ++++++- 4 files changed, 456 insertions(+), 42 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart index b4710bfe7..364859ef0 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart @@ -1,8 +1,11 @@ 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'; @@ -11,6 +14,7 @@ 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({ @@ -84,31 +88,56 @@ class FrostParticipantsView extends ConsumerWidget { ), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ for (int i = 0; i < frostInfo.participants.length; i++) Padding( padding: const EdgeInsets.symmetric( - vertical: 8, + vertical: 5, ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Index $i", - style: STextStyles.label(context), - ), - const SizedBox( - height: 6, - ), - SelectableText( - frostInfo.participants[i] == frostInfo.myName - ? "${frostInfo.participants[i]} (me)" - : frostInfo.participants[i], - style: STextStyles.itemSubtitle12(context), - ), - ], + child: RoundedWhiteContainer( + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + frostInfo.participants[i] == frostInfo.myName + ? "${frostInfo.participants[i]} (me)" + : frostInfo.participants[i], + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: frostInfo.participants[i], + ), + ], + ), ), ), ], diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart index 74cfaee17..caa77af5f 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -19,6 +21,7 @@ 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'; import 'package:stackwallet/widgets/stack_dialog.dart'; final class CompleteReshareConfigView extends ConsumerStatefulWidget { @@ -45,9 +48,12 @@ class _CompleteReshareConfigViewState final List controllers = []; + late final String myName; + int _participantsCount = 0; bool _buttonLock = false; + bool _includeMeInReshare = false; Future _onPressed() async { if (_buttonLock) { @@ -74,10 +80,16 @@ class _CompleteReshareConfigViewState ); } + final List newParticipants = + controllers.map((e) => e.text.trim()).toList(); + if (_includeMeInReshare) { + newParticipants.insert(0, myName); + } + final config = Frost.createResharerConfig( newThreshold: int.parse(_newThresholdController.text), resharers: widget.resharers, - newParticipants: controllers.map((e) => e.text).toList(), + newParticipants: newParticipants, ); final salt = Format.uint8listToString( @@ -105,7 +117,7 @@ class _CompleteReshareConfigViewState }); } - ref.read(pFrostResharingData).myName = frostInfo.myName; + ref.read(pFrostResharingData).myName = myName; ref.read(pFrostResharingData).resharerConfig = config; if (mounted) { @@ -152,18 +164,29 @@ class _CompleteReshareConfigViewState return "At least two participants required"; } - if (controllers.length != partsCount) { + 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 = controllers - .map((e) => e.text.isEmpty) + final hasEmptyParticipants = newParticipants + .map((e) => e.trim().isEmpty) .reduce((value, element) => value |= element); if (hasEmptyParticipants) { return "Participants must not be empty"; } - if (controllers.length != controllers.map((e) => e.text).toSet().length) { + if (newParticipants.length != newParticipants.toSet().length) { return "Duplicate participant name found"; } @@ -171,8 +194,12 @@ class _CompleteReshareConfigViewState } void _participantsCountChanged(String newValue) { - final count = int.tryParse(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()); @@ -192,6 +219,17 @@ class _CompleteReshareConfigViewState } } + @override + void initState() { + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(widget.walletId)!; + myName = frostInfo.myName; + super.initState(); + } + @override void dispose() { _newThresholdController.dispose(); @@ -231,7 +269,7 @@ class _CompleteReshareConfigViewState }, ), title: Text( - "Modify Participants", + "Edit group details", style: STextStyles.navBarTitle(context), ), ), @@ -258,11 +296,62 @@ class _CompleteReshareConfigViewState ), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + 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.label(context), + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), ), const SizedBox( height: 10, @@ -271,13 +360,32 @@ class _CompleteReshareConfigViewState keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], controller: _newThresholdController, + decoration: InputDecoration( + hintText: "Enter number of signatures", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Text( + "Enter number of signatures required for fund management.", + style: STextStyles.w500_12(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle2, + ), + ), ), const SizedBox( height: 16, ), Text( - "Number of participants", - style: STextStyles.label(context), + "New number of participants", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), ), const SizedBox( height: 10, @@ -287,6 +395,23 @@ class _CompleteReshareConfigViewState inputFormatters: [FilteringTextInputFormatter.digitsOnly], controller: _newParticipantsCountController, onChanged: _participantsCountChanged, + decoration: InputDecoration( + hintText: "Enter number of participants", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Text( + "The number of participants must be equal to or less than the" + " number of required signatures.", + style: STextStyles.w500_12(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle2, + ), + ), ), const SizedBox( height: 16, @@ -294,12 +419,26 @@ class _CompleteReshareConfigViewState if (controllers.isNotEmpty) Text( "Participants", - style: STextStyles.label(context), + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), ), if (controllers.isNotEmpty) const SizedBox( height: 10, ), + if (controllers.isNotEmpty) + RoundedWhiteContainer( + child: Text( + "Type each name in one word without spaces.", + style: STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ), + ), if (controllers.isNotEmpty) Column( children: [ @@ -310,6 +449,10 @@ class _CompleteReshareConfigViewState ), child: TextField( controller: controllers[i], + decoration: InputDecoration( + hintText: "Enter name", + hintStyle: STextStyles.fieldLabel(context), + ), ), ), ], diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart index 2b7f1f899..fa538f29e 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart @@ -1,5 +1,6 @@ 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/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; @@ -8,6 +9,7 @@ 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'; @@ -20,7 +22,10 @@ import 'package:stackwallet/widgets/custom_buttons/simple_copy_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/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class DisplayReshareConfigView extends ConsumerStatefulWidget { @@ -40,9 +45,19 @@ class DisplayReshareConfigView extends ConsumerStatefulWidget { class _DisplayReshareConfigViewState extends ConsumerState { + static const info = [ + "Share this config with the signing group participants as well as any new " + "participant.", + "Wait for them to import the config.", + "Verify that everyone has imported the config. If you try to continue " + "before everyone is ready, the process will be canceled.", + "Check the box and press “Start resharing”.", + ]; + late final bool iAmInvolved; bool _buttonLock = false; + bool _userVerifyContinue = false; Future _onPressed() async { if (_buttonLock) { @@ -88,6 +103,111 @@ class _DisplayReshareConfigViewState } } + void _showParticipantsDialog() { + final participants = + ref.read(pFrostResharingData).configData!.newParticipants; + + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + showCloseButton: false, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "Group participants", + style: STextStyles.w600_20(context), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "The names are case-sensitive and must be entered exactly.", + style: STextStyles.w400_16(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + const SizedBox( + height: 12, + ), + for (final participant in participants) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.background, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + participant, + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: participant, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + ); + } + @override void initState() { // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) @@ -162,7 +282,13 @@ class _DisplayReshareConfigViewState ), child: Column( children: [ - if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), SizedBox( height: 220, child: Row( @@ -197,13 +323,66 @@ class _DisplayReshareConfigViewState SizedBox( height: Util.isDesktop ? 64 : 16, ), - if (!Util.isDesktop) - const Spacer( - flex: 2, + 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) + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has imported the config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + if (iAmInvolved) + const SizedBox( + height: 16, ), if (iAmInvolved) PrimaryButton( label: "Start resharing", + enabled: _userVerifyContinue, onPressed: _onPressed, ), ], diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart index 966a24710..a19d0ec75 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart @@ -21,6 +21,7 @@ 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_step_user_steps.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'; @@ -45,12 +46,22 @@ class ImportReshareConfigView extends ConsumerStatefulWidget { class _ImportReshareConfigViewState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group member who" + " is initiating resharing.", + "Wait for other participants to finish importing the config.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Start resharing”.", + ]; + late final TextEditingController configFieldController; late final FocusNode configFocusNode; bool _configEmpty = true; bool _buttonLock = false; + bool _userVerifyContinue = false; Future _onPressed() async { if (_buttonLock) { @@ -84,14 +95,14 @@ class _ImportReshareConfigViewState if (frostInfo.knownSalts.contains(salt)) { throw Exception("Duplicate config salt"); } else { - final salts = frostInfo.knownSalts; + final salts = frostInfo.knownSalts.toList(); salts.add(salt); final mainDB = ref.read(mainDBProvider); await mainDB.isar.writeTxn(() async { - final info = frostInfo; - await mainDB.isar.frostWalletInfo.delete(info.id); + final id = frostInfo.id; + await mainDB.isar.frostWalletInfo.delete(id); await mainDB.isar.frostWalletInfo.put( - info.copyWith(knownSalts: salts), + frostInfo.copyWith(knownSalts: salts), ); }); } @@ -201,6 +212,20 @@ class _ImportReshareConfigViewState const SizedBox( height: 16, ), + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), + Text( + "Enter config", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), + ), + const SizedBox( + height: 10, + ), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -319,9 +344,47 @@ class _ImportReshareConfigViewState const SizedBox( height: 16, ), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has imported the config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), PrimaryButton( label: "Start resharing", - enabled: !_configEmpty, + enabled: !_configEmpty && _userVerifyContinue, onPressed: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); From d697be81d7ac1aac4f98d5e453735154a029e65d Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 15:43:20 -0600 Subject: [PATCH 179/272] rename temp weird names so git doesn't think I'm completely changing and deleting files --- ...frost_create_step_1.dart => frost_create_step_22.dart} | 2 +- ...frost_create_step_2.dart => frost_create_step_33.dart} | 2 +- ...frost_create_step_3.dart => frost_create_step_44.dart} | 2 +- ...frost_create_step_4.dart => frost_create_step_55.dart} | 0 .../frost_ms/new/steps/frost_route_generator.dart | 8 ++++---- 5 files changed, 7 insertions(+), 7 deletions(-) rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_1.dart => frost_create_step_22.dart} (99%) rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_2.dart => frost_create_step_33.dart} (99%) rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_3.dart => frost_create_step_44.dart} (98%) rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_4.dart => frost_create_step_55.dart} (100%) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_22.dart similarity index 99% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_22.dart index 143804e95..222861dee 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_22.dart @@ -3,7 +3,7 @@ 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/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_22.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_33.dart similarity index 99% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_33.dart index c6f1c7331..8124f5388 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_33.dart @@ -3,7 +3,7 @@ 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/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_44.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_44.dart similarity index 98% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_44.dart index 00b7f7d1a..e8c6ed349 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_44.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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_55.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_55.dart similarity index 100% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_55.dart diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart index a5df36e67..f619c4606 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart @@ -1,10 +1,10 @@ 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_1.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_11.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_22.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_33.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; From 9481475f4579edd3f01136088793e77202f0f0f6 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 15:44:41 -0600 Subject: [PATCH 180/272] add stub and rename back to normal --- .../frost_ms/new/steps/frost_create_step_1.dart | 0 .../{frost_create_step_22.dart => frost_create_step_2.dart} | 0 .../{frost_create_step_33.dart => frost_create_step_3.dart} | 2 +- .../{frost_create_step_44.dart => frost_create_step_4.dart} | 0 .../{frost_create_step_55.dart => frost_create_step_5.dart} | 0 5 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_22.dart => frost_create_step_2.dart} (100%) rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_33.dart => frost_create_step_3.dart} (99%) rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_44.dart => frost_create_step_4.dart} (100%) rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_55.dart => frost_create_step_5.dart} (100%) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_22.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart similarity index 100% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_22.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_33.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart similarity index 99% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_33.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart index 8124f5388..c23f8279f 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_33.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -3,7 +3,7 @@ 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/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_44.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_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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_44.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart similarity index 100% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_44.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_55.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart similarity index 100% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_55.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart From 56b18a6c93760321674697aac8dfb206a37362d6 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 16:14:59 -0600 Subject: [PATCH 181/272] insert step 1a (create new ms config) --- .../new/create_new_frost_ms_wallet_view.dart | 50 ++- .../new/share_new_multisig_config_view.dart | 419 ------------------ .../new/steps/frost_create_step_1.dart | 272 ++++++++++++ .../new/steps/frost_create_step_2.dart | 18 +- .../new/steps/frost_create_step_3.dart | 16 +- .../new/steps/frost_create_step_4.dart | 14 +- .../new/steps/frost_create_step_5.dart | 10 +- .../new/steps/frost_route_generator.dart | 21 +- lib/route_generator.dart | 19 - 9 files changed, 362 insertions(+), 477 deletions(-) delete mode 100644 lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index 533d23e02..b0ddfb3e8 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -1,10 +1,17 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_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/wallets/crypto_currency/intermediate/frost_currency.dart'; @@ -401,12 +408,47 @@ class _NewFrostMsWalletViewState controllers.first.text.trim(); ref.read(pFrostMultisigConfig.notifier).state = config; - await Navigator.of(context).pushNamed( - ShareNewMultisigConfigView.routeName, - arguments: ( + ref.read(pFrostCreateNewArgs.state).state = ( + ( walletName: widget.walletName, frostCurrency: widget.frostCurrency, ), + FrostRouteGenerator.createNewConfigStepRoutes, + () { + // successful completion of steps + if (Util.isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).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(pFrostCreateNewArgs.state).state = null; + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Your wallet is set up.", + iconAsset: Assets.svg.check, + context: context, + ), + ); + } + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, ); }, ), diff --git a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart b/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart deleted file mode 100644 index 30c7a5c23..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart +++ /dev/null @@ -1,419 +0,0 @@ -import 'dart:async'; - -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/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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_desktop_specific/desktop_home_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/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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; -import 'package:stackwallet/widgets/frost_mascot.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; - -class ShareNewMultisigConfigView extends ConsumerStatefulWidget { - const ShareNewMultisigConfigView({ - super.key, - required this.walletName, - required this.frostCurrency, - }); - - static const String routeName = "/shareNewMultisigConfigView"; - - final String walletName; - final FrostCurrency frostCurrency; - - @override - ConsumerState createState() => - _ShareNewMultisigConfigViewState(); -} - -class _ShareNewMultisigConfigViewState - extends ConsumerState { - bool _userVerifyContinue = false; - - void _showParticipantsDialog() { - final participants = Frost.getParticipants( - multisigConfig: ref.read(pFrostMultisigConfig.state).state!, - ); - - showDialog( - context: context, - builder: (_) => SimpleMobileDialog( - showCloseButton: false, - padding: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Text( - "Group participants", - style: STextStyles.w600_20(context), - ), - ), - const SizedBox( - height: 12, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Text( - "The names are case-sensitive and must be entered exactly.", - style: STextStyles.w400_16(context).copyWith( - color: Theme.of(context).extension()!.textDark3, - ), - ), - ), - const SizedBox( - height: 12, - ), - for (final participant in participants) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: double.infinity, - height: 1.5, - color: - Theme.of(context).extension()!.background, - ), - const SizedBox( - height: 12, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - children: [ - Container( - width: 26, - height: 26, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldActiveBG, - borderRadius: BorderRadius.circular( - 200, - ), - ), - child: Center( - child: SvgPicture.asset( - Assets.svg.user, - width: 16, - height: 16, - ), - ), - ), - const SizedBox( - width: 8, - ), - Expanded( - child: Text( - participant, - style: STextStyles.w500_14(context), - ), - ), - const SizedBox( - width: 8, - ), - IconCopyButton( - data: participant, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - ], - ), - const SizedBox( - height: 24, - ), - ], - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - // TODO: [prio=high] get rid of placeholder text?? - trailing: FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Share multisig group info", - 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( - children: [ - const _SharingStepsInfo(), - const SizedBox( - height: 20, - ), - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: - ref.watch(pFrostMultisigConfig.state).state ?? "Error", - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const SizedBox( - height: 20, - ), - DetailItem( - title: "Encoded config", - detail: ref.watch(pFrostMultisigConfig.state).state ?? "Error", - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostMultisigConfig.state).state ?? - "Error", - ) - : SimpleCopyButton( - data: ref.watch(pFrostMultisigConfig.state).state ?? - "Error", - ), - ), - SizedBox( - height: Util.isDesktop ? 64 : 16, - ), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Show group participants", - onPressed: _showParticipantsDialog, - ), - ), - ], - ), - if (!Util.isDesktop) - const Spacer( - flex: 2, - ), - const SizedBox( - height: 16, - ), - GestureDetector( - onTap: () { - setState(() { - _userVerifyContinue = !_userVerifyContinue; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has joined the group", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), - ), - 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(pFrostCreateNewArgs.state).state = ( - ( - walletName: widget.walletName, - frostCurrency: widget.frostCurrency, - ), - FrostRouteGenerator.createNewConfigStepRoutes, - () { - // successful completion of steps - if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); - } else { - unawaited( - Navigator.of(context).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(pFrostCreateNewArgs.state).state = null; - - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Your wallet is set up.", - iconAsset: Assets.svg.check, - context: context, - ), - ); - } - ); - - await Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - // FrostShareCommitmentsView.routeName, - ); - }, - ), - ], - ), - ), - ); - } -} - -class _SharingStepsInfo extends StatelessWidget { - const _SharingStepsInfo({super.key}); - - static const steps = [ - "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”.", - ]; - - @override - Widget build(BuildContext context) { - final style = STextStyles.w500_12(context); - return RoundedWhiteContainer( - child: Column( - children: [ - for (int i = 0; i < steps.length; i++) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${i + 1}.", - style: style, - ), - const SizedBox( - width: 4, - ), - Expanded( - child: Text( - steps[i], - style: style, - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart index e69de29bb..78c297a97 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart @@ -0,0 +1,272 @@ +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/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_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/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 = "/frostCreateStep1"; + static const String title = "Multisig group info"; + + @override + ConsumerState createState() => _FrostCreateStep1aState(); +} + +class _FrostCreateStep1aState extends ConsumerState { + static const info = [ + "Share this config with the group participants.", + "Wait for them to join the group.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Generate keys”.", + ]; + + bool _userVerifyContinue = false; + + void _showParticipantsDialog() { + final participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + showCloseButton: false, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "Group participants", + style: STextStyles.w600_20(context), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "The names are case-sensitive and must be entered exactly.", + style: STextStyles.w400_16(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + const SizedBox( + height: 12, + ), + for (final participant in participants) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.background, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + participant, + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: participant, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 20, + ), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostMultisigConfig.state).state ?? "Error", + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + DetailItem( + title: "Encoded config", + detail: ref.watch(pFrostMultisigConfig.state).state ?? "Error", + button: Util.isDesktop + ? IconCopyButton( + data: + ref.watch(pFrostMultisigConfig.state).state ?? "Error", + ) + : SimpleCopyButton( + data: + ref.watch(pFrostMultisigConfig.state).state ?? "Error", + ), + ), + SizedBox( + height: Util.isDesktop ? 64 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show group participants", + onPressed: _showParticipantsDialog, + ), + ), + ], + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + const SizedBox( + height: 16, + ), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has joined the group", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + 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( + FrostCreateStep2.routeName, + // FrostShareCommitmentsView.routeName, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart index 222861dee..7f755cfa2 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -3,7 +3,7 @@ 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/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_22.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_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'; @@ -27,19 +27,19 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -class FrostCreateStep1 extends ConsumerStatefulWidget { - const FrostCreateStep1({ +class FrostCreateStep2 extends ConsumerStatefulWidget { + const FrostCreateStep2({ super.key, }); - static const String routeName = "/frostCreateStep1"; + static const String routeName = "/frostCreateStep2"; static const String title = "Commitments"; @override - ConsumerState createState() => _FrostCreateStep1State(); + ConsumerState createState() => _FrostCreateStep2State(); } -class _FrostCreateStep1State extends ConsumerState { +class _FrostCreateStep2State extends ConsumerState { static const info = [ "Share your commitment with other group members.", "Enter their commitments into the corresponding fields.", @@ -60,7 +60,7 @@ class _FrostCreateStep1State extends ConsumerState { context: context, builder: (_) => FrostStepQrDialog( myName: ref.read(pFrostMyName)!, - title: "Step 1 of 4 - ${FrostCreateStep1.title}", + title: "Step 2 of 5 - ${FrostCreateStep2.title}", data: myCommitment, ), ); @@ -341,9 +341,9 @@ class _FrostCreateStep1State extends ConsumerState { commitments: commitments, ); - ref.read(pFrostCreateCurrentStep.state).state = 2; + ref.read(pFrostCreateCurrentStep.state).state = 3; await Navigator.of(context).pushNamed( - FrostCreateStep2.routeName, + FrostCreateStep3.routeName, ); } catch (e, s) { Logging.instance.log( diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart index c23f8279f..55da4c407 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -27,17 +27,17 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -class FrostCreateStep2 extends ConsumerStatefulWidget { - const FrostCreateStep2({super.key}); +class FrostCreateStep3 extends ConsumerStatefulWidget { + const FrostCreateStep3({super.key}); - static const String routeName = "/frostCreateStep2"; + static const String routeName = "/frostCreateStep3"; static const String title = "Shares"; @override - ConsumerState createState() => _FrostCreateStep2State(); + ConsumerState createState() => _FrostCreateStep3State(); } -class _FrostCreateStep2State extends ConsumerState { +class _FrostCreateStep3State extends ConsumerState { static const info = [ "Send your share to other group members.", "Enter their shares into the corresponding fields.", @@ -57,7 +57,7 @@ class _FrostCreateStep2State extends ConsumerState { context: context, builder: (_) => FrostStepQrDialog( myName: ref.read(pFrostMyName)!, - title: "Step 2 of 4 - ${FrostCreateStep2.title}", + title: "Step 3 of 5 - ${FrostCreateStep3.title}", data: myShare, ), ); @@ -291,9 +291,9 @@ class _FrostCreateStep2State extends ConsumerState { shares: shares, ); - ref.read(pFrostCreateCurrentStep.state).state = 3; + ref.read(pFrostCreateCurrentStep.state).state = 4; await Navigator.of(context).pushNamed( - FrostCreateStep3.routeName, + FrostCreateStep4.routeName, ); } catch (e, s) { Logging.instance.log( diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart index e8c6ed349..586c45dc0 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_55.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/new/steps/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'; @@ -12,17 +12,17 @@ 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 FrostCreateStep3 extends ConsumerStatefulWidget { - const FrostCreateStep3({super.key}); +class FrostCreateStep4 extends ConsumerStatefulWidget { + const FrostCreateStep4({super.key}); - static const String routeName = "/frostCreateStep3"; + static const String routeName = "/frostCreateStep4"; static const String title = "Verify multisig ID"; @override - ConsumerState createState() => _FrostCreateStep3State(); + ConsumerState createState() => _FrostCreateStep4State(); } -class _FrostCreateStep3State extends ConsumerState { +class _FrostCreateStep4State extends ConsumerState { static const info = [ "Ensure your multisig ID matches that of each other participant.", ]; @@ -64,7 +64,7 @@ class _FrostCreateStep3State extends ConsumerState { onPressed: () { ref.read(pFrostCreateCurrentStep.state).state = 4; Navigator.of(context).pushNamed( - FrostCreateStep4.routeName, + FrostCreateStep5.routeName, ); }, ) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 9cc8ef477..7de9c4828 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -25,17 +25,17 @@ import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; -class FrostCreateStep4 extends ConsumerStatefulWidget { - const FrostCreateStep4({super.key}); +class FrostCreateStep5 extends ConsumerStatefulWidget { + const FrostCreateStep5({super.key}); - static const String routeName = "/frostCreateStep4"; + static const String routeName = "/frostCreateStep5"; static const String title = "Back up your keys"; @override - ConsumerState createState() => _FrostCreateStep4State(); + ConsumerState createState() => _FrostCreateStep5State(); } -class _FrostCreateStep4State extends ConsumerState { +class _FrostCreateStep5State extends ConsumerState { static const _warning = "These are your private keys. Please back them up, " "keep them safe and never share it with anyone. Your private keys are the" " only way you can access your funds if you forget PIN, lose your phone, " diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart index f619c4606..8c7c21e72 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart @@ -1,9 +1,10 @@ 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_11.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_22.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_33.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.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/route_generator.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; @@ -22,20 +23,21 @@ abstract class FrostRouteGenerator { static const bool useMaterialPageRoute = true; static const List createNewConfigStepRoutes = [ - (routeName: FrostCreateStep1.routeName, title: FrostCreateStep1.title), + (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 Route generateRoute(RouteSettings settings) { final args = settings.arguments; switch (settings.name) { - case FrostCreateStep1.routeName: + case FrostCreateStep1a.routeName: return RouteGenerator.getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => const FrostCreateStep1(), + builder: (_) => const FrostCreateStep1a(), settings: settings, ); @@ -60,6 +62,13 @@ abstract class FrostRouteGenerator { settings: settings, ); + case FrostCreateStep5.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep5(), + settings: settings, + ); + default: return _routeError(""); } diff --git a/lib/route_generator.dart b/lib/route_generator.dart index a9fac8581..41155ebc8 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -30,7 +30,6 @@ import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.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/import_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/new/share_new_multisig_config_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart'; @@ -483,24 +482,6 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case ShareNewMultisigConfigView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ShareNewMultisigConfigView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - case SelectNewFrostImportTypeView.routeName: if (args is ({ String walletName, From 999df80e6032181320207a3b5d9e900d73fd8a99 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 29 Apr 2024 17:30:46 -0600 Subject: [PATCH 182/272] refactor "new ms config import" to step 1b --- .../new/import_new_frost_ms_wallet_view.dart | 433 ------------------ .../select_new_frost_import_type_view.dart | 80 +++- ..._step_1.dart => frost_create_step_1a.dart} | 2 +- .../new/steps/frost_create_step_1b.dart | 368 +++++++++++++++ .../new/steps/frost_route_generator.dart | 18 +- lib/route_generator.dart | 19 - 6 files changed, 451 insertions(+), 469 deletions(-) delete mode 100644 lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart rename lib/pages/add_wallet_views/frost_ms/new/steps/{frost_create_step_1.dart => frost_create_step_1a.dart} (99%) create mode 100644 lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart diff --git a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart deleted file mode 100644 index 531d7971f..000000000 --- a/lib/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart +++ /dev/null @@ -1,433 +0,0 @@ -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:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_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/constants.dart'; -import 'package:stackwallet/utilities/logger.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/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 ImportNewFrostMsWalletView extends ConsumerStatefulWidget { - const ImportNewFrostMsWalletView({ - super.key, - required this.walletName, - required this.frostCurrency, - }); - - static const String routeName = "/importNewFrostMsWalletView"; - - final String walletName; - final FrostCurrency frostCurrency; - - @override - ConsumerState createState() => - _ImportNewFrostMsWalletViewState(); -} - -class _ImportNewFrostMsWalletViewState - extends ConsumerState { - late final TextEditingController myNameFieldController, configFieldController; - late final FocusNode myNameFocusNode, configFocusNode; - - bool _nameEmpty = true, _configEmpty = true; - - @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 ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - // TODO: [prio=high] get rid of placeholder text?? - trailing: FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Import FROST multisig config", - 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: myNameFieldController, - onChanged: (_) { - setState(() { - _nameEmpty = myNameFieldController.text.isEmpty; - }); - }, - focusNode: myNameFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "My name", - myNameFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _nameEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_nameEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frMyNameClearButtonKey"), - onTap: () { - myNameFieldController.text = ""; - - setState(() { - _nameEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Name Field.", - key: const Key("frMyNamePasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - myNameFieldController.text = - data.text!.trim(); - } - - setState(() { - _nameEmpty = - myNameFieldController.text.isEmpty; - }); - }, - child: _nameEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Start key generation", - enabled: !_nameEmpty && !_configEmpty, - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - } - - final config = configFieldController.text; - - if (!Frost.validateEncodedMultisigConfig( - encodedConfig: config)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Invalid config", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - if (!Frost.getParticipants(multisigConfig: config) - .contains(myNameFieldController.text)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "My name not found in config participants", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - ref.read(pFrostMyName.state).state = myNameFieldController.text; - ref.read(pFrostMultisigConfig.notifier).state = config; - - ref.read(pFrostStartKeyGenData.state).state = - Frost.startKeyGeneration( - multisigConfig: ref.read(pFrostMultisigConfig.state).state!, - myName: ref.read(pFrostMyName.state).state!, - ); - - ref.read(pFrostCreateNewArgs.state).state = ( - ( - walletName: widget.walletName, - frostCurrency: widget.frostCurrency, - ), - FrostRouteGenerator.createNewConfigStepRoutes, - () { - // successful completion of steps - if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); - } else { - unawaited( - Navigator.of(context).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(pFrostCreateNewArgs.state).state = null; - - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Your wallet is set up.", - iconAsset: Assets.svg.check, - context: context, - ), - ); - } - ); - - await Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index e35822cac..7a797ed9a 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -1,8 +1,16 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -17,7 +25,7 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -class SelectNewFrostImportTypeView extends StatefulWidget { +class SelectNewFrostImportTypeView extends ConsumerStatefulWidget { const SelectNewFrostImportTypeView({ super.key, required this.walletName, @@ -30,12 +38,12 @@ class SelectNewFrostImportTypeView extends StatefulWidget { final FrostCurrency frostCurrency; @override - State createState() => + ConsumerState createState() => _SelectNewFrostImportTypeViewState(); } class _SelectNewFrostImportTypeViewState - extends State { + extends ConsumerState { _ImportOption _selectedOption = _ImportOption.multisigNew; @override @@ -133,18 +141,60 @@ class _SelectNewFrostImportTypeViewState final String route; switch (_selectedOption) { case _ImportOption.multisigNew: - route = ImportNewFrostMsWalletView.routeName; - case _ImportOption.resharerExisting: - route = NewImportResharerConfigView.routeName; - } + ref.read(pFrostCreateNewArgs.state).state = ( + ( + walletName: widget.walletName, + frostCurrency: widget.frostCurrency, + ), + FrostRouteGenerator.importNewConfigStepRoutes, + () { + // successful completion of steps + if (Util.isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + } - await Navigator.of(context).pushNamed( - route, - arguments: ( - walletName: widget.walletName, - frostCurrency: widget.frostCurrency, - ), - ); + ref.read(pFrostMultisigConfig.state).state = null; + ref.read(pFrostStartKeyGenData.state).state = null; + ref.read(pFrostSecretSharesData.state).state = null; + ref.read(pFrostCreateNewArgs.state).state = null; + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Your wallet is set up.", + iconAsset: Assets.svg.check, + context: context, + ), + ); + } + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + break; + + case _ImportOption.resharerExisting: + await Navigator.of(context).pushNamed( + NewImportResharerConfigView.routeName, + arguments: ( + walletName: widget.walletName, + frostCurrency: widget.frostCurrency, + ), + ); + break; + } }, ) ], diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart similarity index 99% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart rename to lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart index 78c297a97..f4fab1000 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart @@ -21,7 +21,7 @@ import 'package:stackwallet/widgets/frost_step_user_steps.dart'; class FrostCreateStep1a extends ConsumerStatefulWidget { const FrostCreateStep1a({super.key}); - static const String routeName = "/frostCreateStep1"; + static const String routeName = "/frostCreateStep1a"; static const String title = "Multisig group info"; @override diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart new file mode 100644 index 000000000..88d3af415 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart @@ -0,0 +1,368 @@ +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: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_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.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 FrostCreateStep1b extends ConsumerStatefulWidget { + const FrostCreateStep1b({super.key}); + + static const String routeName = "/frostCreateStep1b"; + static const String title = "Import group info"; + + @override + ConsumerState createState() => _FrostCreateStep1bState(); +} + +class _FrostCreateStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group creator.", + "Enter your name EXACTLY as the group creator entered it. When in doubt, " + "double check with them. The names are case-sensitive.", + "Wait for other participants to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Generate keys”.", + ]; + + late final TextEditingController myNameFieldController, configFieldController; + late final FocusNode myNameFocusNode, configFocusNode; + + bool _nameEmpty = true, _configEmpty = true, _userVerifyContinue = false; + + @override + void initState() { + myNameFieldController = TextEditingController(); + configFieldController = TextEditingController(); + myNameFocusNode = FocusNode(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + myNameFieldController.dispose(); + configFieldController.dispose(); + myNameFocusNode.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frMyNameTextFieldKey"), + controller: myNameFieldController, + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, + focusNode: myNameFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "My name", + myNameFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _nameEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_nameEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frMyNameClearButtonKey"), + onTap: () { + myNameFieldController.text = ""; + + setState(() { + _nameEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Name Field.", + key: const Key("frMyNamePasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + myNameFieldController.text = + data.text!.trim(); + } + + setState(() { + _nameEmpty = + myNameFieldController.text.isEmpty; + }); + }, + child: _nameEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has joined the group", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start key generation", + enabled: _userVerifyContinue && !_nameEmpty && !_configEmpty, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final config = configFieldController.text; + + if (!Frost.validateEncodedMultisigConfig(encodedConfig: config)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Invalid config", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + if (!Frost.getParticipants(multisigConfig: config) + .contains(myNameFieldController.text)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "My name not found in config participants", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + ref.read(pFrostMyName.state).state = myNameFieldController.text; + ref.read(pFrostMultisigConfig.notifier).state = config; + + ref.read(pFrostStartKeyGenData.state).state = + Frost.startKeyGeneration( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + myName: ref.read(pFrostMyName.state).state!, + ); + ref.read(pFrostCreateCurrentStep.state).state = 2; + Navigator.of(context).pushNamed( + FrostCreateStep2.routeName, + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart index 8c7c21e72..5e848fd48 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart @@ -1,7 +1,8 @@ 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_1.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'; @@ -30,6 +31,14 @@ abstract class FrostRouteGenerator { (routeName: FrostCreateStep5.routeName, title: FrostCreateStep5.title), ]; + static const List importNewConfigStepRoutes = [ + (routeName: FrostCreateStep1b.routeName, title: FrostCreateStep1b.title), + (routeName: FrostCreateStep2.routeName, title: FrostCreateStep2.title), + (routeName: FrostCreateStep3.routeName, title: FrostCreateStep3.title), + (routeName: FrostCreateStep4.routeName, title: FrostCreateStep4.title), + (routeName: FrostCreateStep5.routeName, title: FrostCreateStep5.title), + ]; + static Route generateRoute(RouteSettings settings) { final args = settings.arguments; @@ -41,6 +50,13 @@ abstract class FrostRouteGenerator { settings: settings, ); + case FrostCreateStep1b.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep1b(), + settings: settings, + ); + case FrostCreateStep2.routeName: return RouteGenerator.getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 41155ebc8..594a586ea 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -28,7 +28,6 @@ import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_vi import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.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/import_new_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; @@ -500,24 +499,6 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case ImportNewFrostMsWalletView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ImportNewFrostMsWalletView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - case NewImportResharerConfigView.routeName: if (args is ({ String walletName, From cb5f4e61de08c8c647b21c36bd126f05176962e3 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 30 Apr 2024 15:30:41 -0600 Subject: [PATCH 183/272] wrap one way frost flow in sub navigation --- .../frost_ms/frost_scaffold.dart | 4 +- .../new/create_new_frost_ms_wallet_view.dart | 11 +- .../select_new_frost_import_type_view.dart | 30 +- .../new/steps/frost_create_step_1a.dart | 6 +- .../new/steps/frost_create_step_1b.dart | 8 +- .../new/steps/frost_create_step_2.dart | 6 +- .../new/steps/frost_create_step_3.dart | 6 +- .../new/steps/frost_create_step_4.dart | 8 +- .../new/steps/frost_create_step_5.dart | 8 +- .../new/steps/frost_route_generator.dart | 121 ++++- .../reshare/frost_reshare_step_1a.dart} | 281 +++++------ .../reshare/frost_reshare_step_1b.dart | 349 ++++++++++++++ .../reshare/frost_reshare_step_1c.dart | 425 +++++++++++++++++ .../reshare/frost_reshare_step_2abd.dart | 343 ++++++++++++++ .../reshare/frost_reshare_step_2c.dart | 265 +++++++++++ .../reshare/frost_reshare_step_3abd.dart | 331 +++++++++++++ .../reshare/frost_reshare_step_3c.dart | 91 ++++ .../reshare/frost_reshare_step_4.dart | 369 +++++++++++++++ .../reshare/frost_reshare_step_5.dart | 222 +++++++++ .../frost_ms/frost_ms_options_view.dart | 28 +- .../complete_reshare_config_view.dart | 24 +- .../initiate_resharing_view.dart | 2 +- .../resharing/finish_resharing_view.dart | 437 ----------------- .../step_1b/import_reshare_config_view.dart | 401 ---------------- .../involved/step_2/begin_resharing_view.dart | 439 ------------------ .../step_2/continue_resharing_view.dart | 429 ----------------- .../new/new_continue_sharing_view.dart | 198 -------- .../new/new_import_resharer_config_view.dart | 431 ----------------- .../new/new_start_resharing_view.dart | 373 --------------- .../resharing/verify_updated_wallet_view.dart | 315 ------------- lib/route_generator.dart | 143 +----- 31 files changed, 2719 insertions(+), 3385 deletions(-) rename lib/pages/{settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart => add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart} (55%) create mode 100644 lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart create mode 100644 lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart rename lib/pages/settings_views/wallet_settings_view/frost_ms/{resharing/involved/step_1a => initiate_resharing}/complete_reshare_config_view.dart (94%) rename lib/pages/settings_views/wallet_settings_view/frost_ms/{resharing/involved/step_1a => initiate_resharing}/initiate_resharing_view.dart (99%) delete mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart delete mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart delete mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart delete mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart delete mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart delete mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart delete mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart delete mode 100644 lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart diff --git a/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart b/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart index ab85dd776..b2f2dcbe3 100644 --- a/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart +++ b/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart @@ -38,7 +38,7 @@ class _FrostScaffoldState extends ConsumerState { if (context.mounted) { Navigator.of(context).pop(); - ref.read(pFrostCreateNewArgs.state).state = null; + ref.read(pFrostScaffoldArgs.state).state = null; } _requestPopLock = false; @@ -46,7 +46,7 @@ class _FrostScaffoldState extends ConsumerState { @override void initState() { - _routes = ref.read(pFrostCreateNewArgs)!.$2; + _routes = ref.read(pFrostScaffoldArgs)!.stepRoutes; super.initState(); } diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index b0ddfb3e8..f9f3b8caf 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -408,13 +408,14 @@ class _NewFrostMsWalletViewState controllers.first.text.trim(); ref.read(pFrostMultisigConfig.notifier).state = config; - ref.read(pFrostCreateNewArgs.state).state = ( - ( + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( walletName: widget.walletName, frostCurrency: widget.frostCurrency, ), - FrostRouteGenerator.createNewConfigStepRoutes, - () { + walletId: null, + stepRoutes: FrostRouteGenerator.createNewConfigStepRoutes, + onSuccess: () { // successful completion of steps if (Util.isDesktop) { Navigator.of(context).popUntil( @@ -434,7 +435,7 @@ class _NewFrostMsWalletViewState ref.read(pFrostMultisigConfig.state).state = null; ref.read(pFrostStartKeyGenData.state).state = null; ref.read(pFrostSecretSharesData.state).state = null; - ref.read(pFrostCreateNewArgs.state).state = null; + ref.read(pFrostScaffoldArgs.state).state = null; unawaited( showFloatingFlushBar( diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index 7a797ed9a..ddf01aacb 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -7,7 +7,6 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; @@ -141,13 +140,14 @@ class _SelectNewFrostImportTypeViewState final String route; switch (_selectedOption) { case _ImportOption.multisigNew: - ref.read(pFrostCreateNewArgs.state).state = ( - ( + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( walletName: widget.walletName, frostCurrency: widget.frostCurrency, ), - FrostRouteGenerator.importNewConfigStepRoutes, - () { + walletId: null, // no wallet id yet + stepRoutes: FrostRouteGenerator.importNewConfigStepRoutes, + onSuccess: () { // successful completion of steps if (Util.isDesktop) { Navigator.of(context).popUntil( @@ -167,7 +167,7 @@ class _SelectNewFrostImportTypeViewState ref.read(pFrostMultisigConfig.state).state = null; ref.read(pFrostStartKeyGenData.state).state = null; ref.read(pFrostSecretSharesData.state).state = null; - ref.read(pFrostCreateNewArgs.state).state = null; + ref.read(pFrostScaffoldArgs.state).state = null; unawaited( showFloatingFlushBar( @@ -179,22 +179,26 @@ class _SelectNewFrostImportTypeViewState ); } ); - - await Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - ); break; case _ImportOption.resharerExisting: - await Navigator.of(context).pushNamed( - NewImportResharerConfigView.routeName, - arguments: ( + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( walletName: widget.walletName, frostCurrency: widget.frostCurrency, ), + walletId: null, // no wallet id yet + stepRoutes: FrostRouteGenerator.joinReshareStepRoutes, + onSuccess: () { + // successful completion of steps + } ); break; } + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); }, ) ], diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart index f4fab1000..8c292c574 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart @@ -2,7 +2,6 @@ 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/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_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'; @@ -260,7 +259,10 @@ class _FrostCreateStep1aState extends ConsumerState { ref.read(pFrostCreateCurrentStep.state).state = 2; await Navigator.of(context).pushNamed( - FrostCreateStep2.routeName, + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, // FrostShareCommitmentsView.routeName, ); }, diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart index 88d3af415..ff9c01729 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart @@ -4,7 +4,6 @@ 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: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_route_generator.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; @@ -356,8 +355,11 @@ class _FrostCreateStep1bState extends ConsumerState { myName: ref.read(pFrostMyName.state).state!, ); ref.read(pFrostCreateCurrentStep.state).state = 2; - Navigator.of(context).pushNamed( - FrostCreateStep2.routeName, + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, ); }, ) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart index 7f755cfa2..dea6b9dcb 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -3,7 +3,6 @@ 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/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_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'; @@ -343,7 +342,10 @@ class _FrostCreateStep2State extends ConsumerState { ref.read(pFrostCreateCurrentStep.state).state = 3; await Navigator.of(context).pushNamed( - FrostCreateStep3.routeName, + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, ); } catch (e, s) { Logging.instance.log( diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart index 55da4c407..1d6de5c39 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -3,7 +3,6 @@ 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/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_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'; @@ -293,7 +292,10 @@ class _FrostCreateStep3State extends ConsumerState { ref.read(pFrostCreateCurrentStep.state).state = 4; await Navigator.of(context).pushNamed( - FrostCreateStep4.routeName, + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, ); } catch (e, s) { Logging.instance.log( diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart index 586c45dc0..93fff4f77 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart @@ -2,7 +2,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/new/steps/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'; @@ -62,9 +61,12 @@ class _FrostCreateStep4State extends ConsumerState { PrimaryButton( label: "Confirm", onPressed: () { - ref.read(pFrostCreateCurrentStep.state).state = 4; + ref.read(pFrostCreateCurrentStep.state).state = 5; Navigator.of(context).pushNamed( - FrostCreateStep5.routeName, + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, ); }, ) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 7de9c4828..6b7a674c0 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -162,11 +162,11 @@ class _FrostCreateStep5State extends ConsumerState { ), ); - final data = ref.read(pFrostCreateNewArgs)!; + final data = ref.read(pFrostScaffoldArgs)!; final info = WalletInfo.createNew( - coin: data.$1.frostCurrency.coin, - name: data.$1.walletName, + coin: data.info.frostCurrency.coin, + name: data.info.walletName, ); final wallet = await Wallet.create( @@ -206,7 +206,7 @@ class _FrostCreateStep5State extends ConsumerState { } if (mounted) { - ref.read(pFrostCreateNewArgs)!.$3(); + ref.read(pFrostScaffoldArgs)!.onSuccess(); } } catch (e, s) { Logging.instance.log( diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart index 5e848fd48..67e96136b 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart @@ -7,18 +7,28 @@ import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_crea 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/route_generator.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; typedef FrostStepRoute = ({String routeName, String title}); final pFrostCreateCurrentStep = StateProvider.autoDispose((ref) => 1); -final pFrostCreateNewArgs = StateProvider< - ( - ({String walletName, FrostCurrency frostCurrency}), - List, - VoidCallback, - )?>((ref) => null); +final pFrostScaffoldArgs = StateProvider< + ({ + ({String walletName, FrostCurrency frostCurrency}) info, + String? walletId, + List stepRoutes, + VoidCallback onSuccess, + })?>((ref) => null); abstract class FrostRouteGenerator { static const bool useMaterialPageRoute = true; @@ -39,6 +49,42 @@ abstract class FrostRouteGenerator { (routeName: FrostCreateStep5.routeName, title: FrostCreateStep5.title), ]; + static const List initiateReshareStepRoutes = [ + (routeName: FrostReshareStep1a.routeName, title: FrostReshareStep1a.title), + ( + routeName: FrostReshareStep2abd.routeName, + title: FrostReshareStep2abd.title + ), + ( + routeName: FrostReshareStep3abd.routeName, + title: FrostReshareStep3abd.title + ), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + + static const List importReshareStepRoutes = [ + (routeName: FrostReshareStep1b.routeName, title: FrostReshareStep1b.title), + ( + routeName: FrostReshareStep2abd.routeName, + title: FrostReshareStep2abd.title + ), + ( + routeName: FrostReshareStep3abd.routeName, + title: FrostReshareStep3abd.title + ), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + + static const List joinReshareStepRoutes = [ + (routeName: FrostReshareStep1c.routeName, title: FrostReshareStep1c.title), + (routeName: FrostReshareStep2c.routeName, title: FrostReshareStep2c.title), + (routeName: FrostReshareStep3c.routeName, title: FrostReshareStep3c.title), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + static Route generateRoute(RouteSettings settings) { final args = settings.arguments; @@ -85,6 +131,69 @@ abstract class FrostRouteGenerator { 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 FrostReshareStep2abd(), + settings: settings, + ); + + case FrostReshareStep3c.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep2c(), + 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, + ); + default: return _routeError(""); } diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart similarity index 55% rename from lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart rename to lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart index fa538f29e..d99851e03 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart @@ -2,7 +2,7 @@ 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/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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'; @@ -15,12 +15,7 @@ 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/custom_buttons/simple_copy_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/detail_item.dart'; @@ -28,23 +23,17 @@ import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; import 'package:stackwallet/widgets/frost_step_user_steps.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -class DisplayReshareConfigView extends ConsumerStatefulWidget { - const DisplayReshareConfigView({ - super.key, - required this.walletId, - }); +class FrostReshareStep1a extends ConsumerStatefulWidget { + const FrostReshareStep1a({super.key}); - static const String routeName = "/displayReshareConfigView"; - - final String walletId; + static const String routeName = "/frostReshareStep1a"; + static const String title = "Resharer config"; @override - ConsumerState createState() => - _DisplayReshareConfigViewState(); + ConsumerState createState() => _FrostReshareStep1aState(); } -class _DisplayReshareConfigViewState - extends ConsumerState { +class _FrostReshareStep1aState extends ConsumerState { static const info = [ "Share this config with the signing group participants as well as any new " "participant.", @@ -67,7 +56,8 @@ class _DisplayReshareConfigViewState try { final wallet = - ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; final serializedKeys = await wallet.getSerializedKeys(); if (mounted) { @@ -78,9 +68,12 @@ class _DisplayReshareConfigViewState ref.read(pFrostResharingData).startResharerData = result; + ref.read(pFrostCreateCurrentStep.state).state = 2; await Navigator.of(context).pushNamed( - BeginResharingView.routeName, - arguments: widget.walletId, + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, ); } } catch (e, s) { @@ -215,7 +208,7 @@ class _DisplayReshareConfigViewState .read(mainDBProvider) .isar .frostWalletInfo - .getByWalletIdSync(widget.walletId)!; + .getByWalletIdSync(ref.read(pFrostScaffoldArgs)!.walletId!)!; final myOldIndex = frostInfo.participants.indexOf(frostInfo.myName); @@ -229,164 +222,110 @@ class _DisplayReshareConfigViewState @override Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Resharer config", - 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, - ), - ), - ), - ); - }, - ), - ), + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, ), - ), - child: Column( - children: [ - const SizedBox( - height: 16, - ), - const FrostStepUserSteps( - userSteps: info, - ), - const SizedBox(height: 20), - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref.watch(pFrostResharingData).resharerConfig!, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const SizedBox( - height: 32, - ), - DetailItem( - title: "Config", - detail: ref.watch(pFrostResharingData).resharerConfig!, - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostResharingData).resharerConfig!, - ) - : SimpleCopyButton( - data: ref.watch(pFrostResharingData).resharerConfig!, - ), - ), - SizedBox( - height: Util.isDesktop ? 64 : 16, - ), - Row( + const SizedBox(height: 20), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: SecondaryButton( - label: "Show group participants", - onPressed: _showParticipantsDialog, - ), + QrImageView( + data: ref.watch(pFrostResharingData).resharerConfig!, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, ), ], ), - if (iAmInvolved && !Util.isDesktop) const Spacer(), - if (iAmInvolved) - const SizedBox( - height: 16, - ), - if (iAmInvolved) - GestureDetector( - onTap: () { - setState(() { - _userVerifyContinue = !_userVerifyContinue; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has imported the config", - style: STextStyles.w500_14(context), - ), - ), - ], + ), + const SizedBox( + height: 32, + ), + DetailItem( + title: "Config", + detail: ref.watch(pFrostResharingData).resharerConfig!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostResharingData).resharerConfig!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostResharingData).resharerConfig!, ), + ), + SizedBox( + height: Util.isDesktop ? 64 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show group participants", + onPressed: _showParticipantsDialog, ), ), - if (iAmInvolved) - const SizedBox( - height: 16, + ], + ), + if (iAmInvolved && !Util.isDesktop) const Spacer(), + if (iAmInvolved) + const SizedBox( + height: 16, + ), + if (iAmInvolved) + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has imported the config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), ), - if (iAmInvolved) - PrimaryButton( - label: "Start resharing", - enabled: _userVerifyContinue, - onPressed: _onPressed, - ), - ], - ), + ), + if (iAmInvolved) + const SizedBox( + height: 16, + ), + if (iAmInvolved) + PrimaryButton( + label: "Start resharing", + enabled: _userVerifyContinue, + onPressed: _onPressed, + ), + ], ), ); } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart new file mode 100644 index 000000000..b9c41ec7d --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart @@ -0,0 +1,349 @@ +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'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.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/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.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 FrostReshareStep1b extends ConsumerStatefulWidget { + const FrostReshareStep1b({ + super.key, + }); + + static const String routeName = "/frostReshareStep1b"; + static const String title = "Import reshare config"; + + @override + ConsumerState createState() => _FrostReshareStep1bState(); +} + +class _FrostReshareStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group member who" + " is initiating resharing.", + "Wait for other participants to finish importing the config.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Start resharing”.", + ]; + + late final TextEditingController configFieldController; + late final FocusNode configFocusNode; + + bool _configEmpty = true; + + bool _buttonLock = false; + bool _userVerifyContinue = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + final walletId = ref.read(pFrostScaffoldArgs)!.walletId!; + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; + + ref.read(pFrostResharingData).reset(); + ref.read(pFrostResharingData).myName = frostInfo.myName; + ref.read(pFrostResharingData).resharerConfig = configFieldController.text; + + String? salt; + try { + salt = Format.uint8listToString( + resharerSalt( + resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + ), + ); + } 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: ref.read(pFrostResharingData).resharerConfig!, + ); + + ref.read(pFrostResharingData).startResharerData = result; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } 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), + Text( + "Enter config", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has imported the config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start resharing", + enabled: !_configEmpty && _userVerifyContinue, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + await _onPressed(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart new file mode 100644 index 000000000..3e0e17fdf --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -0,0 +1,425 @@ +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:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.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/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.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 FrostReshareStep1c extends ConsumerStatefulWidget { + const FrostReshareStep1c({super.key}); + + static const String routeName = "/frostReshareStep1c"; + static const String title = "Import reshare config"; + + @override + ConsumerState createState() => _FrostReshareStep1cState(); +} + +class _FrostReshareStep1cState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group creator.", + "Enter your name EXACTLY as the group creator entered it. When in doubt, " + "double check with them. The names are case-sensitive.", + "Wait for other participants to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process could be canceled.", + "Check the box and press “Join group”.", + ]; + + late final TextEditingController myNameFieldController, configFieldController; + late final FocusNode myNameFocusNode, configFocusNode; + + bool _nameEmpty = true, + _configEmpty = true, + _userVerifyContinue = false, + _buttonLock = false; + + Future _createWallet() async { + final data = ref.read(pFrostScaffoldArgs)!; + + final info = WalletInfo.createNew( + name: data.info.walletName, + coin: data.info.frostCurrency.coin, + ); + + final wallet = IncompleteFrostWallet(); + wallet.info = info; + + return wallet; + } + + @override + void initState() { + myNameFieldController = TextEditingController(); + configFieldController = TextEditingController(); + myNameFocusNode = FocusNode(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + myNameFieldController.dispose(); + configFieldController.dispose(); + myNameFocusNode.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frMyNameTextFieldKey"), + controller: myNameFieldController, + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, + focusNode: myNameFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "My name", + myNameFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _nameEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_nameEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frMyNameClearButtonKey"), + onTap: () { + myNameFieldController.text = ""; + + setState(() { + _nameEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Name Field.", + key: const Key("frMyNamePasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + myNameFieldController.text = + data.text!.trim(); + } + + setState(() { + _nameEmpty = + myNameFieldController.text.isEmpty; + }); + }, + child: _nameEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + GestureDetector( + onTap: () { + setState(() { + _userVerifyContinue = !_userVerifyContinue; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _userVerifyContinue, + onChanged: (value) => setState( + () => _userVerifyContinue = value == true, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I have verified that everyone has joined the group", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + 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).resharerConfig = + configFieldController.text; + + if (!ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!)) { + ref.read(pFrostResharingData).reset(); + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "My name not found in config participants", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + Exception? ex; + final wallet = await showLoading( + whileFuture: _createWallet(), + context: context, + message: "Setting up wallet", + isDesktop: Util.isDesktop, + 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, + onSuccess: data.onSuccess, + ); + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart new file mode 100644 index 000000000..fdbb2f4cf --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart @@ -0,0 +1,343 @@ +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/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 FrostReshareStep2abd extends ConsumerStatefulWidget { + const FrostReshareStep2abd({super.key}); + + static const String routeName = "/FrostReshareStep2abd"; + static const String title = "Resharers"; + + @override + ConsumerState createState() => + _FrostReshareStep2abdState(); +} + +class _FrostReshareStep2abdState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List resharerIndexes; + late final int myResharerIndexIndex; + late final String myResharerStart; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + if (!amOutgoingParticipant) { + // collect resharer strings + final resharerStarts = controllers.map((e) => e.text).toList(); + if (myResharerIndexIndex >= 0) { + // only insert my own at the correct index if I am a resharer + resharerStarts.insert(myResharerIndexIndex, myResharerStart); + } + + final result = Frost.beginReshared( + myName: ref.read(pFrostResharingData).myName!, + resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + resharerStarts: resharerStarts, + ); + + ref.read(pFrostResharingData).startResharedData = result; + } + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } 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; + + resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; + myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); + if (myResharerIndexIndex >= 0) { + // remove my name for now as we don't need a text field for it + resharerIndexes.removeAt(myResharerIndexIndex); + } + + amOutgoingParticipant = !ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!); + + for (int i = 0; i < resharerIndexes.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: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myResharerStart, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My resharer", + detail: myResharerStart, + button: Util.isDesktop + ? IconCopyButton( + data: myResharerStart, + ) + : SimpleCopyButton( + data: myResharerStart, + ), + ), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharerIndexes.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostResharerTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter index " + "${resharerIndexes[i]}" + "'s resharer", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Resharer Field Input.", + key: Key( + "frostResharerClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Resharer Field Input.", + key: Key( + "frostResharerPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: Key( + "frostCommitmentsScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Continue", + enabled: amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart new file mode 100644 index 000000000..88c112095 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart @@ -0,0 +1,265 @@ +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:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/primary_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/stack_dialog.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class FrostReshareStep2c extends ConsumerStatefulWidget { + const FrostReshareStep2c({super.key}); + + static const String routeName = "/FrostReshareStep2c"; + static const String title = "Resharers"; + + @override + ConsumerState createState() => _FrostReshareStep2cState(); +} + +class _FrostReshareStep2cState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List resharerIndexes; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // collect resharer strings + final resharerStarts = controllers.map((e) => e.text).toList(); + + final result = Frost.beginReshared( + myName: ref.read(pFrostResharingData).myName!, + resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + 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, + ); + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; + + for (int i = 0; i < resharerIndexes.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 < resharerIndexes.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostResharerTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter index " + "${resharerIndexes[i]}" + "'s resharer", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Resharer Field Input.", + key: Key( + "frostResharerClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Resharer Field Input.", + key: Key( + "frostResharerPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: Key( + "frostCommitmentsScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart new file mode 100644 index 000000000..cf3f9344b --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -0,0 +1,331 @@ +import 'dart:ffi'; + +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/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/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 FrostReshareStep3abd extends ConsumerStatefulWidget { + const FrostReshareStep3abd({super.key}); + + static const String routeName = "/frostReshareStep3abd"; + static const String title = "Encryption keys"; + + @override + ConsumerState createState() => + _FrostReshareStep3abdState(); +} + +class _FrostReshareStep3abdState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List newParticipants; + late final int myIndex; + late final String? myEncryptionKey; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // collect encryptionKeys strings and insert my own at the correct index + final encryptionKeys = controllers.map((e) => e.text).toList(); + if (!amOutgoingParticipant) { + encryptionKeys.insert(myIndex, myEncryptionKey!); + } + + final result = Frost.finishResharer( + machine: ref.read(pFrostResharingData).startResharerData!.machine.ref, + encryptionKeysOfResharedTo: encryptionKeys, + ); + + ref.read(pFrostResharingData).resharerComplete = result; + + ref.read(pFrostCreateCurrentStep.state).state = 4; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } 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) + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myEncryptionKey!, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + if (!amOutgoingParticipant) + const SizedBox( + height: 12, + ), + if (!amOutgoingParticipant) + DetailItem( + title: "My encryption key", + detail: myEncryptionKey!, + button: Util.isDesktop + ? IconCopyButton( + data: myEncryptionKey!, + ) + : SimpleCopyButton( + data: myEncryptionKey!, + ), + ), + if (!amOutgoingParticipant) + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < newParticipants.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostEncryptionKeyTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter " + "${newParticipants[i]}" + "'s encryption key", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Encryption Key Field Input.", + key: Key( + "frostEncryptionKeyClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", + key: Key( + "frostEncryptionKeyPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: Key( + "frostCommitmentsScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Continue", + enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart new file mode 100644 index 000000000..3c92fe11f --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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/themes/stack_colors.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'; + +class FrostReshareStep3c extends ConsumerStatefulWidget { + const FrostReshareStep3c({super.key}); + + static const String routeName = "/frostReshareStep3c"; + static const String title = "Encryption keys"; + + @override + ConsumerState createState() => _FrostReshareStep3bState(); +} + +class _FrostReshareStep3bState extends ConsumerState { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref + .watch(pFrostResharingData) + .startResharedData! + .resharedStart, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + 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, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + onPressed: () { + ref.read(pFrostCreateCurrentStep.state).state = 4; + Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart new file mode 100644 index 000000000..52789ebe2 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -0,0 +1,369 @@ +import 'dart:ffi'; + +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:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/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'; + +// was FinishResharingView +class FrostReshareStep4 extends ConsumerStatefulWidget { + const FrostReshareStep4({super.key}); + + static const String routeName = "/frostReshareStep4"; + static const String title = "Resharer completes"; + + @override + ConsumerState createState() => _FrostReshareStep4State(); +} + +class _FrostReshareStep4State extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List resharerIndexes; + late final String myName; + late final int? myResharerIndexIndex; + late final String? myResharerComplete; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + if (amOutgoingParticipant) { + ref.read(pFrostResharingData).reset(); + Navigator.of(context).popUntil( + ModalRoute.withName( + Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName, + ), + ); + } else { + // collect resharer completes strings and insert my own at the correct index + final resharerCompletes = controllers.map((e) => e.text).toList(); + if (myResharerIndexIndex != null && myResharerComplete != null) { + resharerCompletes.insert(myResharerIndexIndex!, myResharerComplete!); + } + + final data = Frost.finishReshared( + prior: ref.read(pFrostResharingData).startResharedData!.prior.ref, + resharerCompletes: resharerCompletes, + ); + + ref.read(pFrostResharingData).newWalletData = data; + + ref.read(pFrostCreateCurrentStep.state).state = 5; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + final 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!; + + resharerIndexes = 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 = resharerIndexes.indexOf(myOldIndex); + if (myResharerIndexIndex! >= 0) { + // remove my name for now as we don't need a text field for it + resharerIndexes.removeAt(myResharerIndexIndex!); + } + + amOutgoingParticipant = !ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!); + } + + for (int i = 0; i < resharerIndexes.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) + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myResharerComplete!, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + if (myResharerComplete != null) + const SizedBox( + height: 16, + ), + if (myResharerComplete != null) + DetailItem( + title: "My resharer complete", + detail: myResharerComplete!, + button: Util.isDesktop + ? IconCopyButton( + data: myResharerComplete!, + ) + : SimpleCopyButton( + data: myResharerComplete!, + ), + ), + if (!amOutgoingParticipant) + const SizedBox( + height: 16, + ), + if (!amOutgoingParticipant) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharerIndexes.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostEncryptionKeyTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter index " + "${resharerIndexes[i]}" + "'s resharer complete", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Encryption Key Field Input.", + key: Key( + "frostEncryptionKeyClearButtonKey_$i"), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", + key: Key( + "frostEncryptionKeyPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i] + .text + .isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: Key("frostScanQrButtonKey_$i"), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context) + .unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: amOutgoingParticipant ? "Exit" : "Complete", + enabled: amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart new file mode 100644 index 000000000..1e42a0f3e --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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/stack_dialog.dart'; + +// was VerifyUpdatedWalletView +class FrostReshareStep5 extends ConsumerStatefulWidget { + const FrostReshareStep5({super.key}); + + static const String routeName = "/frostReshareStep5"; + static const String title = "Verify"; + + @override + ConsumerState createState() => + _FrostReshareStep5State(); +} + +class _FrostReshareStep5State extends ConsumerState { + late final String config; + late final String serializedKeys; + late final String reshareId; + + late final bool isNew; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + Exception? ex; + + final BitcoinFrostWallet wallet; + + if (isNew) { + wallet = await ref + .read(pFrostResharingData) + .incompleteWallet! + .toBitcoinFrostWallet( + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + ); + + await wallet.info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + ref.read(pWallets).addWallet(wallet); + } else { + wallet = ref + .read(pWallets) + .getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; + } + + if (mounted) { + await showLoading( + whileFuture: wallet.updateWithResharedData( + serializedKeys: serializedKeys, + multisigConfig: config, + isNewWallet: isNew, + ), + context: context, + message: isNew ? "Creating wallet" : "Updating wallet data", + isDesktop: Util.isDesktop, + onException: (e) => ex = e, + ); + + if (ex != null) { + throw ex!; + } + + if (mounted) { + ref.read(pFrostResharingData).reset(); + + Navigator.of(context).popUntil( + ModalRoute.withName( + _popUntilPath, + ), + ); + } + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + String get _popUntilPath => isNew + ? Util.isDesktop + ? DesktopHomeView.routeName + : HomeView.routeName + : Util.isDesktop + ? DesktopWalletView.routeName + : WalletView.routeName; + + @override + void initState() { + config = ref.read(pFrostResharingData).newWalletData!.multisigConfig; + serializedKeys = + ref.read(pFrostResharingData).newWalletData!.serializedKeys; + reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId; + + isNew = ref.read(pFrostResharingData).incompleteWallet != null && + ref.read(pFrostResharingData).incompleteWallet!.walletId == + ref.read(pFrostScaffoldArgs)!.walletId!; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + "Ensure your reshare ID matches that of each other participant", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "ID", + detail: reshareId, + button: Util.isDesktop + ? IconCopyButton( + data: reshareId, + ) + : SimpleCopyButton( + data: reshareId, + ), + ), + const SizedBox( + height: 12, + ), + const SizedBox( + height: 12, + ), + Text( + "Back up your keys and config", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Config", + detail: config, + button: Util.isDesktop + ? IconCopyButton( + data: config, + ) + : SimpleCopyButton( + data: config, + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Keys", + detail: serializedKeys, + button: Util.isDesktop + ? IconCopyButton( + data: serializedKeys, + ) + : SimpleCopyButton( + data: serializedKeys, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Confirm", + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index 0a26cda75..6db2899dd 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -10,18 +10,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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/resharing/involved/step_1a/initiate_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart'; import 'package:stackwallet/pages_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'; @@ -146,9 +149,26 @@ class FrostMSWalletOptionsView extends ConsumerWidget { 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: null, // no wallet id yet + stepRoutes: FrostRouteGenerator.joinReshareStepRoutes, + onSuccess: () { + // successful completion of steps + // TODO + + ref.read(pFrostScaffoldArgs.state).state = null; + } + ); + Navigator.of(context).pushNamed( - ImportReshareConfigView.routeName, - arguments: walletId, + FrostStepScaffold.routeName, ); }, ), diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart similarity index 94% rename from lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart rename to lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart index caa77af5f..ffb1fab0b 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart @@ -4,10 +4,12 @@ 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/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/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'; @@ -15,6 +17,7 @@ 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'; @@ -120,10 +123,25 @@ class _CompleteReshareConfigViewState ref.read(pFrostResharingData).myName = myName; ref.read(pFrostResharingData).resharerConfig = config; + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; + + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: wallet.walletId, + stepRoutes: FrostRouteGenerator.initiateReshareStepRoutes, + onSuccess: () { + // successful completion of steps + // TODO + } + ); + if (mounted) { await Navigator.of(context).pushNamed( - DisplayReshareConfigView.routeName, - arguments: widget.walletId, + FrostStepScaffold.routeName, ); } } catch (e, s) { diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart similarity index 99% rename from lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart rename to lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart index 32818982c..87345370f 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.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'; diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart deleted file mode 100644 index a0ba76770..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart +++ /dev/null @@ -1,437 +0,0 @@ -import 'dart:ffi'; - -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:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_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/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/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/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/custom_buttons/simple_copy_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/detail_item.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 FinishResharingView extends ConsumerStatefulWidget { - const FinishResharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/finishResharingView"; - - final String walletId; - - @override - ConsumerState createState() => - _FinishResharingViewState(); -} - -class _FinishResharingViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List resharerIndexes; - late final String myName; - late final int? myResharerIndexIndex; - late final String? myResharerComplete; - late final bool amOutgoingParticipant; - - final List fieldIsEmptyFlags = []; - - bool _buttonLock = false; - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - if (amOutgoingParticipant) { - ref.read(pFrostResharingData).reset(); - Navigator.of(context).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; - - await Navigator.of(context).pushNamed( - VerifyUpdatedWalletView.routeName, - arguments: widget.walletId, - ); - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - final amNewParticipant = - ref.read(pFrostResharingData).startResharerData == null && - ref.read(pFrostResharingData).incompleteWallet != null && - ref.read(pFrostResharingData).incompleteWallet?.walletId == - widget.walletId; - - myName = ref.read(pFrostResharingData).myName!; - - resharerIndexes = 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(widget.walletId)!; - final myOldIndex = - frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); - - myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); - if (myResharerIndexIndex! >= 0) { - // remove my name for now as we don't need a text field for it - resharerIndexes.removeAt(myResharerIndexIndex!); - } - - amOutgoingParticipant = !ref - .read(pFrostResharingData) - .configData! - .newParticipants - .contains(ref.read(pFrostResharingData).myName!); - } - - for (int i = 0; i < resharerIndexes.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 ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Resharer completes", - 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( - children: [ - if (myResharerComplete != null) - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myResharerComplete!, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - if (myResharerComplete != null) const _Div(), - if (myResharerComplete != null) - DetailItem( - title: "My resharer complete", - detail: myResharerComplete!, - button: Util.isDesktop - ? IconCopyButton( - data: myResharerComplete!, - ) - : SimpleCopyButton( - data: myResharerComplete!, - ), - ), - if (!amOutgoingParticipant) const _Div(), - if (!amOutgoingParticipant) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostEncryptionKeyTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer complete", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key("frostScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .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(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: amOutgoingParticipant ? "Exit" : "Complete", - enabled: amOutgoingParticipant || - !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: _onPressed, - ), - ], - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart deleted file mode 100644 index a19d0ec75..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart +++ /dev/null @@ -1,401 +0,0 @@ -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'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_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/secure_store_provider.dart'; -import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.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/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_step_user_steps.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 ImportReshareConfigView extends ConsumerStatefulWidget { - const ImportReshareConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/importReshareConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _ImportReshareConfigViewState(); -} - -class _ImportReshareConfigViewState - extends ConsumerState { - static const info = [ - "Scan the config QR code or paste the code provided by the group member who" - " is initiating resharing.", - "Wait for other participants to finish importing the config.", - "Verify that everyone has filled out their forms before continuing. If you " - "try to continue before everyone is ready, the process will be canceled.", - "Check the box and press “Start resharing”.", - ]; - - late final TextEditingController configFieldController; - late final FocusNode configFocusNode; - - bool _configEmpty = true; - - bool _buttonLock = false; - bool _userVerifyContinue = false; - - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(widget.walletId)!; - - ref.read(pFrostResharingData).reset(); - ref.read(pFrostResharingData).myName = frostInfo.myName; - ref.read(pFrostResharingData).resharerConfig = configFieldController.text; - - String? salt; - try { - salt = Format.uint8listToString( - resharerSalt( - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, - ), - ); - } 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: "{${widget.walletId}}_serializedFROSTKeys", - ); - if (mounted) { - final result = Frost.beginResharer( - serializedKeys: serializedKeys!, - config: ref.read(pFrostResharingData).resharerConfig!, - ); - - ref.read(pFrostResharingData).startResharerData = result; - - await Navigator.of(context).pushNamed( - BeginResharingView.routeName, - arguments: widget.walletId, - ); - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - configFieldController = TextEditingController(); - configFocusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - configFieldController.dispose(); - configFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: const AppBarBackButton(), - title: Text( - "Import FROST reshare config", - 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, - ), - const FrostStepUserSteps( - userSteps: info, - ), - const SizedBox(height: 20), - Text( - "Enter config", - style: STextStyles.w500_14(context).copyWith( - color: - Theme.of(context).extension()!.textSubtitle1, - ), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), - GestureDetector( - onTap: () { - setState(() { - _userVerifyContinue = !_userVerifyContinue; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has imported the config", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), - ), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Start resharing", - enabled: !_configEmpty && _userVerifyContinue, - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - } - - await _onPressed(); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart deleted file mode 100644 index ebd92cc3c..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart +++ /dev/null @@ -1,439 +0,0 @@ -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:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_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/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/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/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/custom_buttons/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.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 BeginResharingView extends ConsumerStatefulWidget { - const BeginResharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/beginResharingView"; - - final String walletId; - - @override - ConsumerState createState() => _BeginResharingViewState(); -} - -class _BeginResharingViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List resharerIndexes; - late final int myResharerIndexIndex; - late final String myResharerStart; - late final bool amOutgoingParticipant; - - final List fieldIsEmptyFlags = []; - - bool _buttonLock = false; - - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - if (!amOutgoingParticipant) { - // collect resharer strings - final resharerStarts = controllers.map((e) => e.text).toList(); - if (myResharerIndexIndex >= 0) { - // only insert my own at the correct index if I am a resharer - resharerStarts.insert(myResharerIndexIndex, myResharerStart); - } - - final result = Frost.beginReshared( - myName: ref.read(pFrostResharingData).myName!, - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, - resharerStarts: resharerStarts, - ); - - ref.read(pFrostResharingData).startResharedData = result; - } - await Navigator.of(context).pushNamed( - ContinueResharingView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } 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(widget.walletId)!; - final myOldIndex = - frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); - - myResharerStart = - ref.read(pFrostResharingData).startResharerData!.resharerStart; - - resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; - myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); - if (myResharerIndexIndex >= 0) { - // remove my name for now as we don't need a text field for it - resharerIndexes.removeAt(myResharerIndexIndex); - } - - amOutgoingParticipant = !ref - .read(pFrostResharingData) - .configData! - .newParticipants - .contains(ref.read(pFrostResharingData).myName!); - - for (int i = 0; i < resharerIndexes.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 WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: Util.isDesktop - ? DesktopWalletView.routeName - : WalletView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopWalletView.routeName, - ), - ); - }, - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: WalletView.routeName, - ), - ); - }, - ), - title: Text( - "Resharers", - 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( - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myResharerStart, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My resharer", - detail: myResharerStart, - button: Util.isDesktop - ? IconCopyButton( - data: myResharerStart, - ) - : SimpleCopyButton( - data: myResharerStart, - ), - ), - const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostResharerTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Resharer Field Input.", - key: Key( - "frostResharerClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Resharer Field Input.", - key: Key( - "frostResharerPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .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(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue", - enabled: amOutgoingParticipant || - !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: _onPressed, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart deleted file mode 100644 index b70139e69..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart +++ /dev/null @@ -1,429 +0,0 @@ -import 'dart:ffi'; - -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:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_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/my_stack_view/wallet_view/desktop_wallet_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/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.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 ContinueResharingView extends ConsumerStatefulWidget { - const ContinueResharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/continueResharingView"; - - final String walletId; - - @override - ConsumerState createState() => - _ContinueResharingViewState(); -} - -class _ContinueResharingViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List newParticipants; - late final int myIndex; - late final String? myEncryptionKey; - late final bool amOutgoingParticipant; - - final List fieldIsEmptyFlags = []; - - bool _buttonLock = false; - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - // collect encryptionKeys strings and insert my own at the correct index - final encryptionKeys = controllers.map((e) => e.text).toList(); - if (!amOutgoingParticipant) { - encryptionKeys.insert(myIndex, myEncryptionKey!); - } - - final result = Frost.finishResharer( - machine: ref.read(pFrostResharingData).startResharerData!.machine.ref, - encryptionKeysOfResharedTo: encryptionKeys, - ); - - ref.read(pFrostResharingData).resharerComplete = result; - - await Navigator.of(context).pushNamed( - FinishResharingView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } 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 WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: Util.isDesktop - ? DesktopWalletView.routeName - : WalletView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopWalletView.routeName, - ), - ); - }, - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: WalletView.routeName, - ), - ); - }, - ), - title: Text( - "Encryption keys", - 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( - children: [ - if (!amOutgoingParticipant) - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myEncryptionKey!, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - if (!amOutgoingParticipant) const _Div(), - if (!amOutgoingParticipant) - DetailItem( - title: "My encryption key", - detail: myEncryptionKey!, - button: Util.isDesktop - ? IconCopyButton( - data: myEncryptionKey!, - ) - : SimpleCopyButton( - data: myEncryptionKey!, - ), - ), - if (!amOutgoingParticipant) const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < newParticipants.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostEncryptionKeyTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter " - "${newParticipants[i]}" - "'s encryption key", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .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(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue", - enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: _onPressed, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart deleted file mode 100644 index 622dace0b..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_view.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/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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.dart'; -import 'package:stackwallet/widgets/frost_mascot.dart'; - -class NewContinueSharingView extends ConsumerStatefulWidget { - const NewContinueSharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/NewContinueSharingView"; - - final String walletId; - - @override - ConsumerState createState() => - _NewContinueSharingViewState(); -} - -class _NewContinueSharingViewState - extends ConsumerState { - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Encryption keys", - 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( - children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref - .watch(pFrostResharingData) - .startResharedData! - .resharedStart, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - 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, - ), - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue", - onPressed: () { - Navigator.of(context).pushNamed( - FinishResharingView.routeName, - arguments: widget.walletId, - ); - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart deleted file mode 100644 index ec3f00408..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart +++ /dev/null @@ -1,431 +0,0 @@ -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:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; -import 'package:stackwallet/themes/stack_colors.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/wallet_info.dart'; -import 'package:stackwallet/wallets/models/incomplete_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_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 NewImportResharerConfigView extends ConsumerStatefulWidget { - const NewImportResharerConfigView({ - super.key, - required this.walletName, - required this.frostCurrency, - }); - - static const String routeName = "/newImportResharerConfigView"; - - final String walletName; - final FrostCurrency frostCurrency; - - @override - ConsumerState createState() => - _NewImportResharerConfigViewState(); -} - -class _NewImportResharerConfigViewState - extends ConsumerState { - late final TextEditingController myNameFieldController, configFieldController; - late final FocusNode myNameFocusNode, configFocusNode; - - bool _nameEmpty = true, _configEmpty = true; - - bool _buttonLock = false; - - Future _createWallet() async { - final info = WalletInfo.createNew( - name: widget.walletName, - coin: widget.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 ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - // TODO: [prio=high] get rid of placeholder text?? - trailing: FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Import FROST reshare config", - 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: myNameFieldController, - onChanged: (_) { - setState(() { - _nameEmpty = myNameFieldController.text.isEmpty; - }); - }, - focusNode: myNameFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "My name", - myNameFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _nameEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_nameEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frMyNameClearButtonKey"), - onTap: () { - myNameFieldController.text = ""; - - setState(() { - _nameEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Name Field.", - key: const Key("frMyNamePasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - myNameFieldController.text = - data.text!.trim(); - } - - setState(() { - _nameEmpty = - myNameFieldController.text.isEmpty; - }); - }, - child: _nameEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Start", - enabled: !_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).resharerConfig = - configFieldController.text; - - if (!ref - .read(pFrostResharingData) - .configData! - .newParticipants - .contains(ref.read(pFrostResharingData).myName!)) { - ref.read(pFrostResharingData).reset(); - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "My name not found in config participants", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - Exception? ex; - final wallet = await showLoading( - whileFuture: _createWallet(), - context: context, - message: "Setting up wallet", - isDesktop: Util.isDesktop, - onException: (e) => ex = e, - ); - - if (ex != null) { - throw ex!; - } - - if (context.mounted) { - ref.read(pFrostResharingData).incompleteWallet = wallet!; - await Navigator.of(context).pushNamed( - NewStartResharingView.routeName, - arguments: wallet.walletId, - ); - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - if (context.mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } finally { - _buttonLock = false; - } - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart deleted file mode 100644 index 8bf618235..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart +++ /dev/null @@ -1,373 +0,0 @@ -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:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart'; -import 'package:stackwallet/pages_desktop_specific/desktop_home_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/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/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/frost/frost_interruption_dialog.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 NewStartResharingView extends ConsumerStatefulWidget { - const NewStartResharingView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/newStartResharingView"; - - final String walletId; - - @override - ConsumerState createState() => - _NewStartResharingViewState(); -} - -class _NewStartResharingViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final List resharerIndexes; - - final List fieldIsEmptyFlags = []; - - bool _buttonLock = false; - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - // collect resharer strings - final resharerStarts = controllers.map((e) => e.text).toList(); - - final result = Frost.beginReshared( - myName: ref.read(pFrostResharingData).myName!, - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, - resharerStarts: resharerStarts, - ); - - ref.read(pFrostResharingData).startResharedData = result; - - await Navigator.of(context).pushNamed( - NewContinueSharingView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } finally { - _buttonLock = false; - } - } - - @override - void initState() { - resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; - - for (int i = 0; i < resharerIndexes.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 WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: - Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: DesktopHomeView.routeName, - ), - ); - }, - ), - trailing: FrostMascot( - title: 'Lorem ipsum', - body: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: HomeView.routeName, - ), - ); - }, - ), - title: Text( - "Resharers", - 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( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostResharerTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Resharer Field Input.", - key: Key( - "frostResharerClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Resharer Field Input.", - key: Key( - "frostResharerPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .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(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue", - enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), - onPressed: _onPressed, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart deleted file mode 100644 index c2507a2fc..000000000 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.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/exit_to_my_stack_button.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/themes/stack_colors.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/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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; - -class VerifyUpdatedWalletView extends ConsumerStatefulWidget { - const VerifyUpdatedWalletView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/verifyUpdatedWalletView"; - - final String walletId; - - @override - ConsumerState createState() => - _VerifyUpdatedWalletViewState(); -} - -class _VerifyUpdatedWalletViewState - extends ConsumerState { - late final String config; - late final String serializedKeys; - late final String reshareId; - - late final bool isNew; - - bool _buttonLock = false; - Future _onPressed() async { - if (_buttonLock) { - return; - } - _buttonLock = true; - - try { - Exception? ex; - - final BitcoinFrostWallet wallet; - - if (isNew) { - wallet = await ref - .read(pFrostResharingData) - .incompleteWallet! - .toBitcoinFrostWallet( - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - ); - - await wallet.info.setMnemonicVerified( - isar: ref.read(mainDBProvider).isar, - ); - - ref.read(pWallets).addWallet(wallet); - } else { - wallet = - ref.read(pWallets).getWallet(widget.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", - isDesktop: Util.isDesktop, - onException: (e) => ex = e, - ); - - if (ex != null) { - throw ex!; - } - - if (mounted) { - ref.read(pFrostResharingData).reset(); - - Navigator.of(context).popUntil( - ModalRoute.withName( - _popUntilPath, - ), - ); - } - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - } 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 == - widget.walletId; - - super.initState(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: _popUntilPath, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: _popUntilPath, - ), - ); - }, - ), - trailing: ExitToMyStackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: _popUntilPath, - ), - ); - }, - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.resharing, - popUntilOnYesRouteName: _popUntilPath, - ), - ); - }, - ), - ), - 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( - children: [ - Text( - "Ensure your reshare ID matches that of each other participant", - style: STextStyles.pageTitleH2(context), - ), - const _Div(), - DetailItem( - title: "ID", - detail: reshareId, - button: Util.isDesktop - ? IconCopyButton( - data: reshareId, - ) - : SimpleCopyButton( - data: reshareId, - ), - ), - const _Div(), - const _Div(), - Text( - "Back up your keys and config", - style: STextStyles.pageTitleH2(context), - ), - const _Div(), - DetailItem( - title: "Config", - detail: config, - button: Util.isDesktop - ? IconCopyButton( - data: config, - ) - : SimpleCopyButton( - data: config, - ), - ), - const _Div(), - DetailItem( - title: "Keys", - detail: serializedKeys, - button: Util.isDesktop - ? IconCopyButton( - data: serializedKeys, - ) - : SimpleCopyButton( - data: serializedKeys, - ), - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Confirm", - onPressed: _onPressed, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 594a586ea..28402ebfb 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -123,17 +123,8 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_pr import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/initiate_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart'; -import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; @@ -499,52 +490,6 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case NewImportResharerConfigView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewImportResharerConfigView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case NewStartResharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewStartResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case NewContinueSharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewContinueSharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - case FrostStepScaffold.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, @@ -582,20 +527,6 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case ImportReshareConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ImportReshareConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - case InitiateResharingView.routeName: if (args is String) { return getRoute( @@ -625,76 +556,6 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case DisplayReshareConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DisplayReshareConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case BeginResharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BeginResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case ContinueResharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ContinueResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FinishResharingView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FinishResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case VerifyUpdatedWalletView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => VerifyUpdatedWalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - case FrostSendView.routeName: if (args is ({ String walletId, From 2e4a3d406db4b5bb000ebfb6f1b4ba3486d91d84 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 30 Apr 2024 16:01:34 -0600 Subject: [PATCH 184/272] some routing bug clean up --- .../frost_ms/new/steps/frost_route_generator.dart | 4 ++-- .../wallet_settings_view/frost_ms/frost_ms_options_view.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart index 67e96136b..e180259ea 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart @@ -169,14 +169,14 @@ abstract class FrostRouteGenerator { case FrostReshareStep3abd.routeName: return RouteGenerator.getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => const FrostReshareStep2abd(), + builder: (_) => const FrostReshareStep3abd(), settings: settings, ); case FrostReshareStep3c.routeName: return RouteGenerator.getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => const FrostReshareStep2c(), + builder: (_) => const FrostReshareStep3c(), settings: settings, ); diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index 6db2899dd..02484dc63 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -157,8 +157,8 @@ class FrostMSWalletOptionsView extends ConsumerWidget { walletName: wallet.info.name, frostCurrency: wallet.cryptoCurrency, ), - walletId: null, // no wallet id yet - stepRoutes: FrostRouteGenerator.joinReshareStepRoutes, + walletId: wallet.walletId, + stepRoutes: FrostRouteGenerator.importReshareStepRoutes, onSuccess: () { // successful completion of steps // TODO From 69b79b1ff025d89a23011f8d8a8fdc9b96c0bc87 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 30 Apr 2024 18:41:02 -0600 Subject: [PATCH 185/272] frost step text field widget --- lib/widgets/textfields/frost_step_field.dart | 181 +++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 lib/widgets/textfields/frost_step_field.dart diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart new file mode 100644 index 000000000..63b16de97 --- /dev/null +++ b/lib/widgets/textfields/frost_step_field.dart @@ -0,0 +1,181 @@ +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class FrostStepField extends StatefulWidget { + const FrostStepField({ + super.key, + required this.controller, + required this.focusNode, + this.label, + this.hint, + // this.onChanged, + required this.showQrScanOption, + }); + + final TextEditingController controller; + final FocusNode focusNode; + final String? label; + final String? hint; + // final void Function(String)? onChanged; + final bool showQrScanOption; + + @override + State createState() => _FrostStepFieldState(); +} + +class _FrostStepFieldState extends State { + final _xKey = UniqueKey(); + final _pasteKey = UniqueKey(); + late final Key? _qrKey; + + bool _isEmpty = true; + + final _inputBorder = OutlineInputBorder( + borderSide: const BorderSide( + width: 0, + color: Colors.transparent, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ); + + @override + void initState() { + _qrKey = widget.showQrScanOption ? UniqueKey() : null; + _isEmpty = widget.controller.text.isEmpty; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: widget.label != null, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + widget.label!, + style: STextStyles.w500_14(context), + ), + const SizedBox( + height: 4, + ), + child, + ], + ), + child: TextField( + controller: widget.controller, + focusNode: widget.focusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + // onChanged: widget.onChanged, + onChanged: (_) { + setState(() { + _isEmpty = widget.controller.text.isEmpty; + }); + }, + decoration: InputDecoration( + hintText: widget.hint, + fillColor: widget.focusNode.hasFocus + ? Theme.of(context).extension()!.textFieldActiveBG + : Theme.of(context).extension()!.textFieldDefaultBG, + hintStyle: Util.isDesktop + ? STextStyles.desktopTextFieldLabel(context) + : STextStyles.fieldLabel(context), + enabledBorder: _inputBorder, + focusedBorder: _inputBorder, + errorBorder: _inputBorder, + disabledBorder: _inputBorder, + focusedErrorBorder: _inputBorder, + suffixIcon: Padding( + padding: _isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_isEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Frost Step Field Input.", + key: _xKey, + onTap: () { + widget.controller.text = ""; + + setState(() { + _isEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Frost Step Field Input.", + key: _pasteKey, + onTap: () async { + final ClipboardData? data = + await Clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + widget.controller.text = data.text!.trim(); + } + + setState(() { + _isEmpty = widget.controller.text.isEmpty; + }); + }, + child: + _isEmpty ? const ClipboardIcon() : const XIcon(), + ), + if (_isEmpty && widget.showQrScanOption) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: _qrKey, + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + widget.controller.text = qrResult.rawContent; + + setState(() { + _isEmpty = widget.controller.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(), + ), + ], + ), + ), + ), + ), + ), + ); + } +} From 7455ab55f5da81d2e20c6df93483c45d24122aa3 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 09:13:47 -0600 Subject: [PATCH 186/272] use new frost step text field widget --- .../new/steps/frost_create_step_1b.dart | 223 ++---------------- .../new/steps/frost_create_step_2.dart | 152 ++---------- .../new/steps/frost_create_step_3.dart | 146 ++---------- .../reshare/frost_reshare_step_1b.dart | 140 +---------- .../reshare/frost_reshare_step_1c.dart | 220 ++--------------- .../reshare/frost_reshare_step_3abd.dart | 156 ++---------- lib/widgets/textfields/frost_step_field.dart | 35 +-- 7 files changed, 124 insertions(+), 948 deletions(-) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart index ff9c01729..f95991f3e 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart @@ -1,24 +1,14 @@ -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:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/frost_step_user_steps.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'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostCreateStep1b extends ConsumerStatefulWidget { const FrostCreateStep1b({super.key}); @@ -76,199 +66,32 @@ class _FrostCreateStep1bState extends ConsumerState { const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frMyNameTextFieldKey"), - controller: myNameFieldController, - onChanged: (_) { - setState(() { - _nameEmpty = myNameFieldController.text.isEmpty; - }); - }, - focusNode: myNameFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "My name", - myNameFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _nameEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_nameEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frMyNameClearButtonKey"), - onTap: () { - myNameFieldController.text = ""; - - setState(() { - _nameEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Name Field.", - key: const Key("frMyNamePasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - myNameFieldController.text = - data.text!.trim(); - } - - setState(() { - _nameEmpty = - myNameFieldController.text.isEmpty; - }); - }, - child: _nameEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - ], - ), - ), - ), - ), - ), + FrostStepField( + controller: myNameFieldController, + focusNode: myNameFocusNode, + showQrScanOption: false, + label: "My name", + hint: "Enter your name", + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, ), const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, ), const SizedBox( height: 16, diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart index dea6b9dcb..259059adb 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -1,6 +1,4 @@ -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:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; @@ -9,7 +7,6 @@ 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/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -19,12 +16,8 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart'; import 'package:stackwallet/widgets/frost_step_user_steps.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'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostCreateStep2 extends ConsumerStatefulWidget { const FrostCreateStep2({ @@ -135,135 +128,20 @@ class _FrostCreateStep2State extends ConsumerState { ), const SizedBox(height: 12), for (int i = 0; i < participants.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 12, - ), - Text( - participants[i], - style: STextStyles.w500_14(context), - ), - const SizedBox( - height: 4, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostCommitmentsTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter ${participants[i]}'s commitment", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Commitment Field Input.", - key: Key( - "frostCommitmentsClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Commitment Field Input.", - key: Key( - "frostCommitmentsPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: - Key("frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ), - ], - ), - ), - ), - ), - ), - ), - ], + 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), diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart index 1d6de5c39..d48107055 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -1,6 +1,4 @@ -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:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; @@ -9,9 +7,7 @@ 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/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -19,12 +15,8 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart'; import 'package:stackwallet/widgets/frost_step_user_steps.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'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostCreateStep3 extends ConsumerStatefulWidget { const FrostCreateStep3({super.key}); @@ -132,132 +124,26 @@ class _FrostCreateStep3State extends ConsumerState { ), const SizedBox(height: 12), for (int i = 0; i < participants.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 12, - ), - Text( - participants[i], - style: STextStyles.w500_14(context), - ), - const SizedBox( - height: 4, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frSharesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${participants[i]}'s share", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Share Field Input.", - key: Key("frSharesClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Share Field Input.", - key: Key("frSharesPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: Key("frSharesScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ) - ], - ), - ), - ), - ), - ), - ), - ], + 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), PrimaryButton( label: "Generate", + enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), onPressed: () async { // check for empty commitments if (controllers diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart index b9c41ec7d..998bc12f5 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart @@ -1,6 +1,4 @@ -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'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; @@ -8,8 +6,6 @@ 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/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -17,12 +13,8 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/frost_step_user_steps.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'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep1b extends ConsumerStatefulWidget { const FrostReshareStep1b({ @@ -166,125 +158,17 @@ class _FrostReshareStep1bState extends ConsumerState { userSteps: info, ), const SizedBox(height: 20), - Text( - "Enter config", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, ), const SizedBox( height: 16, diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart index 3e0e17fdf..56d28c499 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -1,12 +1,9 @@ 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:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.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'; @@ -15,12 +12,8 @@ import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/frost_step_user_steps.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'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep1c extends ConsumerStatefulWidget { const FrostReshareStep1c({super.key}); @@ -95,199 +88,32 @@ class _FrostReshareStep1cState extends ConsumerState { const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frMyNameTextFieldKey"), - controller: myNameFieldController, - onChanged: (_) { - setState(() { - _nameEmpty = myNameFieldController.text.isEmpty; - }); - }, - focusNode: myNameFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "My name", - myNameFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _nameEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_nameEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frMyNameClearButtonKey"), - onTap: () { - myNameFieldController.text = ""; - - setState(() { - _nameEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Name Field.", - key: const Key("frMyNamePasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - myNameFieldController.text = - data.text!.trim(); - } - - setState(() { - _nameEmpty = - myNameFieldController.text.isEmpty; - }); - }, - child: _nameEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - ], - ), - ), - ), - ), - ), + FrostStepField( + controller: myNameFieldController, + focusNode: myNameFocusNode, + showQrScanOption: false, + label: "My name", + hint: "Enter your name", + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, ), const SizedBox( height: 16, ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, ), const SizedBox( height: 16, diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart index cf3f9344b..ba365d08a 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -1,8 +1,6 @@ import 'dart:ffi'; -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:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; @@ -10,19 +8,13 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_deta 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/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/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/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'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep3abd extends ConsumerStatefulWidget { const FrostReshareStep3abd({super.key}); @@ -182,136 +174,22 @@ class _FrostReshareStep3abdState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ for (int i = 0; i < newParticipants.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostEncryptionKeyTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter " - "${newParticipants[i]}" - "'s encryption key", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], + 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; + }); + }, + ), ), ], ), diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart index 63b16de97..7fd19ba3b 100644 --- a/lib/widgets/textfields/frost_step_field.dart +++ b/lib/widgets/textfields/frost_step_field.dart @@ -19,7 +19,7 @@ class FrostStepField extends StatefulWidget { required this.focusNode, this.label, this.hint, - // this.onChanged, + required this.onChanged, required this.showQrScanOption, }); @@ -27,7 +27,7 @@ class FrostStepField extends StatefulWidget { final FocusNode focusNode; final String? label; final String? hint; - // final void Function(String)? onChanged; + final void Function(String) onChanged; final bool showQrScanOption; @override @@ -51,10 +51,22 @@ class _FrostStepFieldState extends State { ), ); + late final void Function(String) _changed; + @override void initState() { _qrKey = widget.showQrScanOption ? UniqueKey() : null; _isEmpty = widget.controller.text.isEmpty; + + _changed = (value) { + if (context.mounted) { + widget.onChanged.call(value); + setState(() { + _isEmpty = widget.controller.text.isEmpty; + }); + } + }; + super.initState(); } @@ -82,12 +94,7 @@ class _FrostStepFieldState extends State { autocorrect: false, enableSuggestions: false, style: STextStyles.field(context), - // onChanged: widget.onChanged, - onChanged: (_) { - setState(() { - _isEmpty = widget.controller.text.isEmpty; - }); - }, + onChanged: _changed, decoration: InputDecoration( hintText: widget.hint, fillColor: widget.focusNode.hasFocus @@ -117,9 +124,7 @@ class _FrostStepFieldState extends State { onTap: () { widget.controller.text = ""; - setState(() { - _isEmpty = true; - }); + _changed(widget.controller.text); }, child: const XIcon(), ) @@ -134,9 +139,7 @@ class _FrostStepFieldState extends State { widget.controller.text = data.text!.trim(); } - setState(() { - _isEmpty = widget.controller.text.isEmpty; - }); + _changed(widget.controller.text); }, child: _isEmpty ? const ClipboardIcon() : const XIcon(), @@ -158,9 +161,7 @@ class _FrostStepFieldState extends State { widget.controller.text = qrResult.rawContent; - setState(() { - _isEmpty = widget.controller.text.isEmpty; - }); + _changed(widget.controller.text); } on PlatformException catch (e, s) { Logging.instance.log( "Failed to get camera permissions while trying to scan qr code: $e\n$s", From 76844a562b12c8ca7e8a7928fb42922411be33ed Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 10:44:06 -0600 Subject: [PATCH 187/272] encode resharer names in resharer config --- .../reshare/frost_reshare_step_1a.dart | 13 +- .../reshare/frost_reshare_step_1b.dart | 11 +- .../reshare/frost_reshare_step_1c.dart | 2 +- .../reshare/frost_reshare_step_2abd.dart | 169 +++-------------- .../reshare/frost_reshare_step_2c.dart | 168 +++-------------- .../reshare/frost_reshare_step_4.dart | 170 +++--------------- .../complete_reshare_config_view.dart | 9 +- .../initiate_resharing_view.dart | 5 +- .../frost_wallet/frost_wallet_providers.dart | 11 +- lib/route_generator.dart | 2 +- lib/services/frost.dart | 43 ++++- 11 files changed, 143 insertions(+), 460 deletions(-) diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart index d99851e03..9f819b760 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart @@ -63,7 +63,9 @@ class _FrostReshareStep1aState extends ConsumerState { if (mounted) { final result = Frost.beginResharer( serializedKeys: serializedKeys!, - config: ref.read(pFrostResharingData).resharerConfig!, + config: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), ); ref.read(pFrostResharingData).startResharerData = result; @@ -216,6 +218,7 @@ class _FrostReshareStep1aState extends ConsumerState { .read(pFrostResharingData) .configData! .resharers + .values .contains(myOldIndex); super.initState(); } @@ -236,7 +239,7 @@ class _FrostReshareStep1aState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ QrImageView( - data: ref.watch(pFrostResharingData).resharerConfig!, + data: ref.watch(pFrostResharingData).resharerRConfig!, size: 220, backgroundColor: Theme.of(context).extension()!.background, @@ -252,13 +255,13 @@ class _FrostReshareStep1aState extends ConsumerState { ), DetailItem( title: "Config", - detail: ref.watch(pFrostResharingData).resharerConfig!, + detail: ref.watch(pFrostResharingData).resharerRConfig!, button: Util.isDesktop ? IconCopyButton( - data: ref.watch(pFrostResharingData).resharerConfig!, + data: ref.watch(pFrostResharingData).resharerRConfig!, ) : SimpleCopyButton( - data: ref.watch(pFrostResharingData).resharerConfig!, + data: ref.watch(pFrostResharingData).resharerRConfig!, ), ), SizedBox( diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart index 998bc12f5..81815fff8 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart @@ -63,13 +63,16 @@ class _FrostReshareStep1bState extends ConsumerState { ref.read(pFrostResharingData).reset(); ref.read(pFrostResharingData).myName = frostInfo.myName; - ref.read(pFrostResharingData).resharerConfig = configFieldController.text; + ref.read(pFrostResharingData).resharerRConfig = + configFieldController.text; String? salt; try { salt = Format.uint8listToString( resharerSalt( - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), ), ); } catch (_) { @@ -97,7 +100,9 @@ class _FrostReshareStep1bState extends ConsumerState { if (mounted) { final result = Frost.beginResharer( serializedKeys: serializedKeys!, - config: ref.read(pFrostResharingData).resharerConfig!, + config: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), ); ref.read(pFrostResharingData).startResharerData = result; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart index 56d28c499..b46cd3587 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -176,7 +176,7 @@ class _FrostReshareStep1cState extends ConsumerState { ref.read(pFrostResharingData).reset(); ref.read(pFrostResharingData).myName = myNameFieldController.text; - ref.read(pFrostResharingData).resharerConfig = + ref.read(pFrostResharingData).resharerRConfig = configFieldController.text; if (!ref diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart index fdbb2f4cf..bd2b5c8ce 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart @@ -1,8 +1,6 @@ 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:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; @@ -11,20 +9,14 @@ 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/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/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'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep2abd extends ConsumerStatefulWidget { const FrostReshareStep2abd({super.key}); @@ -41,7 +33,7 @@ class _FrostReshareStep2abdState extends ConsumerState { final List controllers = []; final List focusNodes = []; - late final List resharerIndexes; + late final Map resharers; late final int myResharerIndexIndex; late final String myResharerStart; late final bool amOutgoingParticipant; @@ -67,7 +59,9 @@ class _FrostReshareStep2abdState extends ConsumerState { final result = Frost.beginReshared( myName: ref.read(pFrostResharingData).myName!, - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), resharerStarts: resharerStarts, ); @@ -116,11 +110,11 @@ class _FrostReshareStep2abdState extends ConsumerState { myResharerStart = ref.read(pFrostResharingData).startResharerData!.resharerStart; - resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; - myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); + 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 - resharerIndexes.removeAt(myResharerIndexIndex); + resharers.remove(ref.read(pFrostResharingData).myName!); } amOutgoingParticipant = !ref @@ -129,7 +123,7 @@ class _FrostReshareStep2abdState extends ConsumerState { .newParticipants .contains(ref.read(pFrostResharingData).myName!); - for (int i = 0; i < resharerIndexes.length; i++) { + for (int i = 0; i < resharers.length; i++) { controllers.add(TextEditingController()); focusNodes.add(FocusNode()); fieldIsEmptyFlags.add(true); @@ -192,137 +186,20 @@ class _FrostReshareStep2abdState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostResharerTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Resharer Field Input.", - key: Key( - "frostResharerClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Resharer Field Input.", - key: Key( - "frostResharerPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], + 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; + }); + }, ), ], ), diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart index 88c112095..6ba4dd5ca 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart @@ -1,23 +1,15 @@ 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:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/services/frost.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/desktop/primary_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/stack_dialog.dart'; -import 'package:stackwallet/widgets/stack_text_field.dart'; -import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep2c extends ConsumerStatefulWidget { const FrostReshareStep2c({super.key}); @@ -33,7 +25,7 @@ class _FrostReshareStep2cState extends ConsumerState { final List controllers = []; final List focusNodes = []; - late final List resharerIndexes; + late final Map resharers; final List fieldIsEmptyFlags = []; @@ -50,7 +42,9 @@ class _FrostReshareStep2cState extends ConsumerState { final result = Frost.beginReshared( myName: ref.read(pFrostResharingData).myName!, - resharerConfig: ref.read(pFrostResharingData).resharerConfig!, + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), resharerStarts: resharerStarts, ); @@ -84,9 +78,9 @@ class _FrostReshareStep2cState extends ConsumerState { @override void initState() { - resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; + resharers = ref.read(pFrostResharingData).configData!.resharers; - for (int i = 0; i < resharerIndexes.length; i++) { + for (int i = 0; i < resharers.length; i++) { controllers.add(TextEditingController()); focusNodes.add(FocusNode()); fieldIsEmptyFlags.add(true); @@ -115,137 +109,23 @@ class _FrostReshareStep2cState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostResharerTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Resharer Field Input.", - key: Key( - "frostResharerClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Resharer Field Input.", - key: Key( - "frostResharerPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key( - "frostCommitmentsScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], + 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; + }); + }, + ), ), ], ), diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart index 52789ebe2..8a6c3e302 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -1,8 +1,6 @@ import 'dart:ffi'; -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:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; @@ -13,20 +11,14 @@ 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/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/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'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; // was FinishResharingView class FrostReshareStep4 extends ConsumerStatefulWidget { @@ -43,7 +35,7 @@ class _FrostReshareStep4State extends ConsumerState { final List controllers = []; final List focusNodes = []; - late final List resharerIndexes; + late final Map resharers; late final String myName; late final int? myResharerIndexIndex; late final String? myResharerComplete; @@ -118,7 +110,7 @@ class _FrostReshareStep4State extends ConsumerState { myName = ref.read(pFrostResharingData).myName!; - resharerIndexes = ref.read(pFrostResharingData).configData!.resharers; + resharers = ref.read(pFrostResharingData).configData!.resharers; if (amNewParticipant) { myResharerComplete = null; @@ -135,10 +127,10 @@ class _FrostReshareStep4State extends ConsumerState { final myOldIndex = frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); - myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex); + myResharerIndexIndex = resharers.values.toList().indexOf(myOldIndex); if (myResharerIndexIndex! >= 0) { // remove my name for now as we don't need a text field for it - resharerIndexes.removeAt(myResharerIndexIndex!); + resharers.remove(ref.read(pFrostResharingData).myName!); } amOutgoingParticipant = !ref @@ -148,7 +140,7 @@ class _FrostReshareStep4State extends ConsumerState { .contains(ref.read(pFrostResharingData).myName!); } - for (int i = 0; i < resharerIndexes.length; i++) { + for (int i = 0; i < resharers.length; i++) { controllers.add(TextEditingController()); focusNodes.add(FocusNode()); fieldIsEmptyFlags.add(true); @@ -216,139 +208,23 @@ class _FrostReshareStep4State extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (int i = 0; i < resharerIndexes.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostEncryptionKeyTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter index " - "${resharerIndexes[i]}" - "'s resharer complete", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyClearButtonKey_$i"), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Encryption Key Field Input.", - key: Key( - "frostEncryptionKeyPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: "Scan QR Button. " - "Opens Camera For Scanning QR Code.", - key: Key("frostScanQrButtonKey_$i"), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ], + 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; + }); + }, + ), ), ], ), diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart index ffb1fab0b..03cffcb51 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart @@ -37,7 +37,7 @@ final class CompleteReshareConfigView extends ConsumerStatefulWidget { static const String routeName = "/completeReshareConfigView"; final String walletId; - final List resharers; + final Map resharers; @override ConsumerState createState() => @@ -91,7 +91,7 @@ class _CompleteReshareConfigViewState final config = Frost.createResharerConfig( newThreshold: int.parse(_newThresholdController.text), - resharers: widget.resharers, + resharers: widget.resharers.values.toList(), newParticipants: newParticipants, ); @@ -121,7 +121,10 @@ class _CompleteReshareConfigViewState } ref.read(pFrostResharingData).myName = myName; - ref.read(pFrostResharingData).resharerConfig = config; + ref.read(pFrostResharingData).resharerRConfig = Frost.encodeRConfig( + config, + widget.resharers, + ); final wallet = ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart index 87345370f..ca5ab67e7 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart @@ -234,10 +234,11 @@ class _BeginReshareConfigViewState onPressed: () async { // include self now selectedParticipants.add(myName); - final List resharers = []; + + final Map resharers = {}; for (final name in selectedParticipants) { - resharers.add(originalParticipants.indexOf(name)); + resharers[name] = originalParticipants.indexOf(name); } await Navigator.of(context).pushNamed( diff --git a/lib/providers/frost_wallet/frost_wallet_providers.dart b/lib/providers/frost_wallet/frost_wallet_providers.dart index 3b181b7b8..7b3ee3eda 100644 --- a/lib/providers/frost_wallet/frost_wallet_providers.dart +++ b/lib/providers/frost_wallet/frost_wallet_providers.dart @@ -60,13 +60,14 @@ class _ResharingData { IncompleteFrostWallet? incompleteWallet; // resharer encoded config string - String? resharerConfig; + String? resharerRConfig; + ({ int newThreshold, - List resharers, + Map resharers, List newParticipants, - })? get configData => resharerConfig != null - ? Frost.extractResharerConfigData(resharerConfig: resharerConfig!) + })? get configData => resharerRConfig != null + ? Frost.extractResharerConfigData(rConfig: resharerRConfig!) : null; // resharer start string (for sharing) and machine @@ -93,7 +94,7 @@ class _ResharingData { // reset/clear all data void reset() { - resharerConfig = null; + resharerRConfig = null; startResharerData = null; startResharedData = null; resharerComplete = null; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 28402ebfb..d60c60de3 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -542,7 +542,7 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case CompleteReshareConfigView.routeName: - if (args is ({String walletId, List resharers})) { + if (args is ({String walletId, Map resharers})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => CompleteReshareConfigView( diff --git a/lib/services/frost.dart b/lib/services/frost.dart index a420a5b16..adf88695a 100644 --- a/lib/services/frost.dart +++ b/lib/services/frost.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:ffi'; import 'dart:typed_data'; @@ -551,11 +552,14 @@ abstract class Frost { static ({ int newThreshold, - List resharers, + Map resharers, List newParticipants, }) extractResharerConfigData({ - required String resharerConfig, + required String rConfig, }) { + final decoded = _decodeRConfigWithResharers(rConfig); + final resharerConfig = decoded.config; + try { final newThreshold = resharerNewThreshold( resharerConfigPointer: decodedResharerConfig( @@ -597,9 +601,17 @@ abstract class Frost { ); } + final Map resharersMap = {}; + + for (final resharer in resharers) { + resharersMap[decoded.resharers.entries + .firstWhere((e) => e.value == resharer) + .key] = resharer; + } + return ( newThreshold: newThreshold, - resharers: resharers, + resharers: resharersMap, newParticipants: newParticipants, ); } catch (e, s) { @@ -610,4 +622,29 @@ abstract class Frost { rethrow; } } + + static String encodeRConfig( + String config, + Map resharers, + ) { + return base64Encode("$config@${jsonEncode(resharers)}".toUint8ListFromUtf8); + } + + static String decodeRConfig( + String rConfig, + ) { + return base64Decode(rConfig).toUtf8String.split("@").first; + } + + static ({Map resharers, String config}) + _decodeRConfigWithResharers( + String rConfig, + ) { + final parts = base64Decode(rConfig).toUtf8String.split("@"); + + final config = parts[0]; + final resharers = Map.from(jsonDecode(parts[1]) as Map); + + return (resharers: resharers, config: config); + } } From 92d07330058b4d8f386fe3fec38bfb16224ca360 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 13:08:59 -0600 Subject: [PATCH 188/272] re route frost sign tx on mobile --- .../send_view/frost_ms/frost_send_view.dart | 85 ++----------------- lib/pages/wallet_view/wallet_view.dart | 10 +-- 2 files changed, 13 insertions(+), 82 deletions(-) diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 5f04bb346..93488a43f 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -17,7 +17,6 @@ import 'package:flutter_svg/flutter_svg.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/frost_create_sign_config_view.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_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'; @@ -25,7 +24,6 @@ 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/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/show_loading.dart'; @@ -49,10 +47,10 @@ import 'package:tuple/tuple.dart'; class FrostSendView extends ConsumerStatefulWidget { const FrostSendView({ - Key? key, + super.key, required this.walletId, required this.coin, - }) : super(key: key); + }); static const String routeName = "/frostSendView"; @@ -172,11 +170,11 @@ class _FrostSendViewState extends ConsumerState { for (final i in recipientWidgetIndexes) { final state = ref.read(pRecipient(i).state).state; if (state?.amount == null || state?.address == null) { - ref.read(previewTxButtonStateProvider.notifier).state = false; + ref.read(_previewTxButtonStateProvider.notifier).state = false; return; } } - ref.read(previewTxButtonStateProvider.notifier).state = true; + ref.read(_previewTxButtonStateProvider.notifier).state = true; return; } @@ -223,7 +221,7 @@ class _FrostSendViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 50)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -232,40 +230,6 @@ class _FrostSendViewState extends ConsumerState { "Send ${coin.ticker}", style: STextStyles.navBarTitle(context), ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - semanticsLabel: "Import sign config Button.", - key: const Key("importSignConfigButtonKey"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension()!.background, - icon: SvgPicture.asset( - Assets.svg.circlePlus, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - Navigator.of(context).pushNamed( - FrostImportSignConfigView.routeName, - arguments: walletId, - ); - }, - ), - ), - ), - ], ), body: LayoutBuilder( builder: (builderContext, constraints) { @@ -349,14 +313,7 @@ class _FrostSendViewState extends ConsumerState { ) : const Spacer(), GestureDetector( - onTap: () { - // cryptoAmountController.text = ref - // .read(pAmountFormatter(coin)) - // .format( - // _cachedBalance!, - // withUnitName: false, - // ); - }, + onTap: () {}, child: Container( color: Colors.transparent, child: Column( @@ -372,24 +329,6 @@ class _FrostSendViewState extends ConsumerState { ), textAlign: TextAlign.right, ), - // Text( - // "${(manager.balance.spendable.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, - // ) ], ), ), @@ -481,12 +420,6 @@ class _FrostSendViewState extends ConsumerState { } if (mounted) { - // finally spendable = ref - // .read(walletsChangeNotifierProvider) - // .getManager(widget.walletId) - // .balance - // .spendable; - // TODO: [prio=high] make sure this coincontrol works correctly Amount? amount; @@ -585,10 +518,10 @@ class _FrostSendViewState extends ConsumerState { height: 12, ), TextButton( - onPressed: ref.watch(previewTxButtonStateProvider.state).state + onPressed: ref.watch(_previewTxButtonStateProvider.state).state ? _createSignConfig : null, - style: ref.watch(previewTxButtonStateProvider.state).state + style: ref.watch(_previewTxButtonStateProvider.state).state ? Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context) @@ -610,4 +543,4 @@ class _FrostSendViewState extends ConsumerState { } } -final previewTxButtonStateProvider = StateProvider((_) => false); +final _previewTxButtonStateProvider = StateProvider((_) => false); diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 79046c600..79a9fac7c 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -29,6 +29,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_import_sign_config_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'; @@ -358,12 +359,9 @@ class _WalletViewState extends ConsumerState { } Future _onFrostSignPressed(BuildContext context) async { - // TODO - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "TODO FROST SIGN TX", - ), + await Navigator.of(context).pushNamed( + FrostImportSignConfigView.routeName, + arguments: walletId, ); } From 48e05919d59b58479d6e37ecdf6edbe1827bc15c Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 14:30:33 -0600 Subject: [PATCH 189/272] use frost step scaffold for frost send/sign flow --- .../new/steps => }/frost_route_generator.dart | 54 +++ .../new/create_new_frost_ms_wallet_view.dart | 4 +- .../select_new_frost_import_type_view.dart | 4 +- .../new/steps/frost_create_step_1a.dart | 2 +- .../new/steps/frost_create_step_1b.dart | 2 +- .../new/steps/frost_create_step_2.dart | 2 +- .../new/steps/frost_create_step_3.dart | 2 +- .../new/steps/frost_create_step_4.dart | 2 +- .../new/steps/frost_create_step_5.dart | 2 +- .../reshare/frost_reshare_step_1a.dart | 2 +- .../reshare/frost_reshare_step_1b.dart | 2 +- .../reshare/frost_reshare_step_1c.dart | 2 +- .../reshare/frost_reshare_step_2abd.dart | 2 +- .../reshare/frost_reshare_step_2c.dart | 2 +- .../reshare/frost_reshare_step_3abd.dart | 2 +- .../reshare/frost_reshare_step_3c.dart | 2 +- .../reshare/frost_reshare_step_4.dart | 2 +- .../reshare/frost_reshare_step_5.dart | 5 +- .../frost_attempt_sign_config_view.dart | 404 ---------------- .../frost_ms/frost_complete_sign_view.dart | 206 -------- .../frost_continue_sign_config_view.dart | 445 ------------------ .../frost_create_sign_config_view.dart | 185 -------- .../frost_import_sign_config_view.dart | 332 ------------- .../send_view/frost_ms/frost_send_view.dart | 24 +- .../send_steps/frost_send_step_1a.dart | 196 ++++++++ .../send_steps/frost_send_step_1b.dart | 180 +++++++ .../send_steps/frost_send_step_2.dart | 407 ++++++++++++++++ .../send_steps/frost_send_step_3.dart | 345 ++++++++++++++ .../send_steps/frost_send_step_4.dart | 144 ++++++ .../frost_ms/frost_ms_options_view.dart | 4 +- .../complete_reshare_config_view.dart | 4 +- lib/pages/wallet_view/wallet_view.dart | 22 +- .../wallet_view/sub_widgets/my_wallet.dart | 37 +- lib/route_generator.dart | 47 +- .../frost_ms => widgets}/frost_scaffold.dart | 2 +- 35 files changed, 1422 insertions(+), 1657 deletions(-) rename lib/{pages/add_wallet_views/frost_ms/new/steps => }/frost_route_generator.dart (78%) delete mode 100644 lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart delete mode 100644 lib/pages/send_view/frost_ms/frost_complete_sign_view.dart delete mode 100644 lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart delete mode 100644 lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart delete mode 100644 lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart create mode 100644 lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart create mode 100644 lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart create mode 100644 lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart create mode 100644 lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart create mode 100644 lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart rename lib/{pages/add_wallet_views/frost_ms => widgets}/frost_scaffold.dart (98%) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart b/lib/frost_route_generator.dart similarity index 78% rename from lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart rename to lib/frost_route_generator.dart index e180259ea..c8b06f916 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart +++ b/lib/frost_route_generator.dart @@ -16,6 +16,11 @@ import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshar 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'; @@ -85,6 +90,20 @@ abstract class FrostRouteGenerator { (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), ]; + static const List sendFrostTxStepRoutes = [ + (routeName: FrostSendStep1a.routeName, title: FrostSendStep1a.title), + (routeName: FrostSendStep2.routeName, title: FrostSendStep2.title), + (routeName: FrostSendStep3.routeName, title: FrostSendStep3.title), + (routeName: FrostSendStep4.routeName, title: FrostSendStep4.title), + ]; + + static const List signFrostTxStepRoutes = [ + (routeName: FrostSendStep1b.routeName, title: FrostSendStep1b.title), + (routeName: FrostSendStep2.routeName, title: FrostSendStep2.title), + (routeName: FrostSendStep3.routeName, title: FrostSendStep3.title), + (routeName: FrostSendStep4.routeName, title: FrostSendStep4.title), + ]; + static Route generateRoute(RouteSettings settings) { final args = settings.arguments; @@ -194,6 +213,41 @@ abstract class FrostRouteGenerator { 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(""); } diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index f9f3b8caf..4a9af99f6 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -3,9 +3,8 @@ import 'dart:async'; 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/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; @@ -24,6 +23,7 @@ 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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index ddf01aacb..1abcc74cd 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -3,9 +3,8 @@ import 'dart:async'; 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/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; @@ -22,6 +21,7 @@ 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 { diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart index 8c292c574..6e0f1aab2 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart @@ -2,7 +2,7 @@ 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/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart index f95991f3e..3b34d18cc 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart index 259059adb..4bce49625 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart index d48107055..58ae379ec 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart index 93fff4f77..864e905bf 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 6b7a674c0..5210ed269 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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/db/main_db_provider.dart'; import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart index 9f819b760..7534322d3 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart @@ -2,7 +2,7 @@ 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/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart index 81815fff8..23853546f 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:frostdart/frostdart.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart index b46cd3587..9e30f4ab9 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart index bd2b5c8ce..8f4b33a09 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart index 6ba4dd5ca..0c811c635 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart index ba365d08a..9eb87a874 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -3,7 +3,7 @@ import 'dart:ffi'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart index 3c92fe11f..226caa544 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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/themes/stack_colors.dart'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart index 8a6c3e302..cb56fbfc2 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -3,7 +3,7 @@ import 'dart:ffi'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart index 1e42a0f3e..10c7a6a24 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; @@ -30,8 +30,7 @@ class FrostReshareStep5 extends ConsumerStatefulWidget { static const String title = "Verify"; @override - ConsumerState createState() => - _FrostReshareStep5State(); + ConsumerState createState() => _FrostReshareStep5State(); } class _FrostReshareStep5State extends ConsumerState { diff --git a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart deleted file mode 100644 index 149eb61d3..000000000 --- a/lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart +++ /dev/null @@ -1,404 +0,0 @@ -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:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_continue_sign_config_view.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/constants.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/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/simple_copy_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/detail_item.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 FrostAttemptSignConfigView extends ConsumerStatefulWidget { - const FrostAttemptSignConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostAttemptSignConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostAttemptSignConfigViewState(); -} - -class _FrostAttemptSignConfigViewState - extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final String myName; - late final List participantsWithoutMe; - late final String myPreprocess; - late final int myIndex; - late final int threshold; - - final List fieldIsEmptyFlags = []; - - bool hasEnoughPreprocesses() { - // 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 >= threshold; - } - - @override - void initState() { - final wallet = - ref.read(pWallets).getWallet(widget.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 ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Preprocesses", - 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: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myPreprocess, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My name", - detail: myName, - ), - const _Div(), - DetailItem( - title: "My preprocess", - detail: myPreprocess, - button: Util.isDesktop - ? IconCopyButton( - data: myPreprocess, - ) - : SimpleCopyButton( - data: myPreprocess, - ), - ), - const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < participantsWithoutMe.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostPreprocessesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter ${participantsWithoutMe[i]}'s preprocess", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Preprocess Field Input.", - key: Key( - "frostPreprocessesClearButtonKey_$i", - ), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Preprocess Field Input.", - key: Key( - "frostPreprocessesPasteButtonKey_$i", - ), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: Key( - "frostPreprocessesScanQrButtonKey_$i", - ), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Continue signing", - enabled: hasEnoughPreprocesses(), - onPressed: () async { - // collect Preprocess strings (not including my own) - final preprocesses = controllers.map((e) => e.text).toList(); - - // collect participants who are involved in this transaction - final List requiredParticipantsUnordered = []; - for (int i = 0; i < participantsWithoutMe.length; i++) { - if (preprocesses[i].isNotEmpty) { - requiredParticipantsUnordered.add(participantsWithoutMe[i]); - } - } - ref.read(pFrostSelectParticipantsUnordered.notifier).state = - requiredParticipantsUnordered; - - // insert an empty string at my index - preprocesses.insert(myIndex, ""); - - try { - ref.read(pFrostContinueSignData.notifier).state = - Frost.continueSigning( - machinePtr: - ref.read(pFrostAttemptSignData.state).state!.machinePtr, - preprocesses: preprocesses, - ); - - await Navigator.of(context).pushNamed( - FrostContinueSignView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to continue signing", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - }, - ), - ], - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart b/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart deleted file mode 100644 index 6478495c0..000000000 --- a/lib/pages/send_view/frost_ms/frost_complete_sign_view.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.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/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/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/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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; - -class FrostCompleteSignView extends ConsumerStatefulWidget { - const FrostCompleteSignView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostCompleteSignView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostCompleteSignViewState(); -} - -class _FrostCompleteSignViewState extends ConsumerState { - bool _broadcastLock = false; - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Preview transaction", - 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: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref.watch(pFrostTxData.state).state!.raw!, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "Raw transaction hex", - detail: ref.watch(pFrostTxData.state).state!.raw!, - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostTxData.state).state!.raw!, - ) - : SimpleCopyButton( - data: ref.watch(pFrostTxData.state).state!.raw!, - ), - ), - const _Div(), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Broadcast Transaction", - onPressed: () async { - if (_broadcastLock) { - return; - } - _broadcastLock = true; - - try { - Exception? ex; - final txData = await showLoading( - whileFuture: ref - .read(pWallets) - .getWallet(widget.walletId) - .confirmSend( - txData: ref.read(pFrostTxData.state).state!, - ), - context: context, - message: "Broadcasting transaction to network", - isDesktop: Util.isDesktop, - onException: (e) { - ex = e; - }, - ); - - if (ex != null) { - throw ex!; - } - - if (mounted) { - if (txData != null) { - ref.read(pFrostTxData.state).state = txData; - Navigator.of(context).popUntil( - ModalRoute.withName( - Util.isDesktop - ? MyStackView.routeName - : WalletView.routeName, - ), - ); - } - } - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Broadcast error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } finally { - _broadcastLock = false; - } - }, - ), - ], - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart deleted file mode 100644 index 8bfa512ca..000000000 --- a/lib/pages/send_view/frost_ms/frost_continue_sign_config_view.dart +++ /dev/null @@ -1,445 +0,0 @@ -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:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_complete_sign_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/my_stack_view/wallet_view/desktop_wallet_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/constants.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/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/simple_copy_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/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost/frost_interruption_dialog.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 FrostContinueSignView extends ConsumerStatefulWidget { - const FrostContinueSignView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostContinueSignView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostContinueSignViewState(); -} - -class _FrostContinueSignViewState extends ConsumerState { - final List controllers = []; - final List focusNodes = []; - - late final String myName; - late final List participantsWithoutMe; - late final List participantsAll; - late final String myShare; - late final int myIndex; - - final List fieldIsEmptyFlags = []; - - @override - void initState() { - final wallet = - ref.read(pWallets).getWallet(widget.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 WillPopScope( - onWillPop: () async { - await showDialog( - context: context, - builder: (_) => FrostInterruptionDialog( - type: FrostInterruptionDialogType.transactionCreation, - popUntilOnYesRouteName: Util.isDesktop - ? DesktopWalletView.routeName - : WalletView.routeName, - ), - ); - return false; - }, - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.transactionCreation, - popUntilOnYesRouteName: DesktopWalletView.routeName, - ), - ); - }, - ), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - await showDialog( - context: context, - builder: (_) => const FrostInterruptionDialog( - type: FrostInterruptionDialogType.transactionCreation, - popUntilOnYesRouteName: WalletView.routeName, - ), - ); - }, - ), - title: Text( - "Shares", - 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: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myShare, - size: 220, - backgroundColor: Theme.of(context) - .extension()! - .background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const _Div(), - DetailItem( - title: "My name", - detail: myName, - ), - const _Div(), - DetailItem( - title: "My shares", - detail: myShare, - button: Util.isDesktop - ? IconCopyButton( - data: myShare, - ) - : SimpleCopyButton( - data: myShare, - ), - ), - const _Div(), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (int i = 0; i < participantsWithoutMe.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostSharesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${participantsWithoutMe[i]}'s share", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears " - "The Share Field Input.", - key: Key( - "frostSharesClearButtonKey_$i", - ), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From " - "Clipboard To Share Field Input.", - key: Key( - "frostSharesPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .text - .isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera " - "For Scanning QR Code.", - key: Key( - "frostSharesScanQrButtonKey_$i", - ), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context) - .unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i] - .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(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - if (!Util.isDesktop) const Spacer(), - const _Div(), - PrimaryButton( - label: "Complete signing", - onPressed: () async { - // check for empty shares - if (controllers - .map((e) => e.text.isEmpty) - .reduce((value, element) => value |= element)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Missing Shares", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - - // collect Share strings - final sharesCollected = - controllers.map((e) => e.text).toList(); - - final List shares = []; - for (final participant in participantsAll) { - if (participantsWithoutMe.contains(participant)) { - shares.add(sharesCollected[ - participantsWithoutMe.indexOf(participant)]); - } else { - shares.add(""); - } - } - - try { - final rawTx = Frost.completeSigning( - machinePtr: ref - .read(pFrostContinueSignData.state) - .state! - .machinePtr, - shares: shares, - ); - - ref.read(pFrostTxData.state).state = - ref.read(pFrostTxData.state).state!.copyWith( - raw: rawTx, - ); - - await Navigator.of(context).pushNamed( - FrostCompleteSignView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Fatal, - ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to complete signing process", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - }, - ), - ], - ), - ), - ), - ); - } -} - -class _Div extends StatelessWidget { - const _Div({super.key}); - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 12, - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart deleted file mode 100644 index bb2c129ca..000000000 --- a/lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.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/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/simple_copy_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/detail_item.dart'; - -class FrostCreateSignConfigView extends ConsumerStatefulWidget { - const FrostCreateSignConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostCreateSignConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostCreateSignConfigViewState(); -} - -class _FrostCreateSignConfigViewState - extends ConsumerState { - bool _attemptSignLock = false; - - Future _attemptSign() async { - if (_attemptSignLock) { - return; - } - - _attemptSignLock = true; - - try { - final wallet = - ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - - final attemptSignRes = await wallet.frostAttemptSignConfig( - config: ref.read(pFrostTxData.state).state!.frostMSConfig!, - ); - - ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; - - await Navigator.of(context).pushNamed( - FrostAttemptSignConfigView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Error, - ); - } finally { - _attemptSignLock = false; - } - } - - @override - Widget build(BuildContext context) { - double qrImageSize = - Util.isDesktop ? 360 : MediaQuery.of(context).size.width - 32; - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SingleChildScrollView( - child: SizedBox( - width: 600, // Was 480, may look better but overflows the bottom. - child: child, - ), - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Sign config", - 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.symmetric(horizontal: 16), - child: child, - ), - ), - ), - ); - }, - ), - ), - ), - ), - child: Column( - children: [ - if (!Util.isDesktop) const Spacer(), - SizedBox( - height: qrImageSize, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, - size: qrImageSize, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - if (!Util.isDesktop) - const SizedBox( - height: 32, - ), - DetailItem( - title: "Encoded 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 : 16, - ), - if (!Util.isDesktop) - const Spacer( - flex: 2, - ), - PrimaryButton( - label: "Attempt sign", - onPressed: () { - _attemptSign(); - }, - ), - const SizedBox( - height: 16, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart b/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart deleted file mode 100644 index c89b6846a..000000000 --- a/lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart +++ /dev/null @@ -1,332 +0,0 @@ -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:isar/isar.dart'; -import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_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/constants.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/models/tx_data.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/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 FrostImportSignConfigView extends ConsumerStatefulWidget { - const FrostImportSignConfigView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/frostImportSignConfigView"; - - final String walletId; - - @override - ConsumerState createState() => - _FrostImportSignConfigViewState(); -} - -class _FrostImportSignConfigViewState - extends ConsumerState { - late final TextEditingController configFieldController; - late final FocusNode configFocusNode; - - bool _configEmpty = true; - - bool _attemptSignLock = false; - - Future _attemptSign() async { - if (_attemptSignLock) { - return; - } - - _attemptSignLock = true; - - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - } - - final config = configFieldController.text; - final wallet = - ref.read(pWallets).getWallet(widget.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; - - await Navigator.of(context).pushNamed( - FrostAttemptSignConfigView.routeName, - arguments: widget.walletId, - ); - } catch (e, s) { - Logging.instance.log( - "$e\n$s", - level: LogLevel.Error, - ); - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Import and attempt sign config failed", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } finally { - _attemptSignLock = false; - } - } - - @override - void initState() { - configFieldController = TextEditingController(); - configFocusNode = FocusNode(); - super.initState(); - } - - @override - void dispose() { - configFieldController.dispose(); - configFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Import FROST sign config", - 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("frConfigTextFieldKey"), - controller: configFieldController, - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, - focusNode: configFocusNode, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter config", - configFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - !_configEmpty - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Config Field.", - key: const Key("frConfigClearButtonKey"), - onTap: () { - configFieldController.text = ""; - - setState(() { - _configEmpty = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Config Field Input.", - key: const Key("frConfigPasteButtonKey"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - configFieldController.text = - data.text!.trim(); - } - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - }, - child: _configEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_configEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key("frConfigScanQrButtonKey"), - onTap: () async { - try { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - - final qrResult = await BarcodeScanner.scan(); - - configFieldController.text = - qrResult.rawContent; - - setState(() { - _configEmpty = - configFieldController.text.isEmpty; - }); - } on PlatformException catch (e, s) { - Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code: $e\n$s", - level: LogLevel.Warning, - ); - } - }, - child: const QrCodeIcon(), - ) - ], - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Start signing", - enabled: !_configEmpty, - onPressed: () { - _attemptSign(); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 93488a43f..0dde93687 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -14,9 +14,9 @@ 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/frost_create_sign_config_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'; @@ -38,6 +38,7 @@ 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/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'; @@ -120,12 +121,29 @@ class _FrostSendViewState extends ConsumerState { ); } + 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, + onSuccess: () { + // successful completion of steps + // TODO ? + + ref.read(pFrostScaffoldArgs.state).state = null; + } + ); + await Navigator.of(context).pushNamed( - FrostCreateSignConfigView.routeName, - arguments: widget.walletId, + FrostStepScaffold.routeName, ); } } catch (e) { diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart new file mode 100644 index 000000000..fd83a05a0 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart @@ -0,0 +1,196 @@ +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/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostSendStep1a extends ConsumerStatefulWidget { + const FrostSendStep1a({super.key}); + + static const String routeName = "/FrostSendStep1a"; + static const String title = "FROST transaction"; + + @override + ConsumerState createState() => _FrostSendStep1aState(); +} + +class _FrostSendStep1aState extends ConsumerState { + static const steps2to4 = [ + "Wait for them to import the transaction config.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be " + "canceled.", + "Check the box and press “Attempt sign”.", + ]; + + bool _attemptSignLock = false; + + Future _attemptSign() async { + if (_attemptSignLock) { + return; + } + + _attemptSignLock = true; + + try { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + final attemptSignRes = await wallet.frostAttemptSignConfig( + config: ref.read(pFrostTxData.state).state!.frostMSConfig!, + ); + + ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Error, + ); + } finally { + _attemptSignLock = false; + } + } + + @override + Widget build(BuildContext context) { + final double qrImageSize = + Util.isDesktop ? 360 : MediaQuery.of(context).size.width - 32; + + 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.w600_12(context), + ), + TextSpan( + text: + "You must have the threshold number of signatures (including yours) to send the transaction.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), + ), + ), + ], + ), + for (int i = 0; i < steps2to4.length; i++) + 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 : 16, + ), + SizedBox( + height: qrImageSize, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + size: qrImageSize, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + if (!Util.isDesktop) + const SizedBox( + height: 32, + ), + DetailItem( + title: "Encoded 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 : 16, + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + PrimaryButton( + label: "Attempt sign", + onPressed: () { + _attemptSign(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart new file mode 100644 index 000000000..94f367507 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart @@ -0,0 +1,180 @@ +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/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostSendStep1b extends ConsumerStatefulWidget { + const FrostSendStep1b({super.key}); + + static const String routeName = "/FrostSendStep1b"; + static const String title = "Sign FROST transaction"; + + @override + ConsumerState createState() => _FrostSendStep1bState(); +} + +class _FrostSendStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the member " + "initiating this transaction.", + "Wait for other members to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be " + "canceled.", + "Check the box and press “Start signing”.", + ]; + + late final TextEditingController configFieldController; + late final FocusNode configFocusNode; + + bool _configEmpty = true; + + bool _attemptSignLock = false; + + Future _attemptSign() async { + if (_attemptSignLock) { + return; + } + + _attemptSignLock = true; + + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final config = configFieldController.text; + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + final data = Frost.extractDataFromSignConfig( + signConfig: config, + coin: wallet.cryptoCurrency, + ); + + final utxos = await ref + .read(mainDBProvider) + .getUTXOs(wallet.walletId) + .filter() + .anyOf( + data.inputs, + (q, e) => q + .txidEqualTo(Format.uint8listToString(e.hash)) + .and() + .valueEqualTo(e.value) + .and() + .voutEqualTo(e.vout)) + .findAll(); + + // TODO add more data from 'data' and display to user ? + ref.read(pFrostTxData.notifier).state = TxData( + frostMSConfig: config, + recipients: data.recipients + .map((e) => (address: e.address, amount: e.amount, isChange: false)) + .toList(), + utxos: utxos.toSet(), + ); + + final attemptSignRes = await wallet.frostAttemptSignConfig( + config: ref.read(pFrostTxData.state).state!.frostMSConfig!, + ); + + ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Error, + ); + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Import and attempt sign config failed", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _attemptSignLock = false; + } + } + + @override + void initState() { + configFieldController = TextEditingController(); + configFocusNode = FocusNode(); + 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: 12), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Import sign config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start signing", + enabled: !_configEmpty, + onPressed: () { + _attemptSign(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart new file mode 100644 index 000000000..06394c2a6 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart @@ -0,0 +1,407 @@ +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: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/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/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/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_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'; + +class FrostSendStep2 extends ConsumerStatefulWidget { + const FrostSendStep2({super.key}); + + static const String routeName = "/FrostSendStep2"; + static const String title = "Preprocesses"; + + @override + ConsumerState createState() => _FrostSendStep2State(); +} + +class _FrostSendStep2State extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final String myName; + late final List participantsWithoutMe; + late final String myPreprocess; + late final int myIndex; + late final int threshold; + + final List fieldIsEmptyFlags = []; + + bool hasEnoughPreprocesses() { + // 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 >= threshold; + } + + @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), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "1.", + 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.w600_12(context), + ), + TextSpan( + text: "You must have the threshold number of " + "preprocesses (including yours) to send this transaction.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myPreprocess, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My name", + detail: myName, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My preprocess", + detail: myPreprocess, + button: Util.isDesktop + ? IconCopyButton( + data: myPreprocess, + ) + : SimpleCopyButton( + data: myPreprocess, + ), + ), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participantsWithoutMe.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostPreprocessesTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter ${participantsWithoutMe[i]}'s preprocess", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Preprocess Field Input.", + key: Key( + "frostPreprocessesClearButtonKey_$i", + ), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Preprocess Field Input.", + key: Key( + "frostPreprocessesPasteButtonKey_$i", + ), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: Key( + "frostPreprocessesScanQrButtonKey_$i", + ), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Continue signing", + enabled: hasEnoughPreprocesses(), + onPressed: () async { + // collect Preprocess strings (not including my own) + final preprocesses = controllers.map((e) => e.text).toList(); + + // collect participants who are involved in this transaction + final List requiredParticipantsUnordered = []; + for (int i = 0; i < participantsWithoutMe.length; i++) { + if (preprocesses[i].isNotEmpty) { + requiredParticipantsUnordered.add(participantsWithoutMe[i]); + } + } + ref.read(pFrostSelectParticipantsUnordered.notifier).state = + requiredParticipantsUnordered; + + // insert an empty string at my index + preprocesses.insert(myIndex, ""); + + try { + ref.read(pFrostContinueSignData.notifier).state = + Frost.continueSigning( + machinePtr: + ref.read(pFrostAttemptSignData.state).state!.machinePtr, + preprocesses: preprocesses, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + + // await Navigator.of(context).pushNamed( + // FrostContinueSignView.routeName, + // arguments: widget.walletId, + // ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to continue signing", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart new file mode 100644 index 000000000..25a39b13b --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -0,0 +1,345 @@ +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: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/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/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/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 FrostSendStep3 extends ConsumerStatefulWidget { + const FrostSendStep3({super.key}); + + static const String routeName = "/FrostSendStep3"; + static const String title = "Shares"; + + @override + ConsumerState createState() => _FrostSendStep3State(); +} + +class _FrostSendStep3State extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final String myName; + late final List participantsWithoutMe; + late final List participantsAll; + late final String myShare; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + @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: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: myShare, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My name", + detail: myName, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My shares", + detail: myShare, + button: Util.isDesktop + ? IconCopyButton( + data: myShare, + ) + : SimpleCopyButton( + data: myShare, + ), + ), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participantsWithoutMe.length; i++) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: Key("frostSharesTextFieldKey_$i"), + controller: controllers[i], + focusNode: focusNodes[i], + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${participantsWithoutMe[i]}'s share", + focusNodes[i], + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: fieldIsEmptyFlags[i] + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + !fieldIsEmptyFlags[i] + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears " + "The Share Field Input.", + key: Key( + "frostSharesClearButtonKey_$i", + ), + onTap: () { + controllers[i].text = ""; + + setState(() { + fieldIsEmptyFlags[i] = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From " + "Clipboard To Share Field Input.", + key: Key( + "frostSharesPasteButtonKey_$i"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + controllers[i].text = + data.text!.trim(); + } + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].text.isEmpty; + }); + }, + child: fieldIsEmptyFlags[i] + ? const ClipboardIcon() + : const XIcon(), + ), + if (fieldIsEmptyFlags[i]) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera " + "For Scanning QR Code.", + key: Key( + "frostSharesScanQrButtonKey_$i", + ), + onTap: () async { + try { + if (FocusScope.of(context) + .hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75)); + } + + final qrResult = + await BarcodeScanner.scan(); + + controllers[i].text = + qrResult.rawContent; + + setState(() { + fieldIsEmptyFlags[i] = + controllers[i].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(), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Complete signing", + onPressed: () async { + // check for empty shares + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing Shares", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect Share strings + final sharesCollected = controllers.map((e) => e.text).toList(); + + final List shares = []; + for (final participant in participantsAll) { + if (participantsWithoutMe.contains(participant)) { + shares.add(sharesCollected[ + participantsWithoutMe.indexOf(participant)]); + } else { + shares.add(""); + } + } + + try { + final rawTx = Frost.completeSigning( + machinePtr: + ref.read(pFrostContinueSignData.state).state!.machinePtr, + shares: shares, + ); + + ref.read(pFrostTxData.state).state = + ref.read(pFrostTxData.state).state!.copyWith( + raw: rawTx, + ); + + 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, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to complete signing process", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart new file mode 100644 index 000000000..315bbb9a9 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart @@ -0,0 +1,144 @@ +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/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/logger.dart'; +import 'package:stackwallet/utilities/show_loading.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/stack_dialog.dart'; + +class FrostSendStep4 extends ConsumerStatefulWidget { + const FrostSendStep4({super.key}); + + static const String routeName = "/FrostSendStep4"; + static const String title = "Preview transaction"; + + @override + ConsumerState createState() => _FrostSendStep4State(); +} + +class _FrostSendStep4State extends ConsumerState { + bool _broadcastLock = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostTxData.state).state!.raw!, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Raw transaction hex", + detail: ref.watch(pFrostTxData.state).state!.raw!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostTxData.state).state!.raw!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostTxData.state).state!.raw!, + ), + ), + const SizedBox( + height: 12, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Broadcast 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.state).state!, + ), + context: context, + message: "Broadcasting transaction to network", + isDesktop: Util.isDesktop, + onException: (e) { + ex = e; + }, + ); + + if (ex != null) { + throw ex!; + } + + if (mounted) { + if (txData != null) { + ref.read(pFrostTxData.state).state = txData; + Navigator.of(context).popUntil( + ModalRoute.withName( + Util.isDesktop + ? MyStackView.routeName + : WalletView.routeName, + ), + ); + } + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Broadcast error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } finally { + _broadcastLock = false; + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index 02484dc63..16eda73fd 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -10,8 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; @@ -30,6 +29,7 @@ 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 { diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart index 03cffcb51..6a92b76ed 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart @@ -4,8 +4,7 @@ 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/pages/add_wallet_views/frost_ms/frost_scaffold.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.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'; @@ -24,6 +23,7 @@ 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'; diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 79a9fac7c..57c5c977f 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; @@ -29,7 +30,6 @@ 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_import_sign_config_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'; @@ -79,6 +79,7 @@ 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'; @@ -359,9 +360,24 @@ class _WalletViewState extends ConsumerState { } Future _onFrostSignPressed(BuildContext context) async { + final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet; + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: walletId, + stepRoutes: FrostRouteGenerator.signFrostTxStepRoutes, + onSuccess: () { + // successful completion of steps + // TODO ? + + ref.read(pFrostScaffoldArgs.state).state = null; + } + ); + await Navigator.of(context).pushNamed( - FrostImportSignConfigView.routeName, - arguments: walletId, + FrostStepScaffold.routeName, ); } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 5b0cac5f7..22cf282af 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; @@ -21,14 +21,15 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/widgets/custom_tab_view.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class MyWallet extends ConsumerStatefulWidget { const MyWallet({ - Key? key, + super.key, required this.walletId, this.contractAddress, - }) : super(key: key); + }); final String walletId; final String? contractAddress; @@ -85,10 +86,32 @@ class _MyWalletState extends ConsumerState { width: 200, buttonHeight: ButtonHeight.l, label: "Import sign config", - onPressed: () { - Navigator.of(context).pushNamed( - FrostImportSignConfigView.routeName, - arguments: widget.walletId, + onPressed: () async { + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId) + as BitcoinFrostWallet; + ref.read(pFrostScaffoldArgs.state).state = + ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: widget.walletId, + stepRoutes: FrostRouteGenerator + .signFrostTxStepRoutes, + onSuccess: () { + // successful completion of steps + // TODO ? + + ref + .read(pFrostScaffoldArgs.state) + .state = null; + } + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, ); }, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index d60c60de3..f6dcaa6da 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -26,7 +26,6 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_scaffold.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'; @@ -80,9 +79,6 @@ import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.d import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart'; -import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/send_view/token_send_view.dart'; @@ -197,6 +193,7 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency. import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/widgets/choose_coin_view.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:tuple/tuple.dart'; /* @@ -574,48 +571,6 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case FrostImportSignConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostImportSignConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FrostCreateSignConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostCreateSignConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case FrostAttemptSignConfigView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostAttemptSignConfigView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - // case MonkeyLoadedView.routeName: // if (args is Tuple2>) { // return getRoute( diff --git a/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart b/lib/widgets/frost_scaffold.dart similarity index 98% rename from lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart rename to lib/widgets/frost_scaffold.dart index b2f2dcbe3..34ab2b922 100644 --- a/lib/pages/add_wallet_views/frost_ms/frost_scaffold.dart +++ b/lib/widgets/frost_scaffold.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_route_generator.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; From ccca53f3d890b001c01cd111014743b4e7417f68 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 14:50:12 -0600 Subject: [PATCH 190/272] frost flow interruption dialog --- lib/frost_route_generator.dart | 7 ++ .../new/create_new_frost_ms_wallet_view.dart | 4 +- .../select_new_frost_import_type_view.dart | 8 ++- .../reshare/frost_reshare_step_1c.dart | 2 + .../send_view/frost_ms/frost_send_view.dart | 4 +- .../frost_ms/frost_ms_options_view.dart | 4 +- .../complete_reshare_config_view.dart | 3 +- lib/pages/wallet_view/wallet_view.dart | 4 +- .../wallet_view/sub_widgets/my_wallet.dart | 5 +- .../frost/frost_interruption_dialog.dart | 70 ------------------- lib/widgets/frost_scaffold.dart | 51 +++++++++++++- 11 files changed, 81 insertions(+), 81 deletions(-) delete mode 100644 lib/widgets/dialogs/frost/frost_interruption_dialog.dart diff --git a/lib/frost_route_generator.dart b/lib/frost_route_generator.dart index c8b06f916..297adbf04 100644 --- a/lib/frost_route_generator.dart +++ b/lib/frost_route_generator.dart @@ -26,6 +26,12 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency. typedef FrostStepRoute = ({String routeName, String title}); +enum FrostInterruptionDialogType { + walletCreation, + resharing, + transactionCreation; +} + final pFrostCreateCurrentStep = StateProvider.autoDispose((ref) => 1); final pFrostScaffoldArgs = StateProvider< ({ @@ -33,6 +39,7 @@ final pFrostScaffoldArgs = StateProvider< String? walletId, List stepRoutes, VoidCallback onSuccess, + FrostInterruptionDialogType frostInterruptionDialogType, })?>((ref) => null); abstract class FrostRouteGenerator { diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index 4a9af99f6..c3a4ba47c 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -445,7 +445,9 @@ class _NewFrostMsWalletViewState context: context, ), ); - } + }, + frostInterruptionDialogType: + FrostInterruptionDialogType.walletCreation, ); await Navigator.of(context).pushNamed( diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index 1abcc74cd..8b0e4118f 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -177,7 +177,9 @@ class _SelectNewFrostImportTypeViewState context: context, ), ); - } + }, + frostInterruptionDialogType: + FrostInterruptionDialogType.walletCreation, ); break; @@ -191,7 +193,9 @@ class _SelectNewFrostImportTypeViewState stepRoutes: FrostRouteGenerator.joinReshareStepRoutes, onSuccess: () { // successful completion of steps - } + }, + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, ); break; } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart index 9e30f4ab9..6908cf8bd 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -215,6 +215,8 @@ class _FrostReshareStep1cState extends ConsumerState { walletId: wallet.walletId, stepRoutes: data.stepRoutes, onSuccess: data.onSuccess, + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, ); ref.read(pFrostCreateCurrentStep.state).state = 2; await Navigator.of(context).pushNamed( diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 0dde93687..e0a080a88 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -139,7 +139,9 @@ class _FrostSendViewState extends ConsumerState { // TODO ? ref.read(pFrostScaffoldArgs.state).state = null; - } + }, + frostInterruptionDialogType: + FrostInterruptionDialogType.transactionCreation, ); await Navigator.of(context).pushNamed( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index 16eda73fd..f93a069b0 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -164,7 +164,9 @@ class FrostMSWalletOptionsView extends ConsumerWidget { // TODO ref.read(pFrostScaffoldArgs.state).state = null; - } + }, + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, ); Navigator.of(context).pushNamed( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart index 6a92b76ed..1b3b9a9fd 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart @@ -139,7 +139,8 @@ class _CompleteReshareConfigViewState onSuccess: () { // successful completion of steps // TODO - } + }, + frostInterruptionDialogType: FrostInterruptionDialogType.resharing, ); if (mounted) { diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 57c5c977f..c7b30d81c 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -373,7 +373,9 @@ class _WalletViewState extends ConsumerState { // TODO ? ref.read(pFrostScaffoldArgs.state).state = null; - } + }, + frostInterruptionDialogType: + FrostInterruptionDialogType.transactionCreation, ); await Navigator.of(context).pushNamed( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 22cf282af..0b7875b34 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -107,7 +107,10 @@ class _MyWalletState extends ConsumerState { ref .read(pFrostScaffoldArgs.state) .state = null; - } + }, + frostInterruptionDialogType: + FrostInterruptionDialogType + .transactionCreation, ); await Navigator.of(context).pushNamed( diff --git a/lib/widgets/dialogs/frost/frost_interruption_dialog.dart b/lib/widgets/dialogs/frost/frost_interruption_dialog.dart deleted file mode 100644 index 4d36456bb..000000000 --- a/lib/widgets/dialogs/frost/frost_interruption_dialog.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/desktop/primary_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; - -enum FrostInterruptionDialogType { - walletCreation, - resharing, - transactionCreation; -} - -class FrostInterruptionDialog extends StatelessWidget { - const FrostInterruptionDialog({ - super.key, - required this.type, - required this.popUntilOnYesRouteName, - this.onNoPressedOverride, - this.onYesPressedOverride, - }); - - final FrostInterruptionDialogType type; - final String popUntilOnYesRouteName; - final VoidCallback? onNoPressedOverride; - final VoidCallback? onYesPressedOverride; - - String get message { - switch (type) { - case FrostInterruptionDialogType.walletCreation: - return "wallet creation"; - case FrostInterruptionDialogType.resharing: - return "resharing"; - case FrostInterruptionDialogType.transactionCreation: - return "transaction signing"; - } - } - - @override - Widget build(BuildContext context) { - return StackDialog( - title: "Cancel $message process", - message: "Are you sure you want to cancel the $message process?", - leftButton: SecondaryButton( - label: "No", - onPressed: onNoPressedOverride ?? - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop, - ), - rightButton: PrimaryButton( - label: "Yes", - onPressed: onYesPressedOverride ?? - () { - // pop dialog - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop(); - - Navigator.of(context).popUntil( - ModalRoute.withName( - popUntilOnYesRouteName, - ), - ); - }, - ), - ); - } -} diff --git a/lib/widgets/frost_scaffold.dart b/lib/widgets/frost_scaffold.dart index 34ab2b922..a5d218dc0 100644 --- a/lib/widgets/frost_scaffold.dart +++ b/lib/widgets/frost_scaffold.dart @@ -7,7 +7,10 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/progress_bar.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; class FrostStepScaffold extends ConsumerStatefulWidget { const FrostStepScaffold({super.key}); @@ -25,18 +28,60 @@ class _FrostScaffoldState extends ConsumerState { late final List _routes; bool _requestPopLock = false; + + String get _message { + switch (ref.read(pFrostScaffoldArgs)!.frostInterruptionDialogType) { + case FrostInterruptionDialogType.walletCreation: + return "wallet creation"; + case FrostInterruptionDialogType.resharing: + return "resharing"; + case FrostInterruptionDialogType.transactionCreation: + return "transaction signing"; + } + } + Future _requestPop(BuildContext context) async { if (_requestPopLock) { return; } _requestPopLock = true; - // TODO: dialog to confirm exit + final resultFuture = showDialog( + context: context, + barrierDismissible: false, + builder: (context) => StackDialog( + title: "Cancel $_message process", + message: "Are you sure you want to cancel the $_message process?", + leftButton: SecondaryButton( + label: "No", + onPressed: () { + // pop dialog + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop("no"); + }, + ), + rightButton: PrimaryButton( + label: "Yes", + onPressed: () { + // pop dialog + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop("yes"); + }, + ), + ), + ); // make sure to at least delay some time otherwise flutter pops back more than a single route lol... - await Future.delayed(const Duration(milliseconds: 200)); + final minTimeFuture = + Future.delayed(const Duration(milliseconds: 200)); - if (context.mounted) { + final result = await Future.wait([resultFuture, minTimeFuture]); + + if (context.mounted && result[0] == "yes") { Navigator.of(context).pop(); ref.read(pFrostScaffoldArgs.state).state = null; } From fc3ec6aa0a74645fd9a41492be5e56b01f482d2f Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 14:59:44 -0600 Subject: [PATCH 191/272] frost display WU instead of vByte --- lib/pages/send_view/frost_ms/frost_send_view.dart | 1 + lib/widgets/fee_slider.dart | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index e0a080a88..778085b6f 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -524,6 +524,7 @@ class _FrostSendViewState extends ConsumerState { ), child: FeeSlider( coin: coin, + showWU: true, onSatVByteChanged: (rate) { customFeeRate = rate; }, diff --git a/lib/widgets/fee_slider.dart b/lib/widgets/fee_slider.dart index d125988f3..64e3af12b 100644 --- a/lib/widgets/fee_slider.dart +++ b/lib/widgets/fee_slider.dart @@ -9,9 +9,11 @@ class FeeSlider extends StatefulWidget { super.key, required this.onSatVByteChanged, required this.coin, + this.showWU = false, }); final Coin coin; + final bool showWU; final void Function(int) onSatVByteChanged; @override @@ -34,7 +36,7 @@ class _FeeSliderState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "sat/vByte", + widget.showWU ? "sat/WU" : "sat/vByte", style: STextStyles.smallMed12(context), ), Text( From 2de96f15e0f5e08e20afb0c44917796aa036dc70 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 16:05:35 -0600 Subject: [PATCH 192/272] frost send view recipients ui update --- .../send_view/frost_ms/frost_send_view.dart | 59 +++--- lib/pages/send_view/frost_ms/recipient.dart | 189 ++++++------------ 2 files changed, 99 insertions(+), 149 deletions(-) diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 778085b6f..c275439dc 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -37,6 +37,7 @@ 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/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'; @@ -357,30 +358,8 @@ class _FrostSendViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recipients", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - CustomTextButton( - text: "Add", - onTap: () { - // used for tracking recipient forms - _greatestWidgetIndex++; - recipientWidgetIndexes.add(_greatestWidgetIndex); - setState(() {}); - }, - ), - ], - ), - const SizedBox( - height: 8, + SizedBox( + height: recipientWidgetIndexes.length > 1 ? 8 : 16, ), Column( children: [ @@ -396,6 +375,7 @@ class _FrostSendViewState extends ConsumerState { "recipientKey_${recipientWidgetIndexes[i]}", ), index: recipientWidgetIndexes[i], + displayNumber: i + 1, coin: coin, onChanged: () { _validateRecipientFormStates(); @@ -403,13 +383,44 @@ class _FrostSendViewState extends ConsumerState { remove: i == 0 && recipientWidgetIndexes.length == 1 ? null : () { + ref + .read(pRecipient(recipientWidgetIndexes[i]) + .notifier) + .state = null; recipientWidgetIndexes.removeAt(i); setState(() {}); }, + addAnotherRecipientTapped: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); + }, + 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, diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index 89121d065..36f5a597f 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -20,7 +20,7 @@ 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_white_container.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -45,16 +45,22 @@ class Recipient extends ConsumerStatefulWidget { const Recipient({ super.key, required this.index, + required this.displayNumber, required this.coin, this.remove, this.onChanged, + required this.addAnotherRecipientTapped, + required this.sendAllTapped, }); final int index; + final int displayNumber; final Coin coin; final VoidCallback? remove; final VoidCallback? onChanged; + final VoidCallback addAnotherRecipientTapped; + final String Function() sendAllTapped; @override ConsumerState createState() => _RecipientState(); @@ -65,7 +71,9 @@ class _RecipientState extends ConsumerState { late final FocusNode addressFocusNode, amountFocusNode; bool _addressIsEmpty = true; - bool _cryptoAmountChangeLock = false; + final bool _cryptoAmountChangeLock = false; + + bool get isSingle => widget.remove == null; void _updateRecipientData() { final address = addressController.text; @@ -116,6 +124,16 @@ class _RecipientState extends ConsumerState { 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(); @@ -148,12 +166,31 @@ class _RecipientState extends ConsumerState { ), ); - return RoundedWhiteContainer( + 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, @@ -323,9 +360,31 @@ class _RecipientState extends ConsumerState { ), ), ), - const SizedBox( - height: 12, + SizedBox( + height: isSingle ? 12 : 8, ), + if (isSingle) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: "Send all ${widget.coin.ticker.toUpperCase()}", + onTap: () { + amountController.text = widget.sendAllTapped(); + _cryptoAmountChanged(); + }, + ), + ], + ), + if (isSingle) + const SizedBox( + height: 8, + ), TextField( autocorrect: false, enableSuggestions: false, @@ -375,126 +434,6 @@ class _RecipientState extends ConsumerState { ), ), ), - // if (ref.watch(prefsChangeNotifierProvider - // .select((value) => value.externalCalls))) - // const SizedBox( - // height: 8, - // ), - // if (ref.watch(prefsChangeNotifierProvider - // .select((value) => value.externalCalls))) - // TextField( - // autocorrect: Util.isDesktop ? false : true, - // enableSuggestions: Util.isDesktop ? false : true, - // style: STextStyles.smallMed14(context).copyWith( - // color: Theme.of(context).extension()!.textDark, - // ), - // key: const Key("amountInputFieldFiatTextFieldKey"), - // controller: baseController, - // focusNode: baseFocusNode, - // keyboardType: Util.isDesktop - // ? null - // : const TextInputType.numberWithOptions( - // signed: false, - // decimal: true, - // ), - // textAlign: TextAlign.right, - // inputFormatters: [ - // AmountInputFormatter( - // decimals: 2, - // locale: locale, - // ), - // ], - // onChanged: (baseAmountString) { - // final baseAmount = Amount.tryParseFiatString( - // baseAmountString, - // locale: locale, - // ); - // Amount? cryptoAmount; - // final int decimals = widget.coin.decimals; - // if (baseAmount != null) { - // final _price = ref.read(_pPrice(widget.coin)); - // - // if (_price == Decimal.zero) { - // cryptoAmount = 0.toAmountAsRaw( - // fractionDigits: decimals, - // ); - // } else { - // cryptoAmount = baseAmount <= Amount.zero - // ? 0.toAmountAsRaw(fractionDigits: decimals) - // : (baseAmount.decimal / _price) - // .toDecimal( - // scaleOnInfinitePrecision: decimals, - // ) - // .toAmount(fractionDigits: decimals); - // } - // if (ref.read(pRecipient(widget.index))?.amount != null && - // ref.read(pRecipient(widget.index))?.amount == - // cryptoAmount) { - // return; - // } - // - // final amountString = - // ref.read(pAmountFormatter(widget.coin)).format( - // cryptoAmount, - // withUnitName: false, - // ); - // - // _cryptoAmountChangeLock = true; - // amountController.text = amountString; - // _cryptoAmountChangeLock = false; - // } else { - // cryptoAmount = 0.toAmountAsRaw( - // fractionDigits: decimals, - // ); - // _cryptoAmountChangeLock = true; - // amountController.text = ""; - // _cryptoAmountChangeLock = false; - // } - // - // _updateRecipientData(); - // }, - // 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(prefsChangeNotifierProvider - // .select((value) => value.currency)), - // style: STextStyles.smallMed14(context).copyWith( - // color: Theme.of(context) - // .extension()! - // .accentColorDark), - // ), - // ), - // ), - // ), - // ), - if (widget.remove != null) - const SizedBox( - height: 6, - ), - if (widget.remove != null) - Row( - children: [ - const Spacer(), - CustomTextButton( - text: "Remove", - onTap: () { - ref.read(pRecipient(widget.index).notifier).state = null; - widget.remove?.call(); - }, - ), - ], - ), ], ), ); From 7f0585d4f9042f04b0789e8cc7b6b48bd1013ba0 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 16:30:37 -0600 Subject: [PATCH 193/272] fix button enabled state bug --- .../send_view/frost_ms/frost_send_view.dart | 46 +++++++++---------- lib/pages/send_view/frost_ms/recipient.dart | 4 ++ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index c275439dc..d6cfc505c 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -37,6 +37,7 @@ 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'; @@ -187,16 +188,24 @@ class _FrostSendViewState extends ConsumerState { int customFeeRate = 1; - void _validateRecipientFormStates() { + bool _buttonEnabled = false; + + bool _validateRecipientFormStatesHelper() { for (final i in recipientWidgetIndexes) { - final state = ref.read(pRecipient(i).state).state; - if (state?.amount == null || state?.address == null) { - ref.read(_previewTxButtonStateProvider.notifier).state = false; - return; + final state = ref.read(pRecipient(i)); + if (state?.amount == null || + state?.address == null || + state!.address.isEmpty) { + return false; } } - ref.read(_previewTxButtonStateProvider.notifier).state = true; - return; + return true; + } + + void _validateRecipientFormStates() { + setState(() { + _buttonEnabled = _validateRecipientFormStatesHelper(); + }); } @override @@ -389,12 +398,14 @@ class _FrostSendViewState extends ConsumerState { .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( @@ -549,21 +560,10 @@ class _FrostSendViewState extends ConsumerState { const SizedBox( height: 12, ), - TextButton( - onPressed: ref.watch(_previewTxButtonStateProvider.state).state - ? _createSignConfig - : null, - style: ref.watch(_previewTxButtonStateProvider.state).state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Create config", - style: STextStyles.button(context), - ), + PrimaryButton( + label: "Create multisig transaction", + enabled: _buttonEnabled, + onPressed: _createSignConfig, ), const SizedBox( height: 16, @@ -574,5 +574,3 @@ class _FrostSendViewState extends ConsumerState { ); } } - -final _previewTxButtonStateProvider = StateProvider((_) => false); diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index 36f5a597f..0540dd787 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -204,6 +204,7 @@ class _RecipientState extends ConsumerState { focusNode: addressFocusNode, style: STextStyles.field(context), onChanged: (_) { + _updateRecipientData(); setState(() { _addressIsEmpty = addressController.text.isEmpty; }); @@ -394,6 +395,9 @@ class _RecipientState extends ConsumerState { key: const Key("amountInputFieldCryptoTextFieldKey"), controller: amountController, focusNode: amountFocusNode, + onChanged: (_) { + _updateRecipientData(); + }, keyboardType: Util.isDesktop ? null : const TextInputType.numberWithOptions( From b2087c1f1dcff386848af90bf9d44630ae6c187c Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 16:50:01 -0600 Subject: [PATCH 194/272] tweak frost join new wallet step 1 screen ui --- .../new/steps/frost_create_step_1b.dart | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart index 3b34d18cc..1cdacf8a4 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.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'; @@ -66,6 +67,21 @@ class _FrostCreateStep1bState extends ConsumerState { 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, @@ -79,19 +95,20 @@ class _FrostCreateStep1bState extends ConsumerState { }, ), const SizedBox( - height: 16, + height: 6, ), - FrostStepField( - controller: configFieldController, - focusNode: configFocusNode, - showQrScanOption: true, - label: "Enter config", - hint: "Enter config", - onChanged: (_) { - setState(() { - _configEmpty = configFieldController.text.isEmpty; - }); - }, + 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, From 72069bd070389ddd4869d4498ef7baafbe2a7084 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 17:15:04 -0600 Subject: [PATCH 195/272] frost checkbox widget --- .../new/steps/frost_create_step_1a.dart | 36 ++---------- .../new/steps/frost_create_step_1b.dart | 36 ++---------- .../new/steps/frost_create_step_2.dart | 37 ++---------- .../new/steps/frost_create_step_3.dart | 17 +++++- .../new/steps/frost_create_step_5.dart | 36 ++---------- .../reshare/frost_reshare_step_1a.dart | 36 ++---------- .../reshare/frost_reshare_step_1b.dart | 37 ++---------- .../reshare/frost_reshare_step_1c.dart | 37 ++---------- .../reshare/frost_reshare_step_2abd.dart | 19 ++++++- .../reshare/frost_reshare_step_3abd.dart | 17 +++++- .../reshare/frost_reshare_step_3c.dart | 19 ++++++- .../reshare/frost_reshare_step_4.dart | 19 ++++++- .../custom_buttons/checkbox_text_button.dart | 56 +++++++++++++++++++ 13 files changed, 174 insertions(+), 228 deletions(-) create mode 100644 lib/widgets/custom_buttons/checkbox_text_button.dart diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart index 6e0f1aab2..0f1cd84aa 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart @@ -10,6 +10,7 @@ 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'; @@ -209,40 +210,13 @@ class _FrostCreateStep1aState extends ConsumerState { const SizedBox( height: 16, ), - GestureDetector( - onTap: () { + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { setState(() { - _userVerifyContinue = !_userVerifyContinue; + _userVerifyContinue = value; }); }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has joined the group", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), ), const SizedBox( height: 16, diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart index 1cdacf8a4..21ed92a7d 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart @@ -5,6 +5,7 @@ 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'; @@ -117,40 +118,13 @@ class _FrostCreateStep1bState extends ConsumerState { const SizedBox( height: 16, ), - GestureDetector( - onTap: () { + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { setState(() { - _userVerifyContinue = !_userVerifyContinue; + _userVerifyContinue = value; }); }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has joined the group", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), ), const SizedBox( height: 16, diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart index 4bce49625..a99d2b6db 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -8,8 +8,8 @@ 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/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'; @@ -145,40 +145,13 @@ class _FrostCreateStep2State extends ConsumerState { ), if (!Util.isDesktop) const Spacer(), const SizedBox(height: 12), - GestureDetector( - onTap: () { + CheckboxTextButton( + label: "I have verified that everyone has my commitment", + onChanged: (value) { setState(() { - _userVerifyContinue = !_userVerifyContinue; + _userVerifyContinue = value; }); }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has all commitments", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), ), const SizedBox(height: 12), PrimaryButton( diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart index 58ae379ec..72dd81410 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.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/simple_copy_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; @@ -34,6 +35,8 @@ class _FrostCreateStep3State extends ConsumerState { "Enter their shares into the corresponding fields.", ]; + bool _userVerifyContinue = false; + final List controllers = []; final List focusNodes = []; @@ -141,9 +144,21 @@ class _FrostCreateStep3State extends ConsumerState { ), 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: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), onPressed: () async { // check for empty commitments if (controllers diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 5210ed269..88cbfc5a6 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -19,6 +19,7 @@ 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'; @@ -104,40 +105,13 @@ class _FrostCreateStep5State extends ConsumerState { ), if (!Util.isDesktop) const Spacer(), const SizedBox(height: 12), - GestureDetector( - onTap: () { + CheckboxTextButton( + label: "I have backed up my keys and the config", + onChanged: (value) { setState(() { - _userVerifyContinue = !_userVerifyContinue; + _userVerifyContinue = value; }); }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have backed up my keys and the config", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), ), const SizedBox(height: 12), PrimaryButton( diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart index 7534322d3..46f272daf 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart @@ -15,6 +15,7 @@ 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'; @@ -283,40 +284,13 @@ class _FrostReshareStep1aState extends ConsumerState { height: 16, ), if (iAmInvolved) - GestureDetector( - onTap: () { + CheckboxTextButton( + label: "I have verified that everyone has imported the config", + onChanged: (value) { setState(() { - _userVerifyContinue = !_userVerifyContinue; + _userVerifyContinue = value; }); }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has imported the config", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), ), if (iAmInvolved) const SizedBox( diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart index 23853546f..71fe0e737 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart @@ -8,9 +8,9 @@ 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/text_styles.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/frost_step_user_steps.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; @@ -182,40 +182,13 @@ class _FrostReshareStep1bState extends ConsumerState { const SizedBox( height: 16, ), - GestureDetector( - onTap: () { + CheckboxTextButton( + label: "I have verified that everyone has imported the config", + onChanged: (value) { setState(() { - _userVerifyContinue = !_userVerifyContinue; + _userVerifyContinue = value; }); }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has imported the config", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), ), const SizedBox( height: 16, diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart index 6908cf8bd..a0e39221d 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -6,10 +6,10 @@ 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/text_styles.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'; @@ -122,40 +122,13 @@ class _FrostReshareStep1cState extends ConsumerState { const SizedBox( height: 16, ), - GestureDetector( - onTap: () { + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { setState(() { - _userVerifyContinue = !_userVerifyContinue; + _userVerifyContinue = value; }); }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 26, - child: Checkbox( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: _userVerifyContinue, - onChanged: (value) => setState( - () => _userVerifyContinue = value == true, - ), - ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Text( - "I have verified that everyone has joined the group", - style: STextStyles.w500_14(context), - ), - ), - ], - ), - ), ), const SizedBox( height: 16, diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart index 8f4b33a09..e6c7a70ee 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/themes/stack_colors.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/simple_copy_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; @@ -42,6 +43,8 @@ class _FrostReshareStep2abdState extends ConsumerState { bool _buttonLock = false; + bool _userVerifyContinue = false; + Future _onPressed() async { if (_buttonLock) { return; @@ -207,10 +210,22 @@ class _FrostReshareStep2abdState extends ConsumerState { 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: amOutgoingParticipant || - !fieldIsEmptyFlags.reduce((v, e) => v |= e), + enabled: _userVerifyContinue && + (amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e)), onPressed: _onPressed, ), ], diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart index 9eb87a874..145ceb72b 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/themes/stack_colors.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/simple_copy_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; @@ -38,6 +39,8 @@ class _FrostReshareStep3abdState extends ConsumerState { final List fieldIsEmptyFlags = []; + bool _userVerifyContinue = false; + bool _buttonLock = false; Future _onPressed() async { if (_buttonLock) { @@ -197,9 +200,21 @@ class _FrostReshareStep3abdState extends ConsumerState { const SizedBox( height: 12, ), + CheckboxTextButton( + label: "I have verified that everyone has my encryption key", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), PrimaryButton( label: "Continue", - enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), onPressed: _onPressed, ), ], diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart index 226caa544..3e129604a 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_deta import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/themes/stack_colors.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/detail_item.dart'; @@ -17,10 +18,12 @@ class FrostReshareStep3c extends ConsumerStatefulWidget { static const String title = "Encryption keys"; @override - ConsumerState createState() => _FrostReshareStep3bState(); + ConsumerState createState() => _FrostReshareStep3cState(); } -class _FrostReshareStep3bState extends ConsumerState { +class _FrostReshareStep3cState extends ConsumerState { + bool _userVerifyContinue = false; + @override Widget build(BuildContext context) { return Padding( @@ -72,8 +75,20 @@ class _FrostReshareStep3bState extends ConsumerState { 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( diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart index cb56fbfc2..f60dd29f9 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/themes/stack_colors.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/simple_copy_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; @@ -43,6 +44,8 @@ class _FrostReshareStep4State extends ConsumerState { final List fieldIsEmptyFlags = []; + bool _userVerifyContinue = false; + bool _buttonLock = false; Future _onPressed() async { if (_buttonLock) { @@ -232,10 +235,22 @@ class _FrostReshareStep4State extends ConsumerState { const SizedBox( height: 16, ), + CheckboxTextButton( + label: "I have verified that everyone has my resharer complete", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), PrimaryButton( label: amOutgoingParticipant ? "Exit" : "Complete", - enabled: amOutgoingParticipant || - !fieldIsEmptyFlags.reduce((v, e) => v |= e), + enabled: _userVerifyContinue && + (amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e)), onPressed: _onPressed, ), ], diff --git a/lib/widgets/custom_buttons/checkbox_text_button.dart b/lib/widgets/custom_buttons/checkbox_text_button.dart new file mode 100644 index 000000000..8ad254ae9 --- /dev/null +++ b/lib/widgets/custom_buttons/checkbox_text_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; + +class CheckboxTextButton extends StatefulWidget { + const CheckboxTextButton({super.key, required this.label, this.onChanged}); + + final String label; + final void Function(bool)? onChanged; + + @override + State createState() => _CheckboxTextButtonState(); +} + +class _CheckboxTextButtonState extends State { + bool _value = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + setState(() { + _value = !_value; + }); + widget.onChanged?.call(_value); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: IgnorePointer( + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _value, + onChanged: (_) {}, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + widget.label, + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ); + } +} From 969e7f2dcd040b2e92b7ef8428730c7b1349bc8f Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 May 2024 17:36:07 -0600 Subject: [PATCH 196/272] simplify showing the frost flow steps qr code data --- .../new/steps/frost_create_step_2.dart | 29 ++----------- .../new/steps/frost_create_step_3.dart | 29 ++----------- .../reshare/frost_reshare_step_2abd.dart | 27 +++--------- .../reshare/frost_reshare_step_3abd.dart | 30 +++---------- .../reshare/frost_reshare_step_3c.dart | 31 +++---------- .../reshare/frost_reshare_step_4.dart | 30 +++---------- .../send_steps/frost_send_step_2.dart | 23 +++------- .../send_steps/frost_send_step_3.dart | 29 +++---------- .../frost_qr_dialog_button.dart | 43 +++++++++++++++++++ 9 files changed, 84 insertions(+), 187 deletions(-) create mode 100644 lib/widgets/custom_buttons/frost_qr_dialog_button.dart diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart index a99d2b6db..181a7c3d6 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -1,20 +1,16 @@ 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/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/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/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_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'; @@ -47,17 +43,6 @@ class _FrostCreateStep2State extends ConsumerState { final List fieldIsEmptyFlags = []; bool _userVerifyContinue = false; - Future _showQrCodeDialog() async { - await showDialog( - context: context, - builder: (_) => FrostStepQrDialog( - myName: ref.read(pFrostMyName)!, - title: "Step 2 of 5 - ${FrostCreateStep2.title}", - data: myCommitment, - ), - ); - } - @override void initState() { participants = Frost.getParticipants( @@ -115,16 +100,8 @@ class _FrostCreateStep2State extends ConsumerState { ), ), const SizedBox(height: 12), - SecondaryButton( - label: "View QR code", - icon: SvgPicture.asset( - Assets.svg.qrcode, - colorFilter: ColorFilter.mode( - Theme.of(context).extension()!.buttonTextSecondary, - BlendMode.srcIn, - ), - ), - onPressed: _showQrCodeDialog, + FrostQrDialogPopupButton( + data: myCommitment, ), const SizedBox(height: 12), for (int i = 0; i < participants.length; i++) diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart index 72dd81410..782d86135 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -1,20 +1,16 @@ 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/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/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/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/detail_item.dart'; -import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_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'; @@ -46,17 +42,6 @@ class _FrostCreateStep3State extends ConsumerState { final List fieldIsEmptyFlags = []; - Future _showQrCodeDialog() async { - await showDialog( - context: context, - builder: (_) => FrostStepQrDialog( - myName: ref.read(pFrostMyName)!, - title: "Step 3 of 5 - ${FrostCreateStep3.title}", - data: myShare, - ), - ); - } - @override void initState() { participants = Frost.getParticipants( @@ -114,16 +99,8 @@ class _FrostCreateStep3State extends ConsumerState { ), ), const SizedBox(height: 12), - SecondaryButton( - label: "View QR code", - icon: SvgPicture.asset( - Assets.svg.qrcode, - colorFilter: ColorFilter.mode( - Theme.of(context).extension()!.buttonTextSecondary, - BlendMode.srcIn, - ), - ), - onPressed: _showQrCodeDialog, + FrostQrDialogPopupButton( + data: myShare, ), const SizedBox(height: 12), for (int i = 0; i < participants.length; i++) diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart index e6c7a70ee..63edc03cf 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart @@ -2,17 +2,16 @@ import 'dart:async'; 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/db/main_db_provider.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/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'; @@ -151,26 +150,6 @@ class _FrostReshareStep2abdState extends ConsumerState { padding: const EdgeInsets.all(16), child: Column( children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myResharerStart, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), DetailItem( title: "My resharer", detail: myResharerStart, @@ -182,6 +161,10 @@ class _FrostReshareStep2abdState extends ConsumerState { data: myResharerStart, ), ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myResharerStart, + ), const SizedBox( height: 12, ), diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart index 145ceb72b..050baa3e8 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -2,15 +2,14 @@ import 'dart:ffi'; 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/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.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'; @@ -134,28 +133,6 @@ class _FrostReshareStep3abdState extends ConsumerState { padding: const EdgeInsets.all(16), child: Column( children: [ - if (!amOutgoingParticipant) - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myEncryptionKey!, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - if (!amOutgoingParticipant) - const SizedBox( - height: 12, - ), if (!amOutgoingParticipant) DetailItem( title: "My encryption key", @@ -168,6 +145,11 @@ class _FrostReshareStep3abdState extends ConsumerState { data: myEncryptionKey!, ), ), + if (!amOutgoingParticipant) const SizedBox(height: 12), + if (!amOutgoingParticipant) + FrostQrDialogPopupButton( + data: myEncryptionKey!, + ), if (!amOutgoingParticipant) const SizedBox( height: 12, diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart index 3e129604a..3bde7bc76 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart @@ -1,12 +1,11 @@ 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/themes/stack_colors.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'; @@ -30,29 +29,6 @@ class _FrostReshareStep3cState extends ConsumerState { padding: const EdgeInsets.all(16), child: Column( children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref - .watch(pFrostResharingData) - .startResharedData! - .resharedStart, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const SizedBox( - height: 16, - ), DetailItem( title: "My encryption key", detail: @@ -71,6 +47,11 @@ class _FrostReshareStep3cState extends ConsumerState { .resharedStart, ), ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: + ref.watch(pFrostResharingData).startResharedData!.resharedStart, + ), if (!Util.isDesktop) const Spacer(), const SizedBox( height: 16, diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart index f60dd29f9..973dc99d7 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -2,7 +2,6 @@ import 'dart:ffi'; 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/pages/wallet_view/wallet_view.dart'; @@ -10,11 +9,11 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/des 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/themes/stack_colors.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'; @@ -168,28 +167,6 @@ class _FrostReshareStep4State extends ConsumerState { padding: const EdgeInsets.all(16), child: Column( children: [ - if (myResharerComplete != null) - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myResharerComplete!, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - if (myResharerComplete != null) - const SizedBox( - height: 16, - ), if (myResharerComplete != null) DetailItem( title: "My resharer complete", @@ -202,6 +179,11 @@ class _FrostReshareStep4State extends ConsumerState { data: myResharerComplete!, ), ), + if (myResharerComplete != null) const SizedBox(height: 12), + if (myResharerComplete != null) + FrostQrDialogPopupButton( + data: myResharerComplete!, + ), if (!amOutgoingParticipant) const SizedBox( height: 16, diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart index 06394c2a6..782783911 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart @@ -2,7 +2,6 @@ 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: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'; @@ -14,6 +13,7 @@ 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'; @@ -161,23 +161,6 @@ class _FrostSendStep2State extends ConsumerState { ], ), ), - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myPreprocess, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), const SizedBox( height: 12, ), @@ -199,6 +182,10 @@ class _FrostSendStep2State extends ConsumerState { data: myPreprocess, ), ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myPreprocess, + ), const SizedBox( height: 12, ), diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart index 25a39b13b..e606eb307 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -2,18 +2,17 @@ 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: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/services/frost.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/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'; @@ -94,26 +93,6 @@ class _FrostSendStep3State extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: myShare, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), DetailItem( title: "My name", detail: myName, @@ -135,6 +114,12 @@ class _FrostSendStep3State extends ConsumerState { const SizedBox( height: 12, ), + FrostQrDialogPopupButton( + data: myShare, + ), + const SizedBox( + height: 12, + ), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/widgets/custom_buttons/frost_qr_dialog_button.dart b/lib/widgets/custom_buttons/frost_qr_dialog_button.dart new file mode 100644 index 000000000..0b63f097e --- /dev/null +++ b/lib/widgets/custom_buttons/frost_qr_dialog_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart'; + +class FrostQrDialogPopupButton extends ConsumerWidget { + const FrostQrDialogPopupButton({super.key, required this.data}); + + final String data; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SecondaryButton( + label: "View QR code", + icon: SvgPicture.asset( + Assets.svg.qrcode, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.buttonTextSecondary, + BlendMode.srcIn, + ), + ), + onPressed: () async { + await showDialog( + context: context, + builder: (_) => FrostStepQrDialog( + myName: ref.read(pFrostMyName)!, + title: "Step " + "${ref.read(pFrostCreateCurrentStep)}" + " of " + "${ref.read(pFrostScaffoldArgs)!.stepRoutes.length}" + " - ${ref.read(pFrostScaffoldArgs)!.stepRoutes[ref.watch(pFrostCreateCurrentStep) - 1].title}", + data: data, + ), + ); + }, + ); + } +} From 7276cd41f0d7f2c81360f8b287ff7d08a29b233e Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 May 2024 09:41:57 -0600 Subject: [PATCH 197/272] frost wallet backup screen bugfix --- .../wallet_backup_view.dart | 4 +-- .../wallet_settings_view.dart | 32 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index fee8781ff..ab6baf455 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -35,12 +35,12 @@ 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"; diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 2eca78057..66dfb8c23 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -53,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"; @@ -267,31 +267,27 @@ class _WalletSettingsViewState extends ConsumerState { })? prevGen, })? frostWalletData; if (wallet is BitcoinFrostWallet) { - List> futures = []; - - futures.addAll( - [ - wallet.getSerializedKeys(), - wallet.getMultisigConfig(), - wallet.getSerializedKeysPrevGen(), - wallet.getMultisigConfigPrevGen(), - ], - ); + final futures = [ + wallet.getSerializedKeys(), + wallet.getMultisigConfig(), + wallet.getSerializedKeysPrevGen(), + wallet.getMultisigConfigPrevGen(), + ]; final results = await Future.wait(futures); - if (results.length == 5) { + if (results.length == 4) { frostWalletData = ( myName: wallet.frostInfo.myName, - config: results[1], - keys: results[0], + config: results[1]!, + keys: results[0]!, prevGen: results[2] == null || results[3] == null ? null : ( - config: results[3], - keys: results[2], + config: results[3]!, + keys: results[2]!, ), ); } @@ -301,7 +297,7 @@ class _WalletSettingsViewState extends ConsumerState { await wallet.getMnemonicAsWords(); } - if (mounted) { + if (context.mounted) { await Navigator.push( context, RouteGenerator.getRoute( From a858e4924ceb64b695dca1a6e32fdee5fbb184d8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 May 2024 10:50:28 -0600 Subject: [PATCH 198/272] desktop frost ui bandaid --- lib/frost_route_generator.dart | 1 + .../select_new_frost_import_type_view.dart | 4 +++ .../new/steps/frost_create_step_5.dart | 1 + .../dialogs/frost/frost_step_qr_dialog.dart | 34 ++++++++++++------- lib/widgets/frost_scaffold.dart | 5 +-- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/frost_route_generator.dart b/lib/frost_route_generator.dart index 297adbf04..6de2f6104 100644 --- a/lib/frost_route_generator.dart +++ b/lib/frost_route_generator.dart @@ -33,6 +33,7 @@ enum FrostInterruptionDialogType { } final pFrostCreateCurrentStep = StateProvider.autoDispose((ref) => 1); +final pFrostScaffoldCanPopDesktop = StateProvider.autoDispose((_) => false); final pFrostScaffoldArgs = StateProvider< ({ ({String walletName, FrostCurrency frostCurrency}) info, diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index 8b0e4118f..664d86357 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -193,6 +193,10 @@ class _SelectNewFrostImportTypeViewState stepRoutes: FrostRouteGenerator.joinReshareStepRoutes, onSuccess: () { // successful completion of steps + ref.read(pFrostMultisigConfig.state).state = null; + ref.read(pFrostStartKeyGenData.state).state = null; + ref.read(pFrostSecretSharesData.state).state = null; + ref.read(pFrostScaffoldArgs.state).state = null; }, frostInterruptionDialogType: FrostInterruptionDialogType.resharing, diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 88cbfc5a6..6bb5e3643 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -180,6 +180,7 @@ class _FrostCreateStep5State extends ConsumerState { } if (mounted) { + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; ref.read(pFrostScaffoldArgs)!.onSuccess(); } } catch (e, s) { diff --git a/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart b/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart index acb028af1..2dd1ace26 100644 --- a/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart +++ b/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -137,18 +138,27 @@ class _FrostStepQrDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.all(16), - child: AspectRatio( - aspectRatio: 1, - child: QrImageView( - data: widget.data, - padding: EdgeInsets.zero, - dataModuleStyle: QrDataModuleStyle( - dataModuleShape: QrDataModuleShape.square, - color: Theme.of(context) - .extension()! - .accentColorDark, + ConditionalParent( + condition: Util.isDesktop, + builder: (child) => ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 360, + ), + child: child, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: AspectRatio( + aspectRatio: 1, + child: QrImageView( + data: widget.data, + padding: EdgeInsets.zero, + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), diff --git a/lib/widgets/frost_scaffold.dart b/lib/widgets/frost_scaffold.dart index a5d218dc0..e5d83e088 100644 --- a/lib/widgets/frost_scaffold.dart +++ b/lib/widgets/frost_scaffold.dart @@ -41,7 +41,8 @@ class _FrostScaffoldState extends ConsumerState { } Future _requestPop(BuildContext context) async { - if (_requestPopLock) { + if (_requestPopLock || + (Util.isDesktop && ref.read(pFrostScaffoldCanPopDesktop))) { return; } _requestPopLock = true; @@ -98,7 +99,7 @@ class _FrostScaffoldState extends ConsumerState { @override Widget build(BuildContext context) { return PopScope( - canPop: false, + canPop: Util.isDesktop && ref.watch(pFrostScaffoldCanPopDesktop), onPopInvoked: (_) => _requestPop(context), child: Material( child: ConditionalParent( From 4b4647e02cdadd21929bb663d003dee2da9451b7 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 May 2024 11:06:42 -0600 Subject: [PATCH 199/272] disable frost send all button --- lib/pages/send_view/frost_ms/recipient.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index 0540dd787..043f080f6 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -373,13 +373,15 @@ class _RecipientState extends ConsumerState { style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - CustomTextButton( - text: "Send all ${widget.coin.ticker.toUpperCase()}", - onTap: () { - amountController.text = widget.sendAllTapped(); - _cryptoAmountChanged(); - }, - ), + // 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) From 1be51a666b2465774d68fb4baf439074b77029a3 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 May 2024 11:32:02 -0600 Subject: [PATCH 200/272] frost initiate tx signing screen ui polish --- .../send_steps/frost_send_step_1a.dart | 79 +++++++--- lib/widgets/detail_item.dart | 137 +++++++++++------- lib/widgets/frost_step_user_steps.dart | 38 +++-- 3 files changed, 168 insertions(+), 86 deletions(-) diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart index fd83a05a0..0a026f02d 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart @@ -10,6 +10,7 @@ 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'; @@ -34,6 +35,9 @@ class _FrostSendStep1aState extends ConsumerState { "Check the box and press “Attempt sign”.", ]; + late final int _threshold; + + bool _userVerifyContinue = false; bool _attemptSignLock = false; Future _attemptSign() async { @@ -71,10 +75,20 @@ class _FrostSendStep1aState extends ConsumerState { } } + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + _threshold = wallet.frostInfo.threshold; + super.initState(); + } + @override Widget build(BuildContext context) { final double qrImageSize = - Util.isDesktop ? 360 : MediaQuery.of(context).size.width - 32; + Util.isDesktop ? 360 : MediaQuery.of(context).size.width / 1.67; return Padding( padding: const EdgeInsets.all(16), @@ -120,23 +134,26 @@ class _FrostSendStep1aState extends ConsumerState { ], ), for (int i = 0; i < steps2to4.length; i++) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${i + 2}.", - style: STextStyles.w500_12(context), - ), - const SizedBox( - width: 4, - ), - Expanded( - child: Text( - steps2to4[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), + ), + ), + ], + ), ), ], ), @@ -161,12 +178,11 @@ class _FrostSendStep1aState extends ConsumerState { ], ), ), - if (!Util.isDesktop) - const SizedBox( - height: 32, - ), + SizedBox( + height: Util.isDesktop ? 20 : 16, + ), DetailItem( - title: "Encoded config", + title: "Encoded transaction config", detail: ref.watch(pFrostTxData.state).state!.frostMSConfig!, button: Util.isDesktop ? IconCopyButton( @@ -179,12 +195,33 @@ class _FrostSendStep1aState extends ConsumerState { SizedBox( height: Util.isDesktop ? 20 : 16, ), + DetailItem( + title: "Threshold", + detail: "$_threshold signatures", + horizontal: true, + ), + SizedBox( + height: Util.isDesktop ? 20 : 16, + ), 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 : 16, + ), PrimaryButton( label: "Attempt sign", + enabled: _userVerifyContinue, onPressed: () { _attemptSign(); }, diff --git a/lib/widgets/detail_item.dart b/lib/widgets/detail_item.dart index 75e0e6a1e..ee3694d0a 100644 --- a/lib/widgets/detail_item.dart +++ b/lib/widgets/detail_item.dart @@ -7,22 +7,35 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; class DetailItem extends StatelessWidget { const DetailItem({ - Key? key, + super.key, required this.title, required this.detail, this.button, + this.overrideDetailTextColor, this.showEmptyDetail = true, + this.horizontal = false, this.disableSelectableText = false, - }) : super(key: key); + }); final String title; final String detail; final Widget? button; final bool showEmptyDetail; + final bool horizontal; final bool disableSelectableText; + final Color? overrideDetailTextColor; @override Widget build(BuildContext context) { + final TextStyle detailStyle; + if (overrideDetailTextColor != null) { + detailStyle = STextStyles.w500_14(context).copyWith( + color: overrideDetailTextColor, + ); + } else { + detailStyle = STextStyles.w500_14(context); + } + return ConditionalParent( condition: !Util.isDesktop, builder: (child) => RoundedWhiteContainer( @@ -34,56 +47,80 @@ class DetailItem extends StatelessWidget { padding: const EdgeInsets.all(16), child: child, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - disableSelectableText - ? Text( - title, - style: STextStyles.itemSubtitle(context), - ) - : SelectableText( - title, - style: STextStyles.itemSubtitle(context), - ), - button ?? Container(), - ], - ), - const SizedBox( - height: 5, - ), - detail.isEmpty && showEmptyDetail - ? disableSelectableText - ? Text( - "$title will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, + child: horizontal + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + disableSelectableText + ? Text( + title, + style: STextStyles.itemSubtitle(context), + ) + : SelectableText( + title, + style: STextStyles.itemSubtitle(context), ), - ) - : SelectableText( - "$title will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, + disableSelectableText + ? Text( + detail, + style: detailStyle, + ) + : SelectableText( + detail, + style: detailStyle, ), - ) - : disableSelectableText - ? Text( - detail, - style: STextStyles.w500_14(context), - ) - : SelectableText( - detail, - style: STextStyles.w500_14(context), - ), - ], - ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + disableSelectableText + ? Text( + title, + style: STextStyles.itemSubtitle(context), + ) + : SelectableText( + title, + style: STextStyles.itemSubtitle(context), + ), + button ?? Container(), + ], + ), + const SizedBox( + height: 5, + ), + detail.isEmpty && showEmptyDetail + ? disableSelectableText + ? Text( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ) + : SelectableText( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ) + : disableSelectableText + ? Text( + detail, + style: detailStyle, + ) + : SelectableText( + detail, + style: detailStyle, + ), + ], + ), ), ); } diff --git a/lib/widgets/frost_step_user_steps.dart b/lib/widgets/frost_step_user_steps.dart index 6624ef45d..729264344 100644 --- a/lib/widgets/frost_step_user_steps.dart +++ b/lib/widgets/frost_step_user_steps.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class FrostStepUserSteps extends StatelessWidget { @@ -13,23 +14,30 @@ class FrostStepUserSteps extends StatelessWidget { child: Column( children: [ for (int i = 0; i < userSteps.length; i++) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${i + 1}.", - style: STextStyles.w500_12(context), - ), - const SizedBox( - width: 4, - ), - Expanded( - child: Text( - userSteps[i], + ConditionalParent( + condition: i > 0, + builder: (child) => Padding( + padding: const EdgeInsets.only(top: 4), + child: child, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${i + 1}.", style: STextStyles.w500_12(context), ), - ), - ], + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + userSteps[i], + style: STextStyles.w500_12(context), + ), + ), + ], + ), ), ], ), From 832be89cab217330ece57b774f79d50108685277 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 May 2024 16:43:02 -0600 Subject: [PATCH 201/272] frost transaction sending ui update --- .../send_steps/frost_send_step_1a.dart | 15 +- .../send_steps/frost_send_step_1b.dart | 30 ++- .../send_steps/frost_send_step_2.dart | 204 +++++--------- .../send_steps/frost_send_step_3.dart | 254 ++++++------------ .../send_steps/frost_send_step_4.dart | 229 +++++++++++++--- lib/wallets/models/tx_data.dart | 7 +- 6 files changed, 377 insertions(+), 362 deletions(-) diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart index 0a026f02d..d49474f6d 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart @@ -82,6 +82,9 @@ class _FrostSendStep1aState extends ConsumerState { ) as BitcoinFrostWallet; _threshold = wallet.frostInfo.threshold; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(pFrostMyName.state).state = wallet.frostInfo.myName; + }); super.initState(); } @@ -116,7 +119,7 @@ class _FrostSendStep1aState extends ConsumerState { TextSpan( text: "Share this config with the group members. ", - style: STextStyles.w600_12(context), + style: STextStyles.w500_12(context), ), TextSpan( text: @@ -159,7 +162,7 @@ class _FrostSendStep1aState extends ConsumerState { ), ), SizedBox( - height: Util.isDesktop ? 20 : 16, + height: Util.isDesktop ? 20 : 12, ), SizedBox( height: qrImageSize, @@ -179,7 +182,7 @@ class _FrostSendStep1aState extends ConsumerState { ), ), SizedBox( - height: Util.isDesktop ? 20 : 16, + height: Util.isDesktop ? 20 : 12, ), DetailItem( title: "Encoded transaction config", @@ -193,7 +196,7 @@ class _FrostSendStep1aState extends ConsumerState { ), ), SizedBox( - height: Util.isDesktop ? 20 : 16, + height: Util.isDesktop ? 20 : 12, ), DetailItem( title: "Threshold", @@ -201,7 +204,7 @@ class _FrostSendStep1aState extends ConsumerState { horizontal: true, ), SizedBox( - height: Util.isDesktop ? 20 : 16, + height: Util.isDesktop ? 20 : 12, ), if (!Util.isDesktop) const Spacer( @@ -217,7 +220,7 @@ class _FrostSendStep1aState extends ConsumerState { }, ), SizedBox( - height: Util.isDesktop ? 20 : 16, + height: Util.isDesktop ? 20 : 12, ), PrimaryButton( label: "Attempt sign", diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart index 94f367507..197d72e63 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart @@ -12,6 +12,7 @@ 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'; @@ -41,7 +42,7 @@ class _FrostSendStep1bState extends ConsumerState { late final TextEditingController configFieldController; late final FocusNode configFocusNode; - bool _configEmpty = true; + bool _configEmpty = true, _userVerifyContinue = false; bool _attemptSignLock = false; @@ -125,6 +126,12 @@ class _FrostSendStep1bState extends ConsumerState { 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(); } @@ -146,7 +153,7 @@ class _FrostSendStep1bState extends ConsumerState { const FrostStepUserSteps( userSteps: info, ), - const SizedBox(height: 12), + const SizedBox(height: 20), FrostStepField( controller: configFieldController, focusNode: configFocusNode, @@ -159,16 +166,25 @@ class _FrostSendStep1bState extends ConsumerState { }); }, ), - const SizedBox( - height: 16, - ), if (!Util.isDesktop) const Spacer(), const SizedBox( - height: 16, + 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, + enabled: !_configEmpty && _userVerifyContinue, onPressed: () { _attemptSign(); }, diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart index 782783911..66f088b39 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart @@ -1,6 +1,4 @@ -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:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; @@ -8,7 +6,6 @@ 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/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -17,13 +14,9 @@ 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/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_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:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostSendStep2 extends ConsumerStatefulWidget { const FrostSendStep2({super.key}); @@ -47,7 +40,7 @@ class _FrostSendStep2State extends ConsumerState { final List fieldIsEmptyFlags = []; - bool hasEnoughPreprocesses() { + int countPreprocesses() { // own preprocess is not included in controllers and must be set here int count = 1; @@ -57,7 +50,7 @@ class _FrostSendStep2State extends ConsumerState { } } - return count >= threshold; + return count; } @override @@ -124,11 +117,14 @@ class _FrostSendStep2State extends ConsumerState { ), ], ), + const SizedBox( + height: 4, + ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "1.", + "2.", style: STextStyles.w500_12(context), ), const SizedBox( @@ -141,7 +137,7 @@ class _FrostSendStep2State extends ConsumerState { TextSpan( text: "Enter their preprocesses into the corresponding fields. ", - style: STextStyles.w600_12(context), + style: STextStyles.w500_12(context), ), TextSpan( text: "You must have the threshold number of " @@ -164,9 +160,24 @@ class _FrostSendStep2State extends ConsumerState { 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, @@ -189,141 +200,46 @@ class _FrostSendStep2State extends ConsumerState { const SizedBox( height: 12, ), + RoundedWhiteContainer( + child: Text( + "You need to obtain ${threshold - 1} preprocess from signing members to send this transaction.", + style: STextStyles.label(context), + ), + ), + const SizedBox( + height: 12, + ), + Builder(builder: (context) { + final count = countPreprocesses(); + final colors = Theme.of(context).extension()!; + return DetailItem( + title: "Required preprocesses", + detail: "$count of $threshold", + horizontal: true, + overrideDetailTextColor: count >= threshold + ? colors.accentColorGreen + : colors.accentColorRed, + ); + }), + const SizedBox( + height: 12, + ), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ for (int i = 0; i < participantsWithoutMe.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostPreprocessesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - onChanged: (_) { - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - decoration: standardInputDecoration( - "Enter ${participantsWithoutMe[i]}'s preprocess", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Preprocess Field Input.", - key: Key( - "frostPreprocessesClearButtonKey_$i", - ), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Preprocess Field Input.", - key: Key( - "frostPreprocessesPasteButtonKey_$i", - ), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: Key( - "frostPreprocessesScanQrButtonKey_$i", - ), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - ], + 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, ), ], ), @@ -332,8 +248,8 @@ class _FrostSendStep2State extends ConsumerState { height: 12, ), PrimaryButton( - label: "Continue signing", - enabled: hasEnoughPreprocesses(), + label: "Generate shares", + enabled: countPreprocesses() >= threshold, onPressed: () async { // collect Preprocess strings (not including my own) final preprocesses = controllers.map((e) => e.text).toList(); diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart index e606eb307..bb7e462b9 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -1,27 +1,23 @@ -import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; 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/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/constants.dart'; +import 'package:stackwallet/utilities/amount/amount.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/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/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/frost_step_user_steps.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:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostSendStep3 extends ConsumerStatefulWidget { const FrostSendStep3({super.key}); @@ -34,6 +30,11 @@ class FrostSendStep3 extends ConsumerStatefulWidget { } class _FrostSendStep3State extends ConsumerState { + static const info = [ + "Send your share to other signing group members.", + "Enter their shares into the corresponding fields.", + ]; + final List controllers = []; final List focusNodes = []; @@ -45,6 +46,8 @@ class _FrostSendStep3State extends ConsumerState { final List fieldIsEmptyFlags = []; + bool _userVerifyContinue = false; + @override void initState() { final wallet = ref.read(pWallets).getWallet( @@ -93,15 +96,28 @@ class _FrostSendStep3State extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - DetailItem( - title: "My name", - detail: myName, + const FrostStepUserSteps( + userSteps: info, ), const SizedBox( height: 12, ), DetailItem( - title: "My shares", + 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( @@ -125,133 +141,17 @@ class _FrostSendStep3State extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ for (int i = 0; i < participantsWithoutMe.length; i++) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: Key("frostSharesTextFieldKey_$i"), - controller: controllers[i], - focusNode: focusNodes[i], - readOnly: false, - autocorrect: false, - enableSuggestions: false, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${participantsWithoutMe[i]}'s share", - focusNodes[i], - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: fieldIsEmptyFlags[i] - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - !fieldIsEmptyFlags[i] - ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears " - "The Share Field Input.", - key: Key( - "frostSharesClearButtonKey_$i", - ), - onTap: () { - controllers[i].text = ""; - - setState(() { - fieldIsEmptyFlags[i] = true; - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From " - "Clipboard To Share Field Input.", - key: Key( - "frostSharesPasteButtonKey_$i"), - onTap: () async { - final ClipboardData? data = - await Clipboard.getData( - Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - controllers[i].text = - data.text!.trim(); - } - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].text.isEmpty; - }); - }, - child: fieldIsEmptyFlags[i] - ? const ClipboardIcon() - : const XIcon(), - ), - if (fieldIsEmptyFlags[i]) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera " - "For Scanning QR Code.", - key: Key( - "frostSharesScanQrButtonKey_$i", - ), - onTap: () async { - try { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75)); - } - - final qrResult = - await BarcodeScanner.scan(); - - controllers[i].text = - qrResult.rawContent; - - setState(() { - fieldIsEmptyFlags[i] = - controllers[i].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(), - ) - ], - ), - ), - ), - ), - ), - ), - ), - ], + 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, ), ], ), @@ -259,22 +159,22 @@ class _FrostSendStep3State extends ConsumerState { 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: "Complete signing", + label: "Generate transaction", + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), onPressed: () async { - // check for empty shares - if (controllers - .map((e) => e.text.isEmpty) - .reduce((value, element) => value |= element)) { - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Missing Shares", - desktopPopRootNavigator: Util.isDesktop, - ), - ); - } - // collect Share strings final sharesCollected = controllers.map((e) => e.text).toList(); @@ -295,10 +195,32 @@ class _FrostSendStep3State extends ConsumerState { shares: shares, ); - ref.read(pFrostTxData.state).state = - ref.read(pFrostTxData.state).state!.copyWith( - raw: rawTx, - ); + 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( @@ -313,13 +235,15 @@ class _FrostSendStep3State extends ConsumerState { level: LogLevel.Fatal, ); - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to complete signing process", - desktopPopRootNavigator: Util.isDesktop, - ), - ); + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to complete signing process", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } } }, ), diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart index 315bbb9a9..7ad7bfcdd 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart @@ -1,19 +1,27 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:qr_flutter/qr_flutter.dart'; +import 'package:flutter_svg/flutter_svg.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/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 { @@ -27,46 +35,154 @@ class FrostSendStep4 extends ConsumerStatefulWidget { } class _FrostSendStep4State extends ConsumerState { + final List _expandedStates = []; + bool _broadcastLock = false; + late final CryptoCurrency cryptoCurrency; + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + cryptoCurrency = wallet.cryptoCurrency; + + for (final _ in ref.read(pFrostTxData)!.recipients!) { + _expandedStates.add(false); + } + + super.initState(); + } + @override Widget build(BuildContext context) { + final signerNames = ref.watch(pFrostTxData)!.frostSigners!; + final recipients = ref.watch(pFrostTxData)!.recipients!; + + final String signers; + if (signerNames.length > 1) { + signers = signerNames + .sublist(1) + .fold(signerNames.first, (pv, e) => pv += ", $e"); + } else { + signers = signerNames.first; + } + return Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox( - height: 220, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QrImageView( - data: ref.watch(pFrostTxData.state).state!.raw!, - size: 220, - backgroundColor: - Theme.of(context).extension()!.background, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark, - ), - ], + if (kDebugMode) + DetailItem( + title: "Tx hex (debug mode only)", + detail: ref.watch(pFrostTxData)!.raw!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostTxData)!.raw!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostTxData)!.raw!, + ), ), + if (kDebugMode) + const SizedBox( + height: 12, + ), + Text( + "Send ${cryptoCurrency.coin.ticker}", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + recipients.length == 1 + ? _Recipient( + address: recipients[0].address, + amount: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(recipients[0].amount), + ) + : Column( + children: [ + for (int i = 0; i < recipients.length; i++) + Padding( + padding: const EdgeInsets.only(top: 10), + child: Expandable( + onExpandChanged: (state) { + setState(() { + _expandedStates[i] = + state == ExpandableState.expanded; + }); + }, + header: Padding( + padding: const EdgeInsets.only(top: 12, bottom: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient ${i + 1}", + style: STextStyles.itemSubtitle(context), + ), + SvgPicture.asset( + _expandedStates[i] + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ], + ), + ), + body: _Recipient( + address: recipients[i].address, + amount: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(recipients[i].amount), + ), + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Transaction fee", + detail: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(ref.watch(pFrostTxData)!.fee!), + horizontal: true, ), const SizedBox( height: 12, ), DetailItem( - title: "Raw transaction hex", - detail: ref.watch(pFrostTxData.state).state!.raw!, - button: Util.isDesktop - ? IconCopyButton( - data: ref.watch(pFrostTxData.state).state!.raw!, - ) - : SimpleCopyButton( - data: ref.watch(pFrostTxData.state).state!.raw!, - ), + 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, @@ -76,7 +192,7 @@ class _FrostSendStep4State extends ConsumerState { height: 12, ), PrimaryButton( - label: "Broadcast Transaction", + label: "Approve transaction", onPressed: () async { if (_broadcastLock) { return; @@ -92,11 +208,11 @@ class _FrostSendStep4State extends ConsumerState { ref.read(pFrostScaffoldArgs)!.walletId!, ) .confirmSend( - txData: ref.read(pFrostTxData.state).state!, + txData: ref.read(pFrostTxData)!, ), context: context, message: "Broadcasting transaction to network", - isDesktop: Util.isDesktop, + isDesktop: true, // used to pop using root nav onException: (e) { ex = e; }, @@ -106,7 +222,7 @@ class _FrostSendStep4State extends ConsumerState { throw ex!; } - if (mounted) { + if (context.mounted) { if (txData != null) { ref.read(pFrostTxData.state).state = txData; Navigator.of(context).popUntil( @@ -123,15 +239,18 @@ class _FrostSendStep4State extends ConsumerState { "$e\n$s", level: LogLevel.Fatal, ); - - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Broadcast error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Broadcast error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + onOkPressed: + Navigator.of(context, rootNavigator: true).pop, + ), + ); + } } finally { _broadcastLock = false; } @@ -142,3 +261,35 @@ class _FrostSendStep4State extends ConsumerState { ); } } + +class _Recipient extends StatelessWidget { + const _Recipient({ + super.key, + required this.address, + required this.amount, + }); + + final String address; + final String amount; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + DetailItem( + title: "Address", + detail: address, + ), + const SizedBox( + height: 6, + ), + DetailItem( + title: "Amount", + detail: amount, + horizontal: true, + ), + ], + ); + } +} diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 22101003c..d98dd088e 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -33,7 +33,9 @@ class TxData { final String? changeAddress; + // frost specific final String? frostMSConfig; + final List? frostSigners; // paynym specific final PaynymAccountLite? paynymAccountLite; @@ -91,6 +93,7 @@ class TxData { this.usedUTXOs, this.changeAddress, this.frostMSConfig, + this.frostSigners, this.paynymAccountLite, this.web3dartTransaction, this.nonce, @@ -166,6 +169,7 @@ class TxData { })>? recipients, String? frostMSConfig, + List? frostSigners, String? changeAddress, PaynymAccountLite? paynymAccountLite, web3dart.Transaction? web3dartTransaction, @@ -209,6 +213,7 @@ class TxData { usedUTXOs: usedUTXOs ?? this.usedUTXOs, recipients: recipients ?? this.recipients, frostMSConfig: frostMSConfig ?? this.frostMSConfig, + frostSigners: frostSigners ?? this.frostSigners, changeAddress: changeAddress ?? this.changeAddress, paynymAccountLite: paynymAccountLite ?? this.paynymAccountLite, web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction, @@ -249,7 +254,7 @@ class TxData { 'recipients: $recipients, ' 'utxos: $utxos, ' 'usedUTXOs: $usedUTXOs, ' - 'frostMSConfig: $frostMSConfig, ' + 'frostSigners: $frostSigners, ' 'changeAddress: $changeAddress, ' 'paynymAccountLite: $paynymAccountLite, ' 'web3dartTransaction: $web3dartTransaction, ' From e2fe526be4dafbf80326ed35501e993f98068283 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 3 May 2024 10:32:32 -0600 Subject: [PATCH 202/272] Don't print keys --- .../wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 163052ec0..52fe50a4f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -92,8 +92,6 @@ class _UnlockWalletKeysDesktopState keys: (await wallet.getSerializedKeys())!, config: (await wallet.getMultisigConfig())!, ); - print(1111111); - print(frostData); } else { throw Exception("FIXME ~= see todo in code"); } From 0d3ae2a635e7e1cdf0a02a4992abc022e368321b Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 3 May 2024 12:22:19 -0600 Subject: [PATCH 203/272] various frost ui flow tweaks and fixes --- .../reshare/frost_reshare_step_1c.dart | 4 ++- .../reshare/frost_reshare_step_3abd.dart | 26 +++++++------- .../reshare/frost_reshare_step_4.dart | 34 +++++++++++-------- .../reshare/frost_reshare_step_5.dart | 2 +- .../restore/restore_frost_ms_wallet_view.dart | 2 +- .../cashfusion/fusion_progress_view.dart | 2 +- lib/pages/monkey/monkey_view.dart | 6 ++-- lib/pages/ordinals/ordinal_details_view.dart | 2 +- .../send_view/frost_ms/frost_send_view.dart | 2 +- .../send_steps/frost_send_step_4.dart | 2 +- .../change_representative_view.dart | 2 +- .../sub_widgets/my_token_select_item.dart | 2 +- .../sub_widgets/favorite_card.dart | 2 +- .../sub_widgets/wallet_list_item.dart | 2 +- .../cashfusion/sub_widgets/fusion_dialog.dart | 2 +- .../my_stack_view/coin_wallets_table.dart | 2 +- .../desktop_ordinal_details_view.dart | 2 +- .../ordinals/desktop_ordinals_view.dart | 2 +- lib/utilities/show_loading.dart | 4 +-- .../models/incomplete_frost_wallet.dart | 4 ++- lib/widgets/wallet_card.dart | 4 +-- 21 files changed, 60 insertions(+), 50 deletions(-) diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart index a0e39221d..c7690c45b 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -172,7 +172,7 @@ class _FrostReshareStep1cState extends ConsumerState { whileFuture: _createWallet(), context: context, message: "Setting up wallet", - isDesktop: Util.isDesktop, + rootNavigator: true, onException: (e) => ex = e, ); @@ -191,6 +191,8 @@ class _FrostReshareStep1cState extends ConsumerState { frostInterruptionDialogType: FrostInterruptionDialogType.resharing, ); + ref.read(pFrostMyName.state).state = + ref.read(pFrostResharingData).myName!; ref.read(pFrostCreateCurrentStep.state).state = 2; await Navigator.of(context).pushNamed( ref diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart index 050baa3e8..5bccf7756 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -179,23 +179,25 @@ class _FrostReshareStep3abdState extends ConsumerState { ], ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), - CheckboxTextButton( - label: "I have verified that everyone has my encryption key", - onChanged: (value) { - setState(() { - _userVerifyContinue = value; - }); - }, - ), + 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: _userVerifyContinue && + enabled: (amOutgoingParticipant || _userVerifyContinue) && !fieldIsEmptyFlags.reduce((v, e) => v |= e), onPressed: _onPressed, ), diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart index 973dc99d7..02530f8e4 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -40,6 +40,7 @@ class _FrostReshareStep4State extends ConsumerState { late final int? myResharerIndexIndex; late final String? myResharerComplete; late final bool amOutgoingParticipant; + late final bool amNewParticipant; final List fieldIsEmptyFlags = []; @@ -55,7 +56,8 @@ class _FrostReshareStep4State extends ConsumerState { try { if (amOutgoingParticipant) { ref.read(pFrostResharingData).reset(); - Navigator.of(context).popUntil( + // nav broken on desktop + Navigator.of(context, rootNavigator: !Util.isDesktop).popUntil( ModalRoute.withName( Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName, ), @@ -104,7 +106,7 @@ class _FrostReshareStep4State extends ConsumerState { @override void initState() { - final amNewParticipant = + amNewParticipant = ref.read(pFrostResharingData).startResharerData == null && ref.read(pFrostResharingData).incompleteWallet != null && ref.read(pFrostResharingData).incompleteWallet?.walletId == @@ -217,20 +219,22 @@ class _FrostReshareStep4State extends ConsumerState { const SizedBox( height: 16, ), - CheckboxTextButton( - label: "I have verified that everyone has my resharer complete", - onChanged: (value) { - setState(() { - _userVerifyContinue = value; - }); - }, - ), - 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 ? "Exit" : "Complete", - enabled: _userVerifyContinue && + label: amOutgoingParticipant ? "Done" : "Complete", + enabled: (amNewParticipant || _userVerifyContinue) && (amOutgoingParticipant || !fieldIsEmptyFlags.reduce((v, e) => v |= e)), onPressed: _onPressed, diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart index 10c7a6a24..b044b1d93 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart @@ -84,7 +84,7 @@ class _FrostReshareStep5State extends ConsumerState { ), context: context, message: isNew ? "Creating wallet" : "Updating wallet data", - isDesktop: Util.isDesktop, + rootNavigator: true, onException: (e) => ex = e, ); diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 2d4846289..68c220c1f 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -130,7 +130,7 @@ class _RestoreFrostMsWalletViewState whileFuture: _createWalletAndRecover(), context: context, message: "Restoring wallet...", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, onException: (e) { ex = e; }, diff --git a/lib/pages/cashfusion/fusion_progress_view.dart b/lib/pages/cashfusion/fusion_progress_view.dart index 746825d36..78bc9ccc7 100644 --- a/lib/pages/cashfusion/fusion_progress_view.dart +++ b/lib/pages/cashfusion/fusion_progress_view.dart @@ -79,7 +79,7 @@ class _FusionProgressViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Stopping fusion", ); diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index a747011ee..9e8bdb090 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -349,7 +349,7 @@ class _MonkeyViewState extends ConsumerState { ), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Saving MonKey svg", onException: (e) { didError = true; @@ -402,7 +402,7 @@ class _MonkeyViewState extends ConsumerState { const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Downloading MonKey png", onException: (e) { didError = true; @@ -500,7 +500,7 @@ class _MonkeyViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Fetching MonKey", subMessage: "We are fetching your MonKey", onException: (e) { diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index d15523e10..590bca266 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -321,7 +321,7 @@ class _OrdinalImageGroup extends ConsumerWidget { final filePath = await showLoading( whileFuture: _savePngToFile(ref), context: context, - isDesktop: true, + rootNavigator: true, message: "Saving ordinal image", onException: (e) { didError = true; diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index d6cfc505c..a79ee0e6e 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -116,7 +116,7 @@ class _FrostSendViewState extends ConsumerState { whileFuture: _loadingFuture(), context: context, message: "Generating sign config", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, onException: (e) { throw e; }, diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart index 7ad7bfcdd..2906af589 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart @@ -212,7 +212,7 @@ class _FrostSendStep4State extends ConsumerState { ), context: context, message: "Broadcasting transaction to network", - isDesktop: true, // used to pop using root nav + rootNavigator: true, // used to pop using root nav onException: (e) { ex = e; }, diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart index 0876c92e0..03b131993 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart @@ -83,7 +83,7 @@ class _ChangeRepresentativeViewState whileFuture: changeFuture(_textController.text), context: context, message: "Updating representative...", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, onException: (ex) { String msg = ex.toString(); while (msg.isNotEmpty && msg.startsWith("Exception:")) { diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 003fd515a..2fd42300d 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -98,7 +98,7 @@ class _MyTokenSelectItemState extends ConsumerState { final success = await showLoading( whileFuture: _loadTokenWallet(context, ref), context: context, - isDesktop: isDesktop, + rootNavigator: isDesktop, message: "Loading ${widget.token.name}", ); diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 8aafe8e30..4ef59bf0b 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -127,7 +127,7 @@ class _FavoriteCardState extends ConsumerState { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (mounted) { diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 2099b05e9..fada0b998 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -95,7 +95,7 @@ class WalletListItem extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (context.mounted) { unawaited( diff --git a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart index 2e02aeef9..6d4225fab 100644 --- a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart +++ b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart @@ -132,7 +132,7 @@ class _FusionDialogViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: true, + rootNavigator: true, message: "Stopping fusion", ); diff --git a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart index 7f138509f..94cdf75f8 100644 --- a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart @@ -92,7 +92,7 @@ class CoinWalletsTable extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (context.mounted) { diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index c28524635..b258d0a5e 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -234,7 +234,7 @@ class _DesktopOrdinalDetailsViewState final path = await showLoading( whileFuture: _savePngToFile(), context: context, - isDesktop: true, + rootNavigator: true, message: "Saving ordinal image", onException: (e) { didError = true; diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index 1fb4f29af..c710a4d10 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -210,7 +210,7 @@ class _DesktopOrdinals extends ConsumerState { onPressed: () async { // show loading for a minimum of 2 seconds on refreshing await showLoading( - isDesktop: true, + rootNavigator: true, whileFuture: Future.wait([ Future.delayed(const Duration(seconds: 2)), (ref.read(pWallets).getWallet(widget.walletId) diff --git a/lib/utilities/show_loading.dart b/lib/utilities/show_loading.dart index 759eaa05c..e01a86441 100644 --- a/lib/utilities/show_loading.dart +++ b/lib/utilities/show_loading.dart @@ -20,7 +20,7 @@ Future showLoading({ required BuildContext context, required String message, String? subMessage, - bool isDesktop = false, + bool rootNavigator = false, bool opaqueBG = false, void Function(Exception)? onException, }) async { @@ -59,7 +59,7 @@ Future showLoading({ } if (context.mounted) { - Navigator.of(context, rootNavigator: isDesktop).pop(); + Navigator.of(context, rootNavigator: rootNavigator).pop(); if (ex != null) { onException?.call(ex); } diff --git a/lib/wallets/models/incomplete_frost_wallet.dart b/lib/wallets/models/incomplete_frost_wallet.dart index e5075da63..015ca9fb9 100644 --- a/lib/wallets/models/incomplete_frost_wallet.dart +++ b/lib/wallets/models/incomplete_frost_wallet.dart @@ -35,7 +35,9 @@ class IncompleteFrostWallet { threshold: -1, ); - await mainDB.isar.frostWalletInfo.put(frostInfo); + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.put(frostInfo); + }); return wallet as BitcoinFrostWallet; } diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 31a29fb68..6387b8ccb 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -137,7 +137,7 @@ class SimpleWalletCard extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (popPrevious) nav.pop(); @@ -167,7 +167,7 @@ class SimpleWalletCard extends ConsumerWidget { context: desktopNavigatorState?.context ?? context, opaqueBG: true, message: "Loading ${contract.name}", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (!success!) { From e699522a8c171d518573d7e52314cc30a47d1d44 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 May 2024 14:24:14 -0600 Subject: [PATCH 204/272] code style/linter cleanup --- lib/wallets/wallet/impl/epiccash_wallet.dart | 48 ++++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index a6365321d..a5c4ab2b7 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -50,9 +50,9 @@ class EpiccashWallet extends Bip39Wallet { double highestPercent = 0; Future get getSyncPercent async { - int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; + final int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; final _chainHeight = await chainHeight; - double restorePercent = lastScannedBlock / _chainHeight; + final double restorePercent = lastScannedBlock / _chainHeight; GlobalEventBus.instance .fire(RefreshPercentChangedEvent(highestPercent, walletId)); if (restorePercent > highestPercent) { @@ -67,7 +67,7 @@ class EpiccashWallet extends Bip39Wallet { } Future updateEpicboxConfig(String host, int port) async { - String stringConfig = jsonEncode({ + final String stringConfig = jsonEncode({ "epicbox_domain": host, "epicbox_port": port, "epicbox_protocol_unsecure": false, @@ -103,7 +103,7 @@ class EpiccashWallet extends Bip39Wallet { } Future getEpicBoxConfig() async { - EpicBoxConfigModel? _epicBoxConfig = EpicBoxConfigModel.fromServer( + final EpicBoxConfigModel _epicBoxConfig = EpicBoxConfigModel.fromServer( DefaultEpicBoxes.defaultEpicBoxServer, ); @@ -156,12 +156,12 @@ class EpiccashWallet extends Bip39Wallet { config["api_listen_port"] = port; config["api_listen_interface"] = nodeApiAddress.replaceFirst(uri.scheme, ""); - String stringConfig = jsonEncode(config); + final String stringConfig = jsonEncode(config); return stringConfig; } Future _currentWalletDirPath() async { - Directory appDir = await StackFileSystem.applicationRootDirectory(); + final Directory appDir = await StackFileSystem.applicationRootDirectory(); final path = "${appDir.path}/epiccash"; final String name = walletId.trim(); @@ -176,7 +176,7 @@ class EpiccashWallet extends Bip39Wallet { try { final available = info.cachedBalance.spendable.raw.toInt(); - var transactionFees = await epiccash.LibEpiccash.getTransactionFees( + final transactionFees = await epiccash.LibEpiccash.getTransactionFees( wallet: wallet!, amount: satoshiAmount, minimumConfirmations: cryptoCurrency.minConfirms, @@ -304,7 +304,7 @@ class EpiccashWallet extends Bip39Wallet { int index, ) async { Address? address = await getCurrentReceivingAddress(); - EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); + final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); if (address != null) { final splitted = address.value.split('@'); @@ -322,9 +322,10 @@ class EpiccashWallet extends Bip39Wallet { } Future
thisWalletAddress( - int index, EpicBoxConfigModel epicboxConfig) async { + int index, + EpicBoxConfigModel epicboxConfig, + ) async { final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); - // EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); final walletAddress = await epiccash.LibEpiccash.getAddressInfo( wallet: wallet!, @@ -376,7 +377,7 @@ class EpiccashWallet extends Bip39Wallet { level: LogLevel.Info, ); - int nextScannedBlock = await epiccash.LibEpiccash.scanOutputs( + final int nextScannedBlock = await epiccash.LibEpiccash.scanOutputs( wallet: wallet!, startHeight: lastScannedBlock, numberOfBlocks: scanChunkSize, @@ -416,7 +417,7 @@ class EpiccashWallet extends Bip39Wallet { Future _listenToEpicbox() async { Logging.instance.log("STARTING WALLET LISTENER ....", level: LogLevel.Info); final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); - EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); + final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); epiccash.LibEpiccash.startEpicboxListener( wallet: wallet!, epicboxConfig: epicboxConfig.toString(), @@ -430,7 +431,7 @@ class EpiccashWallet extends Bip39Wallet { ); if (Platform.isIOS) { final walletDir = await _currentWalletDirPath(); - var editConfig = jsonDecode(config as String); + final editConfig = jsonDecode(config as String); editConfig["wallet_dir"] = walletDir; config = jsonEncode(editConfig); @@ -440,14 +441,11 @@ class EpiccashWallet extends Bip39Wallet { // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index int _calculateRestoreHeightFrom({required DateTime date}) { - int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; + final int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; const int epicCashFirstBlock = 1565370278; const double overestimateSecondsPerBlock = 61; - int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; - int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock; - //todo: check if print needed - // debugPrint( - // "approximate height: $approximateHeight chosen_seconds: $chosenSeconds"); + final int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; + final int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock; int height = approximateHeight; if (height < 0) { height = 0; @@ -495,7 +493,7 @@ class EpiccashWallet extends Bip39Wallet { await secureStorageInterface.write( key: '${walletId}_epicboxConfig', value: epicboxConfig.toString()); - String name = walletId; + final String name = walletId; await epiccash.LibEpiccash.initializeNewWallet( config: stringConfig, @@ -579,7 +577,7 @@ class EpiccashWallet extends Bip39Wallet { if (!receiverAddress.startsWith("http://") || !receiverAddress.startsWith("https://")) { - bool isEpicboxConnected = await _testEpicboxServer( + final bool isEpicboxConnected = await _testEpicboxServer( epicboxConfig, ); if (!isEpicboxConnected) { @@ -961,7 +959,7 @@ class EpiccashWallet extends Bip39Wallet { ], walletOwns: true, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -1098,7 +1096,7 @@ class EpiccashWallet extends Bip39Wallet { @override Future estimateFeeFor(Amount amount, int feeRate) async { // setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function????? - int currentFee = await _nativeFee( + final int currentFee = await _nativeFee( amount.raw.toInt(), ifErrorEstimateFee: true, ); @@ -1143,13 +1141,13 @@ Future deleteEpicWallet({ final wallet = await secureStore.read(key: '${walletId}_wallet'); String? config = await secureStore.read(key: '${walletId}_config'); if (Platform.isIOS) { - Directory appDir = await StackFileSystem.applicationRootDirectory(); + final Directory appDir = await StackFileSystem.applicationRootDirectory(); final path = "${appDir.path}/epiccash"; final String name = walletId.trim(); final walletDir = '$path/$name'; - var editConfig = jsonDecode(config as String); + final editConfig = jsonDecode(config as String); editConfig["wallet_dir"] = walletDir; config = jsonEncode(editConfig); From 04e27a166e6e9f9b2eae27bf3c64143302b97046 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 May 2024 14:35:35 -0600 Subject: [PATCH 205/272] unused code cleanup --- lib/wallets/wallet/impl/epiccash_wallet.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index d998a43c3..b2e5ad3f1 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -303,21 +303,20 @@ class EpiccashWallet extends Bip39Wallet { Future
_generateAndStoreReceivingAddressForIndex( int index, ) async { - Address? address = await getCurrentReceivingAddress(); - final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); if (address != null) { final splitted = address.value.split('@'); //Check if the address is the same as the current epicbox domain //Since we're only using one epicbpox now this doesn't apply but will be // useful in the future - final encodedConfig = jsonEncode(epicboxConfig); + final epicboxConfig = await getEpicBoxConfig(); if (splitted[1] != epicboxConfig.host) { //Update the address address = await thisWalletAddress(index, epicboxConfig); } } else { + final epicboxConfig = await getEpicBoxConfig(); address = await thisWalletAddress(index, epicboxConfig); } From 1aad524cdb69dd9ac804dc1cce568383fde0fa78 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 May 2024 15:42:21 -0600 Subject: [PATCH 206/272] no need for class wide htt client property, and close the client before assigning a new one --- lib/wallets/wallet/impl/stellar_wallet.dart | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart index edea60fe5..a2ad93083 100644 --- a/lib/wallets/wallet/impl/stellar_wallet.dart +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -46,7 +46,6 @@ class StellarWallet extends Bip39Wallet { // ============== Private ==================================================== stellar.StellarSDK? _stellarSdk; - HttpClient? _httpClient; Future _getBaseFee() async { final fees = await stellarSdk.feeStats.execute(); @@ -55,6 +54,7 @@ class StellarWallet extends Bip39Wallet { void _updateSdk() { final currentNode = getCurrentNode(); + HttpClient? _httpClient; // TODO [prio=med]: refactor out and call before requests in case Tor is enabled/disabled, listen to prefs change, or similar. if (prefs.useTor) { @@ -63,13 +63,21 @@ class StellarWallet extends Bip39Wallet { _httpClient = HttpClient(); SocksTCPClient.assignToHttpClient( - _httpClient!, [ProxySettings(proxyInfo.host, proxyInfo.port)]); - } else { - _httpClient = null; + _httpClient, + [ + ProxySettings( + proxyInfo.host, + proxyInfo.port, + ), + ], + ); } - _stellarSdk = stellar.StellarSDK("${currentNode.host}:${currentNode.port}", - httpClient: _httpClient); + _stellarSdk?.httpClient.close(); + _stellarSdk = stellar.StellarSDK( + "${currentNode.host}:${currentNode.port}", + httpClient: _httpClient, + ); } Future _accountExists(String accountId) async { From 1c47b1e4c95a27b0cf134aadc03ff7ee572df644 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 3 May 2024 16:56:13 -0500 Subject: [PATCH 207/272] Update building.md --- docs/building.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/building.md b/docs/building.md index 603452e55..d7a357076 100644 --- a/docs/building.md +++ b/docs/building.md @@ -103,11 +103,11 @@ Coinlib's native secp256k1 library must be built prior to running Stack Wallet. - 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 bottom options if the first attempt doesn't succeed! +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 @@ -280,6 +280,8 @@ Copy the resulting `dll`s to their respective positions on the Windows host: --> +Frostdart will be built by the Windows host later. + ### Install Flutter on Windows host Install Flutter 3.19.5 on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk. You can also clone https://github.com/flutter/flutter, check out the `3.19.5` tag, and add its `flutter/bin` folder to your PATH. Run `flutter doctor` in PowerShell to confirm its installation. @@ -316,15 +318,23 @@ 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 on Windows. 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. +``` + ### Running Run the following commands: From 65941478b85b17d5f878deea0c3050c0b27560a5 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 May 2024 16:38:12 -0600 Subject: [PATCH 208/272] experimental navigation --- lib/frost_route_generator.dart | 2 +- .../new/create_new_frost_ms_wallet_view.dart | 38 +-------------- .../select_new_frost_import_type_view.dart | 46 +------------------ .../new/steps/frost_create_step_5.dart | 35 +++++++++++++- .../reshare/frost_reshare_step_1c.dart | 2 +- .../reshare/frost_reshare_step_4.dart | 14 +++--- .../reshare/frost_reshare_step_5.dart | 12 ++--- .../send_view/frost_ms/frost_send_view.dart | 7 +-- .../send_steps/frost_send_step_4.dart | 15 +++--- .../frost_ms/frost_ms_options_view.dart | 7 +-- .../complete_reshare_config_view.dart | 5 +- lib/pages/wallet_view/wallet_view.dart | 7 +-- .../wallet_view/sub_widgets/my_wallet.dart | 9 +--- 13 files changed, 66 insertions(+), 133 deletions(-) diff --git a/lib/frost_route_generator.dart b/lib/frost_route_generator.dart index 6de2f6104..a6905a243 100644 --- a/lib/frost_route_generator.dart +++ b/lib/frost_route_generator.dart @@ -39,8 +39,8 @@ final pFrostScaffoldArgs = StateProvider< ({String walletName, FrostCurrency frostCurrency}) info, String? walletId, List stepRoutes, - VoidCallback onSuccess, FrostInterruptionDialogType frostInterruptionDialogType, + NavigatorState parentNav, })?>((ref) => null); abstract class FrostRouteGenerator { diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index c3a4ba47c..620d9df06 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -1,16 +1,10 @@ -import 'dart:async'; - 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/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/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/wallets/crypto_currency/intermediate/frost_currency.dart'; @@ -415,39 +409,9 @@ class _NewFrostMsWalletViewState ), walletId: null, stepRoutes: FrostRouteGenerator.createNewConfigStepRoutes, - onSuccess: () { - // successful completion of steps - if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); - } else { - unawaited( - Navigator.of(context).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: context, - ), - ); - }, frostInterruptionDialogType: FrostInterruptionDialogType.walletCreation, + parentNav: Navigator.of(context), ); await Navigator.of(context).pushNamed( diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index 664d86357..5596534a3 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -1,14 +1,8 @@ -import 'dart:async'; - 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/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/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -147,37 +141,7 @@ class _SelectNewFrostImportTypeViewState ), walletId: null, // no wallet id yet stepRoutes: FrostRouteGenerator.importNewConfigStepRoutes, - onSuccess: () { - // successful completion of steps - if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); - } else { - unawaited( - Navigator.of(context).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: context, - ), - ); - }, + parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.walletCreation, ); @@ -191,13 +155,7 @@ class _SelectNewFrostImportTypeViewState ), walletId: null, // no wallet id yet stepRoutes: FrostRouteGenerator.joinReshareStepRoutes, - onSuccess: () { - // successful completion of steps - ref.read(pFrostMultisigConfig.state).state = null; - ref.read(pFrostStartKeyGenData.state).state = null; - ref.read(pFrostSecretSharesData.state).state = null; - ref.read(pFrostScaffoldArgs.state).state = null; - }, + parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.resharing, ); diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 6bb5e3643..586a1c189 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -4,7 +4,10 @@ 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'; @@ -13,6 +16,7 @@ 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'; @@ -181,7 +185,36 @@ class _FrostCreateStep5State extends ConsumerState { if (mounted) { ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; - ref.read(pFrostScaffoldArgs)!.onSuccess(); + 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( diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart index c7690c45b..adc1c7f5e 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -187,7 +187,7 @@ class _FrostReshareStep1cState extends ConsumerState { info: data.info, walletId: wallet.walletId, stepRoutes: data.stepRoutes, - onSuccess: data.onSuccess, + parentNav: data.parentNav, frostInterruptionDialogType: FrostInterruptionDialogType.resharing, ); diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart index 02530f8e4..2eb7c8f65 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -56,12 +56,14 @@ class _FrostReshareStep4State extends ConsumerState { try { if (amOutgoingParticipant) { ref.read(pFrostResharingData).reset(); - // nav broken on desktop - Navigator.of(context, rootNavigator: !Util.isDesktop).popUntil( - ModalRoute.withName( - Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName, - ), - ); + 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(); diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart index b044b1d93..2171f5ad8 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart @@ -94,12 +94,12 @@ class _FrostReshareStep5State extends ConsumerState { if (mounted) { ref.read(pFrostResharingData).reset(); - - Navigator.of(context).popUntil( - ModalRoute.withName( - _popUntilPath, - ), - ); + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostScaffoldArgs)?.parentNav.popUntil( + ModalRoute.withName( + _popUntilPath, + ), + ); } } } catch (e, s) { diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index a79ee0e6e..133d0afd4 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -136,12 +136,7 @@ class _FrostSendViewState extends ConsumerState { ), walletId: walletId, stepRoutes: FrostRouteGenerator.sendFrostTxStepRoutes, - onSuccess: () { - // successful completion of steps - // TODO ? - - ref.read(pFrostScaffoldArgs.state).state = null; - }, + parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.transactionCreation, ); diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart index 2906af589..2267e7226 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart @@ -224,14 +224,15 @@ class _FrostSendStep4State extends ConsumerState { if (context.mounted) { if (txData != null) { + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; ref.read(pFrostTxData.state).state = txData; - Navigator.of(context).popUntil( - ModalRoute.withName( - Util.isDesktop - ? MyStackView.routeName - : WalletView.routeName, - ), - ); + ref.read(pFrostScaffoldArgs)!.parentNav.popUntil( + ModalRoute.withName( + Util.isDesktop + ? MyStackView.routeName + : WalletView.routeName, + ), + ); } } } catch (e, s) { diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index f93a069b0..c18d78477 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -159,12 +159,7 @@ class FrostMSWalletOptionsView extends ConsumerWidget { ), walletId: wallet.walletId, stepRoutes: FrostRouteGenerator.importReshareStepRoutes, - onSuccess: () { - // successful completion of steps - // TODO - - ref.read(pFrostScaffoldArgs.state).state = null; - }, + parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.resharing, ); diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart index 1b3b9a9fd..7d8a61bcf 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart @@ -136,10 +136,7 @@ class _CompleteReshareConfigViewState ), walletId: wallet.walletId, stepRoutes: FrostRouteGenerator.initiateReshareStepRoutes, - onSuccess: () { - // successful completion of steps - // TODO - }, + parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.resharing, ); diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index c7b30d81c..a211cddd8 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -368,12 +368,7 @@ class _WalletViewState extends ConsumerState { ), walletId: walletId, stepRoutes: FrostRouteGenerator.signFrostTxStepRoutes, - onSuccess: () { - // successful completion of steps - // TODO ? - - ref.read(pFrostScaffoldArgs.state).state = null; - }, + parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.transactionCreation, ); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 0b7875b34..da7cd15e9 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -100,14 +100,7 @@ class _MyWalletState extends ConsumerState { walletId: widget.walletId, stepRoutes: FrostRouteGenerator .signFrostTxStepRoutes, - onSuccess: () { - // successful completion of steps - // TODO ? - - ref - .read(pFrostScaffoldArgs.state) - .state = null; - }, + parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType .transactionCreation, From dd45c870f69ffa50cfea39ea7b460c794782034c Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 May 2024 18:04:57 -0600 Subject: [PATCH 209/272] untested stellar tor listener --- lib/wallets/wallet/impl/stellar_wallet.dart | 118 +++++++++++++++----- 1 file changed, 90 insertions(+), 28 deletions(-) diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart index a2ad93083..657fd7676 100644 --- a/lib/wallets/wallet/impl/stellar_wallet.dart +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; import 'package:socks5_proxy/socks.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; @@ -11,6 +12,9 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart' import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; @@ -23,11 +27,47 @@ import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart' as stellar; class StellarWallet extends Bip39Wallet { - StellarWallet(CryptoCurrencyNetwork network) : super(Stellar(network)); + StellarWallet(CryptoCurrencyNetwork network) : super(Stellar(network)) { + final bus = GlobalEventBus.instance; - stellar.StellarSDK get stellarSdk { - if (_stellarSdk == null) { - _updateSdk(); + // Listen for tor status changes. + _torStatusListener = bus.on().listen( + (event) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + if (!_torConnectingLock.isLocked) { + await _torConnectingLock.acquire(); + } + _requireMutex = true; + break; + + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + _requireMutex = false; + break; + } + }, + ); + + // Listen for tor preference changes. + _torPreferenceListener = bus.on().listen( + (event) async { + _stellarSdk?.httpClient.close(); + _stellarSdk = null; + }, + ); + } + + Future get stellarSdk async { + if (_requireMutex) { + await _torConnectingLock.protect(() async { + _stellarSdk ??= _getFreshSdk(); + }); + } else { + _stellarSdk ??= _getFreshSdk(); } return _stellarSdk!; } @@ -44,19 +84,32 @@ class StellarWallet extends Bip39Wallet { } // ============== Private ==================================================== + // add finalizer to cancel stream subscription when all references to an + // instance of this becomes inaccessible + final _ = Finalizer( + (p0) { + p0._torPreferenceListener?.cancel(); + p0._torStatusListener?.cancel(); + }, + ); + + StreamSubscription? _torStatusListener; + StreamSubscription? _torPreferenceListener; + + final Mutex _torConnectingLock = Mutex(); + bool _requireMutex = false; stellar.StellarSDK? _stellarSdk; Future _getBaseFee() async { - final fees = await stellarSdk.feeStats.execute(); + final fees = await (await stellarSdk).feeStats.execute(); return int.parse(fees.lastLedgerBaseFee); } - void _updateSdk() { + stellar.StellarSDK _getFreshSdk() { final currentNode = getCurrentNode(); HttpClient? _httpClient; - // TODO [prio=med]: refactor out and call before requests in case Tor is enabled/disabled, listen to prefs change, or similar. if (prefs.useTor) { final ({InternetAddress host, int port}) proxyInfo = TorService.sharedInstance.getProxyInfo(); @@ -73,8 +126,7 @@ class StellarWallet extends Bip39Wallet { ); } - _stellarSdk?.httpClient.close(); - _stellarSdk = stellar.StellarSDK( + return stellar.StellarSDK( "${currentNode.host}:${currentNode.port}", httpClient: _httpClient, ); @@ -84,7 +136,8 @@ class StellarWallet extends Bip39Wallet { bool exists = false; try { - final receiverAccount = await stellarSdk.accounts.account(accountId); + final receiverAccount = + await (await stellarSdk).accounts.account(accountId); if (receiverAccount.accountId != "") { exists = true; } @@ -191,7 +244,8 @@ class StellarWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { final senderKeyPair = await _getSenderKeyPair(index: 0); - final sender = await stellarSdk.accounts.account(senderKeyPair.accountId); + final sender = + await (await stellarSdk).accounts.account(senderKeyPair.accountId); final address = txData.recipients!.first.address; final amountToSend = txData.recipients!.first.amount; @@ -229,7 +283,7 @@ class StellarWallet extends Bip39Wallet { transaction.sign(senderKeyPair, stellarNetwork); try { - final response = await stellarSdk.submitTransaction(transaction); + final response = await (await stellarSdk).submitTransaction(transaction); if (!response.success) { throw Exception("${response.extras?.resultCodes?.transactionResultCode}" " ::: ${response.extras?.resultCodes?.operationsResultCodes}"); @@ -256,7 +310,7 @@ class StellarWallet extends Bip39Wallet { @override Future get fees async { - int fee = await _getBaseFee(); + final int fee = await _getBaseFee(); return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 1, @@ -294,7 +348,8 @@ class StellarWallet extends Bip39Wallet { stellar.AccountResponse accountResponse; try { - accountResponse = await stellarSdk.accounts + accountResponse = await (await stellarSdk) + .accounts .account((await getCurrentReceivingAddress())!.value) .onError((error, stackTrace) => throw error!); } catch (e) { @@ -315,7 +370,7 @@ class StellarWallet extends Bip39Wallet { } } - for (stellar.Balance balance in accountResponse.balances) { + for (final stellar.Balance balance in accountResponse.balances) { switch (balance.assetType) { case stellar.Asset.TYPE_NATIVE: final swBalance = Balance( @@ -352,7 +407,8 @@ class StellarWallet extends Bip39Wallet { @override Future updateChainHeight() async { try { - final height = await stellarSdk.ledgers + final height = await (await stellarSdk) + .ledgers .order(stellar.RequestBuilderOrder.DESC) .limit(1) .execute() @@ -370,7 +426,8 @@ class StellarWallet extends Bip39Wallet { @override Future updateNode() async { - _updateSdk(); + _stellarSdk?.httpClient.close(); + _stellarSdk = _getFreshSdk(); } @override @@ -378,10 +435,11 @@ class StellarWallet extends Bip39Wallet { try { final myAddress = (await getCurrentReceivingAddress())!; - List transactionList = []; + final List transactionList = []; stellar.Page payments; try { - payments = await stellarSdk.payments + payments = await (await stellarSdk) + .payments .forAccount(myAddress.value) .order(stellar.RequestBuilderOrder.DESC) .execute(); @@ -401,7 +459,7 @@ class StellarWallet extends Bip39Wallet { rethrow; } } - for (stellar.OperationResponse response in payments.records!) { + for (final stellar.OperationResponse response in payments.records!) { // PaymentOperationResponse por; if (response is stellar.PaymentOperationResponse) { final por = response; @@ -431,7 +489,8 @@ class StellarWallet extends Bip39Wallet { final List outputs = []; final List inputs = []; - OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + final OutputV2 output = + OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), addresses: [ @@ -439,7 +498,7 @@ class StellarWallet extends Bip39Wallet { ], walletOwns: addressTo == myAddress.value, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -459,8 +518,9 @@ class StellarWallet extends Bip39Wallet { int height = 0; //Query the transaction linked to the payment, // por.transaction returns a null sometimes - stellar.TransactionResponse tx = - await stellarSdk.transactions.transaction(por.transactionHash!); + final stellar.TransactionResponse tx = await (await stellarSdk) + .transactions + .transaction(por.transactionHash!); if (tx.hash.isNotEmpty) { fee = tx.feeCharged!; @@ -511,7 +571,8 @@ class StellarWallet extends Bip39Wallet { final List outputs = []; final List inputs = []; - OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + final OutputV2 output = + OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), addresses: [ @@ -520,7 +581,7 @@ class StellarWallet extends Bip39Wallet { ], walletOwns: caor.sourceAccount! == myAddress.value, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -541,8 +602,9 @@ class StellarWallet extends Bip39Wallet { int fee = 0; int height = 0; - final tx = - await stellarSdk.transactions.transaction(caor.transactionHash!); + final tx = await (await stellarSdk) + .transactions + .transaction(caor.transactionHash!); if (tx.hash.isNotEmpty) { fee = tx.feeCharged!; height = tx.ledger; From aa5bdad3948709730d3c3007081928601359b707 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 3 May 2024 19:24:38 -0500 Subject: [PATCH 210/272] revert to official package with tor support merged --- pubspec.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index fb5a9f31c..86531ddad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -156,10 +156,7 @@ dependencies: desktop_drop: ^0.4.1 nanodart: ^2.0.0 basic_utils: ^5.5.4 - stellar_flutter_sdk: # ^1.5.3 - git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. - url: https://github.com/cypherstack/stellar_flutter_sdk.git - ref: eca1d730e952cf6a6d64502f977cfc03876b75d4 # tor-backport branch (based on 1.5.3). + stellar_flutter_sdk: ^1.7.8 bip340: ^0.2.0 # tezart: ^2.0.5 tezart: From cd6e89470820ad1d82f143b51a75eb413194ab8f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 3 May 2024 20:04:31 -0500 Subject: [PATCH 211/272] update tezart's dio dep --- pubspec.lock | 23 +++++++++++------------ pubspec.yaml | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index bb9486dd4..b07d3ab62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -502,10 +502,10 @@ packages: dependency: transitive description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.4.3+1" dropdown_button2: dependency: "direct main" description: @@ -1412,10 +1412,10 @@ packages: dependency: transitive description: name: pretty_dio_logger - sha256: "948f7eeb36e7aa0760b51c1a8e3331d4b21e36fabd39efca81f585ed93893544" + sha256: "00b80053063935cf9a6190da344c5373b9d0e92da4c944c878ff2fbef0ef6dc2" url: "https://pub.dev" source: hosted - version: "1.2.0-beta-1" + version: "1.3.1" process: dependency: transitive description: @@ -1659,12 +1659,11 @@ packages: stellar_flutter_sdk: dependency: "direct main" description: - path: "." - ref: eca1d730e952cf6a6d64502f977cfc03876b75d4 - resolved-ref: eca1d730e952cf6a6d64502f977cfc03876b75d4 - url: "https://github.com/cypherstack/stellar_flutter_sdk.git" - source: git - version: "1.5.3" + name: stellar_flutter_sdk + sha256: "574e8f40a1a1a9b18a735272196c8d3c8168a669efc8460a4d5d6f45151e8dce" + url: "https://pub.dev" + source: hosted + version: "1.7.8" stream_channel: dependency: "direct main" description: @@ -1741,8 +1740,8 @@ packages: dependency: "direct main" description: path: "." - ref: "1fb2669e2b530367a449217e952f220d5e667043" - resolved-ref: "1fb2669e2b530367a449217e952f220d5e667043" + ref: f31f8f857665d85338824ae171aba4c629c3ba6f + resolved-ref: f31f8f857665d85338824ae171aba4c629c3ba6f url: "https://github.com/cypherstack/tezart.git" source: git version: "2.0.5" diff --git a/pubspec.yaml b/pubspec.yaml index 86531ddad..5985ffdf0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -162,7 +162,7 @@ dependencies: tezart: git: url: https://github.com/cypherstack/tezart.git - ref: 1fb2669e2b530367a449217e952f220d5e667043 + ref: f31f8f857665d85338824ae171aba4c629c3ba6f socks5_proxy: ^1.0.3+dev.3 convert: ^3.1.1 flutter_hooks: ^0.20.3 From 2432629d48b77008ba3e4fd97c831c14792b05cf Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 3 May 2024 20:31:04 -0500 Subject: [PATCH 212/272] tezart dio fix --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5985ffdf0..971751045 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -162,7 +162,7 @@ dependencies: tezart: git: url: https://github.com/cypherstack/tezart.git - ref: f31f8f857665d85338824ae171aba4c629c3ba6f + ref: 13fa937ea9a9fc34caf047e068df9535f65c27ad socks5_proxy: ^1.0.3+dev.3 convert: ^3.1.1 flutter_hooks: ^0.20.3 From 236e7d87fb260366c6eff81c688875c26e7ef5bc Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 May 2024 20:08:29 -0600 Subject: [PATCH 213/272] tagged libsecret version --- scripts/linux/build_secure_storage_deps.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index e63e38665..aff3097dc 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -1,6 +1,7 @@ #!/bin/bash LINUX_DIRECTORY=$(pwd) JSONCPP_TAG=1.7.4 +LIBSECRET_TAG=0.21.4 mkdir -p build # Build JsonCPP @@ -24,8 +25,9 @@ cd "$LINUX_DIRECTORY" || exit 1 #pip3 install --user meson markdown tomli --upgrade # pip3 install --user gi-docgen cd build || exit 1 -git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret +git -C libsecret pull origin $LIBSECRET_TAG || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret cd libsecret || exit 1 +git checkout $LIBSECRET_TAG if ! [ -x "$(command -v meson)" ]; then echo 'Error: meson is not installed.' >&2 exit 1 From 981b7c86d40069e176ca99ef9d496ca9ab3155c0 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 3 May 2024 18:56:27 -0600 Subject: [PATCH 214/272] desktop confirm send pw field enable submit on enter pressed --- .../sub_widgets/desktop_auth_send.dart | 92 +++++++++++-------- pubspec.lock | 4 +- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart index b574d377e..aba28300b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -26,9 +26,9 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; class DesktopAuthSend extends ConsumerStatefulWidget { const DesktopAuthSend({ - Key? key, + super.key, required this.coin, - }) : super(key: key); + }); final Coin coin; @@ -43,11 +43,52 @@ class _DesktopAuthSendState extends ConsumerState { bool hidePassword = true; bool _confirmEnabled = false; + bool _lock = false; - Future verifyPassphrase() async { - return await ref - .read(storageCryptoHandlerProvider) - .verifyPassphrase(passwordController.text); + Future _confirmPressed() async { + if (_lock) { + return; + } + _lock = true; + + try { + unawaited( + showDialog( + context: context, + builder: (context) => const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future.delayed(const Duration(seconds: 1)); + + final passwordIsValid = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (mounted) { + Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(passwordIsValid); + await Future.delayed( + const Duration( + milliseconds: 100, + ), + ); + } + } finally { + _lock = false; + } } @override @@ -108,6 +149,12 @@ class _DesktopAuthSendState extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_confirmEnabled) { + _confirmPressed(); + } + }, decoration: standardInputDecoration( "Enter password", passwordFocusNode, @@ -173,38 +220,7 @@ class _DesktopAuthSendState extends ConsumerState { enabled: _confirmEnabled, label: "Confirm", buttonHeight: ButtonHeight.l, - onPressed: () async { - unawaited( - showDialog( - context: context, - builder: (context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: const [ - LoadingIndicator( - width: 200, - height: 200, - ), - ], - ), - ), - ); - - await Future.delayed(const Duration(seconds: 1)); - - final passwordIsValid = await verifyPassphrase(); - - if (mounted) { - Navigator.of(context).pop(); - Navigator.of( - context, - rootNavigator: true, - ).pop(passwordIsValid); - await Future.delayed(const Duration( - milliseconds: 100, - )); - } - }, + onPressed: _confirmPressed, ), ), ], diff --git a/pubspec.lock b/pubspec.lock index b07d3ab62..7681556d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1740,8 +1740,8 @@ packages: dependency: "direct main" description: path: "." - ref: f31f8f857665d85338824ae171aba4c629c3ba6f - resolved-ref: f31f8f857665d85338824ae171aba4c629c3ba6f + ref: "13fa937ea9a9fc34caf047e068df9535f65c27ad" + resolved-ref: "13fa937ea9a9fc34caf047e068df9535f65c27ad" url: "https://github.com/cypherstack/tezart.git" source: git version: "2.0.5" From 2aca5f472b95328a5ea71064748ca6aab4a55bd9 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 6 May 2024 11:50:14 -0600 Subject: [PATCH 215/272] frost sign icon --- .../components/icons/frost_sign_nav_icon.dart | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart index a7a81c20f..b91542a8e 100644 --- a/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart +++ b/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart @@ -8,29 +8,36 @@ * */ -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/themes/theme_providers.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; class FrostSignNavIcon extends ConsumerWidget { const FrostSignNavIcon({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return SvgPicture.file( - File( - ref.watch( - themeProvider.select( - // TODO: [prio=high] update themes with icon asset - (value) => value.assets.stackIcon, - ), + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .bottomNavIconIcon + .withOpacity(0.4), + borderRadius: BorderRadius.circular( + 24, + ), + ), + child: Padding( + padding: const EdgeInsets.all(6.0), + child: SvgPicture.asset( + Assets.svg.pencil, + width: 12, + height: 12, + color: Theme.of(context).extension()!.bottomNavIconIcon, ), ), - width: 24, - height: 24, ); } } From accc9a9b4e3014c963b367e54988eedadfd52aab Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 6 May 2024 13:26:31 -0600 Subject: [PATCH 216/272] use `final` --- lib/services/wallets.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 729b863ee..2b3ddcbbf 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -156,10 +156,11 @@ class Wallets { return; } - List> walletInitFutures = []; - List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = []; + final List> walletInitFutures = []; + final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = + []; - List walletIdsToEnableAutoSync = []; + final List walletIdsToEnableAutoSync = []; bool shouldAutoSyncAll = false; switch (prefs.syncType) { case SyncingType.currentWalletOnly: @@ -238,10 +239,11 @@ class Wallets { List wallets, bool isDesktop, ) async { - List> walletInitFutures = []; - List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = []; + final List> walletInitFutures = []; + final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = + []; - List walletIdsToEnableAutoSync = []; + final List walletIdsToEnableAutoSync = []; bool shouldAutoSyncAll = false; switch (prefs.syncType) { case SyncingType.currentWalletOnly: From 309a4830265b4f986e2682696b3664da77dd7078 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 6 May 2024 18:25:10 -0600 Subject: [PATCH 217/272] use `final` --- lib/wallets/wallet/impl/firo_wallet.dart | 13 +++++++------ .../electrumx_interface.dart | 18 +++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index ae02c5292..17e08ff3d 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -62,14 +62,15 @@ class FiroWallet extends Bip39HDWallet @override Future updateTransactions() async { - List
allAddressesOld = await fetchAddressesForElectrumXScan(); + final List
allAddressesOld = + await fetchAddressesForElectrumXScan(); - Set receivingAddresses = allAddressesOld + final Set receivingAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.receiving) .map((e) => convertAddressString(e.value)) .toSet(); - Set changeAddresses = allAddressesOld + final Set changeAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.change) .map((e) => convertAddressString(e.value)) .toSet(); @@ -98,7 +99,7 @@ class FiroWallet extends Bip39HDWallet } } - List> allTransactions = []; + final List> allTransactions = []; // some lelantus transactions aren't fetched via wallet addresses so they // will never show as confirmed in the gui. @@ -177,7 +178,7 @@ class FiroWallet extends Bip39HDWallet bool isMint = false; bool isJMint = false; bool isSparkMint = false; - bool isMasterNodePayment = false; + final bool isMasterNodePayment = false; final bool isSparkSpend = txData["type"] == 9 && txData["version"] == 3; final bool isMySpark = sparkTxids.contains(txData["txid"] as String); @@ -555,7 +556,7 @@ class FiroWallet extends Bip39HDWallet Map? jsonTX, String? utxoOwnerAddress, ) async { - bool blocked = false; + final bool blocked = false; String? blockedReason; // // if (jsonTX != null) { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 80e6f89ef..3ceafd5ab 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -193,8 +193,8 @@ mixin ElectrumXInterface on Bip39HDWallet { .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [recipientAddress]; - List recipientsAmtArray = [satoshiAmountToSend]; + final List recipientsArray = [recipientAddress]; + final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); @@ -325,7 +325,7 @@ mixin ElectrumXInterface on Bip39HDWallet { feeForOneOutput + cryptoCurrency.dustLimit.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. - int changeOutputSize = + final int changeOutputSize = satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > cryptoCurrency.dustLimit satoshis, we perform the mechanics required to properly generate and use a new @@ -370,7 +370,7 @@ mixin ElectrumXInterface on Bip39HDWallet { // make sure minimum fee is accurate if that is being used if (txn.vSize! - feeBeingPaid == 1) { - int changeOutputSize = + final int changeOutputSize = satoshisBeingUsed - satoshiAmountToSend - txn.vSize!; feeBeingPaid = satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; @@ -526,7 +526,7 @@ mixin ElectrumXInterface on Bip39HDWallet { List utxosToUse, ) async { // return data - List signingData = []; + final List signingData = []; try { // Populating the addresses to check @@ -879,7 +879,7 @@ mixin ElectrumXInterface on Bip39HDWallet { DerivePathType type, int chain, ) async { - List
addressArray = []; + final List
addressArray = []; int gapCounter = 0; int highestIndexWithHistory = 0; @@ -891,7 +891,7 @@ mixin ElectrumXInterface on Bip39HDWallet { "index: $index, \t GapCounter $chain ${type.name}: $gapCounter", level: LogLevel.Info); - List txCountCallArgs = []; + final List txCountCallArgs = []; for (int j = 0; j < txCountBatchSize; j++) { final derivePath = cryptoCurrency.constructDerivePath( @@ -960,7 +960,7 @@ mixin ElectrumXInterface on Bip39HDWallet { DerivePathType type, int chain, ) async { - List
addressArray = []; + final List
addressArray = []; int gapCounter = 0; int index = 0; for (; @@ -1023,7 +1023,7 @@ mixin ElectrumXInterface on Bip39HDWallet { Iterable allAddresses, ) async { try { - List> allTxHashes = []; + final List> allTxHashes = []; if (serverCanBatch) { final Map>> batches = {}; From d747347414c348d4d4edde14efd2070e0c7989b7 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 6 May 2024 18:25:31 -0600 Subject: [PATCH 218/272] speed up initial load time --- lib/services/wallets.dart | 130 +++++++++++++++++- lib/wallets/wallet/impl/firo_wallet.dart | 6 +- .../electrumx_interface.dart | 21 ++- 3 files changed, 147 insertions(+), 10 deletions(-) diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 2b3ddcbbf..7dab60dbd 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:isar/isar.dart'; @@ -16,7 +18,6 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; @@ -134,6 +135,11 @@ class Wallets { } Future load(Prefs prefs, MainDB mainDB) async { + // return await _loadV1(prefs, mainDB); + return await _loadV2(prefs, mainDB); + } + + Future _loadV1(Prefs prefs, MainDB mainDB) async { if (hasLoaded) { return; } @@ -186,8 +192,8 @@ class Wallets { if (isVerified) { // TODO: integrate this into the new wallets somehow? // requires some thinking - final txTracker = - TransactionNotificationTracker(walletId: walletInfo.walletId); + // final txTracker = + // TransactionNotificationTracker(walletId: walletInfo.walletId); final wallet = await Wallet.load( walletId: walletInfo.walletId, @@ -234,6 +240,124 @@ class Wallets { } } + Future _loadV2(Prefs prefs, MainDB mainDB) async { + if (hasLoaded) { + return; + } + hasLoaded = true; + + // clear out any wallet hive boxes where the wallet was deleted in previous app run + for (final walletId in DB.instance + .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { + await mainDB.isar.writeTxn(() async => await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll()); + } + // clear list + await DB.instance + .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + + final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); + if (walletInfoList.isEmpty) { + return; + } + + final List> walletIDInitFutures = []; + final List> deleteFutures = []; + final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = + []; + + final List walletIdsToEnableAutoSync = []; + bool shouldAutoSyncAll = false; + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + // do nothing as this will be set when going into a wallet from the main screen + break; + case SyncingType.selectedWalletsAtStartup: + walletIdsToEnableAutoSync.addAll(prefs.walletIdsSyncOnStartup); + break; + case SyncingType.allWalletsOnStartup: + shouldAutoSyncAll = true; + break; + } + + for (final walletInfo in walletInfoList) { + try { + final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); + Logging.instance.log( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + level: LogLevel.Info, + ); + + if (isVerified) { + // TODO: integrate this into the new wallets somehow? + // requires some thinking + // final txTracker = + // TransactionNotificationTracker(walletId: walletInfo.walletId); + + final walletIdCompleter = Completer(); + + walletIDInitFutures.add(walletIdCompleter.future); + + await Wallet.load( + walletId: walletInfo.walletId, + mainDB: mainDB, + secureStorageInterface: nodeService.secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + ).then((wallet) { + if (wallet is CwBasedInterface) { + // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); + + walletIdCompleter.complete("dummy_ignore"); + } else { + walletIdCompleter.complete(wallet.walletId); + } + + _wallets[wallet.walletId] = wallet; + }); + } else { + // wallet creation was not completed by user so we remove it completely + deleteFutures.add(_deleteWallet(walletInfo.walletId)); + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Fatal); + continue; + } + } + + final asyncWalletIds = await Future.wait(walletIDInitFutures); + asyncWalletIds.removeWhere((e) => e == "dummy_ignore"); + + final List> walletInitFutures = asyncWalletIds + .map( + (id) => _wallets[id]!.init().then( + (_) { + if (shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(id)) { + _wallets[id]!.shouldAutoSync = true; + } + }, + ), + ) + .toList(); + + if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { + unawaited(Future.wait([ + _initLinearly(walletsToInitLinearly), + ...walletInitFutures, + ])); + } else if (walletInitFutures.isNotEmpty) { + unawaited(Future.wait(walletInitFutures)); + } else if (walletsToInitLinearly.isNotEmpty) { + unawaited(_initLinearly(walletsToInitLinearly)); + } + + // finally await any deletions that haven't completed yet + await Future.wait(deleteFutures); + } + Future loadAfterStackRestore( Prefs prefs, List wallets, diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 17e08ff3d..13d9f324e 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -633,9 +633,11 @@ class FiroWallet extends Bip39HDWallet level: LogLevel.Info, ); + final canBatch = await serverCanBatch; + for (final type in cryptoCurrency.supportedDerivationPathTypes) { receiveFutures.add( - serverCanBatch + canBatch ? checkGapsBatched( txCountBatchSize, root, @@ -657,7 +659,7 @@ class FiroWallet extends Bip39HDWallet ); for (final type in cryptoCurrency.supportedDerivationPathTypes) { changeFutures.add( - serverCanBatch + canBatch ? checkGapsBatched( txCountBatchSize, root, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 3ceafd5ab..6b0f46522 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -35,11 +35,20 @@ mixin ElectrumXInterface on Bip39HDWallet { static const _kServerBatchCutoffVersion = [1, 6]; List? _serverVersion; - bool get serverCanBatch { + Future get serverCanBatch async { // Firo server added batching without incrementing version number... if (cryptoCurrency is Firo) { return true; } + + try { + _serverVersion ??= _parseServerVersion((await electrumXClient + .getServerFeatures() + .timeout(const Duration(seconds: 2)))["server_version"] as String); + } catch (_) { + // ignore failure as it doesn't matter + } + if (_serverVersion != null && _serverVersion!.length > 2) { if (_serverVersion![0] > _kServerBatchCutoffVersion[0]) { return true; @@ -1025,7 +1034,7 @@ mixin ElectrumXInterface on Bip39HDWallet { try { final List> allTxHashes = []; - if (serverCanBatch) { + if (await serverCanBatch) { final Map>> batches = {}; final Map> batchIndexToAddressListMap = {}; const batchSizeMax = 100; @@ -1363,9 +1372,11 @@ mixin ElectrumXInterface on Bip39HDWallet { level: LogLevel.Info, ); + final canBatch = await serverCanBatch; + for (final type in cryptoCurrency.supportedDerivationPathTypes) { receiveFutures.add( - serverCanBatch + canBatch ? checkGapsBatched( txCountBatchSize, root, @@ -1387,7 +1398,7 @@ mixin ElectrumXInterface on Bip39HDWallet { ); for (final type in cryptoCurrency.supportedDerivationPathTypes) { changeFutures.add( - serverCanBatch + canBatch ? checkGapsBatched( txCountBatchSize, root, @@ -1510,7 +1521,7 @@ mixin ElectrumXInterface on Bip39HDWallet { try { final fetchedUtxoList = >>[]; - if (serverCanBatch) { + if (await serverCanBatch) { final Map>> batchArgs = {}; const batchSizeMax = 10; int batchNumber = 0; From 8b52bf8beb5db06a96365907d13bf8f71f6910ca Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 6 May 2024 20:05:31 -0500 Subject: [PATCH 219/272] Flutter 3.19.6, install all Rust toolchains at once, and formatting It's easier to just install them all at once. --- docs/building.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/building.md b/docs/building.md index d7a357076..397fd225b 100644 --- a/docs/building.md +++ b/docs/building.md @@ -12,7 +12,7 @@ Here you will find instructions on how to install the necessary tools for buildi 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 beta (3.19.0-0.1.pre) by following these instructions: 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.0-0.1.pre` tag, and add its `flutter/bin` folder to your PATH. Run `flutter doctor` in a terminal to confirm its installation. +Install Flutter 3.19.6 by following these instructions: 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. 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: @@ -58,10 +58,7 @@ 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.73.0 # For cargo-ndk. -rustup install 1.72.0 # For frostdart & tor. -rustup install 1.73.0 # For cargo-ndk. -rustup install 1.67.1 # For flutter_libepiccash. +rustup install 1.67.1 1.72.0 1.73.0 rustup default 1.67.1 ``` @@ -107,7 +104,7 @@ Coinlib's native secp256k1 library must be built prior to running Stack Wallet. 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! +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 @@ -283,7 +280,7 @@ Copy the resulting `dll`s to their respective positions on the Windows host: Frostdart will be built by the Windows host later. ### Install Flutter on Windows host -Install Flutter 3.19.5 on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk. You can also clone https://github.com/flutter/flutter, check out the `3.19.5` tag, and add its `flutter/bin` folder to your PATH. Run `flutter doctor` in PowerShell to confirm its installation. +Install Flutter 3.19.6 on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows/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. 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: From 0cb85e5264cfc6d7efc40609c86d5396dbf3df4f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 6 May 2024 20:25:58 -0500 Subject: [PATCH 220/272] Remove duplicate Flutter section and add example scripts --- docs/building.md | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/docs/building.md b/docs/building.md index 397fd225b..0d88b1bb2 100644 --- a/docs/building.md +++ b/docs/building.md @@ -9,10 +9,22 @@ Here you will find instructions on how to install the necessary tools for buildi - 100 GB of storage ## Linux host + 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 these instructions: 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. Run `flutter doctor` in a terminal to confirm its installation. +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: @@ -38,17 +50,7 @@ The following *may* be needed for Android studio: sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-1.0:i386 ``` -### Flutter - -Flutter and the Dart SDK should have been set up by Android studio, but if running `flutter` doesn't work (try `flutter doctor`, too), follow the [guide to install Flutter on any of their supported platforms](https://docs.flutter.dev/get-started/install) or: - - `git clone https://github.com/flutter/flutter` somewhere it can live (`/var`, `/opt`, `~`) - - `git checkout 3.16.0` after navigating into the `flutter` directory, and - - add `flutter/bin` to your PATH (on Ubuntu, add `PATH=$PATH:path/to/flutter/bin` to `~/.profile`). - -Run `flutter doctor` to install any missing dependencies and review and agree to any license agreements. - ### 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 @@ -221,7 +223,6 @@ cd scripts/macos ``` ### 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 @@ -247,6 +248,7 @@ 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. @@ -280,7 +282,18 @@ Copy the resulting `dll`s to their respective positions on the Windows host: Frostdart will be built by the Windows host later. ### Install Flutter on Windows host -Install Flutter 3.19.6 on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows/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. Run `flutter doctor` in PowerShell to confirm its installation. +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: From 0ac8885aa81addb22ef5557131f0bbe72c2aebec Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 May 2024 09:49:12 -0600 Subject: [PATCH 221/272] fix eth token abi endpoint and some logic clean up --- lib/services/ethereum/ethereum_api.dart | 2 +- .../impl/sub_wallets/eth_token_wallet.dart | 131 ++++++------------ .../sub_widgets/wallet_info_row_balance.dart | 11 +- 3 files changed, 50 insertions(+), 94 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 3931b4573..b9ac2224b 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -686,7 +686,7 @@ abstract class EthereumAPI { try { final response = await client.get( url: Uri.parse( - "$stackBaseServer/abis?addrs=$contractAddress&verbose=true", + "$stackBaseServer/abis?addrs=$contractAddress", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.getProxyInfo() diff --git a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart index 8244c5fd9..d5c4bc31d 100644 --- a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart +++ b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart @@ -92,16 +92,8 @@ class EthTokenWallet extends Wallet { ); } - // String? mnemonicString = await ethWallet.getMnemonic(); - // - // //Get private key for given mnemonic - // String privateKey = getPrivateKey( - // mnemonicString, - // (await ethWallet.getMnemonicPassphrase()), - // ); - // _credentials = web3dart.EthPrivateKey.fromHex(privateKey); - try { + // try parse abi and extract transfer function _deployedContract = web3dart.DeployedContract( ContractAbiExtensions.fromJsonList( jsonList: tokenContract.abi!, @@ -109,90 +101,51 @@ class EthTokenWallet extends Wallet { ), contractAddress, ); - } catch (_) { - rethrow; - } - - try { _sendFunction = _deployedContract.function('transfer'); } catch (_) { - //==================================================================== - // final list = List>.from( - // jsonDecode(tokenContract.abi!) as List); - // final functionNames = list.map((e) => e["name"] as String); - // - // if (!functionNames.contains("balanceOf")) { - // list.add( - // { - // "encoding": "0x70a08231", - // "inputs": [ - // {"name": "account", "type": "address"} - // ], - // "name": "balanceOf", - // "outputs": [ - // {"name": "val_0", "type": "uint256"} - // ], - // "signature": "balanceOf(address)", - // "type": "function" - // }, - // ); - // } - // - // if (!functionNames.contains("transfer")) { - // list.add( - // { - // "encoding": "0xa9059cbb", - // "inputs": [ - // {"name": "dst", "type": "address"}, - // {"name": "rawAmount", "type": "uint256"} - // ], - // "name": "transfer", - // "outputs": [ - // {"name": "val_0", "type": "bool"} - // ], - // "signature": "transfer(address,uint256)", - // "type": "function" - // }, - // ); - // } - //-------------------------------------------------------------------- - //==================================================================== - - // function not found so likely a proxy so we need to fetch the impl - //==================================================================== - // final updatedToken = tokenContract.copyWith(abi: jsonEncode(list)); - // // Store updated contract - // final id = await MainDB.instance.putEthContract(updatedToken); - // _tokenContract = updatedToken..id = id; - //-------------------------------------------------------------------- - final contractAddressResponse = - await EthereumAPI.getProxyTokenImplementationAddress( - contractAddress.hex); - - if (contractAddressResponse.value != null) { - _tokenContract = await _updateTokenABI( - forContract: tokenContract, - usingContractAddress: contractAddressResponse.value!, - ); - } else { - throw contractAddressResponse.exception!; - } - //==================================================================== - } - - try { - _deployedContract = web3dart.DeployedContract( - ContractAbiExtensions.fromJsonList( - jsonList: tokenContract.abi!, - name: tokenContract.name, - ), - contractAddress, + // some failure so first try to make sure we have the latest abi + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddress.hex, ); - } catch (_) { - rethrow; - } - _sendFunction = _deployedContract.function('transfer'); + try { + // try again to parse abi and extract transfer function + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + _sendFunction = _deployedContract.function('transfer'); + } catch (_) { + // if it fails again we check if there is a proxy token impl and + // then try one last time to update and parse the abi + final contractAddressResponse = + await EthereumAPI.getProxyTokenImplementationAddress( + contractAddress.hex); + + if (contractAddressResponse.value != null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddressResponse.value!, + ); + } else { + throw contractAddressResponse.exception!; + } + + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + + _sendFunction = _deployedContract.function('transfer'); + } + } } catch (e, s) { Logging.instance.log( "$runtimeType wallet failed init(): $e\n$s", diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart index aef410d47..55a897255 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart @@ -22,10 +22,10 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; class WalletInfoRowBalance extends ConsumerWidget { const WalletInfoRowBalance({ - Key? key, + super.key, required this.walletId, this.contractAddress, - }) : super(key: key); + }); final String walletId; final String? contractAddress; @@ -45,8 +45,11 @@ class WalletInfoRowBalance extends ConsumerWidget { } else { contract = MainDB.instance.getEthContractSync(contractAddress!)!; totalBalance = ref - .watch(pTokenBalance( - (contractAddress: contractAddress!, walletId: walletId))) + .watch( + pTokenBalance( + (walletId: walletId, contractAddress: contractAddress!), + ), + ) .total; } From 29e67ec0bffef0c0742c0ddd2e5685f4e0901912 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 May 2024 10:52:33 -0600 Subject: [PATCH 222/272] better logging --- .../wallet/wallet_mixin_interfaces/electrumx_interface.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 6b0f46522..2d84eb420 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1089,7 +1089,10 @@ mixin ElectrumXInterface on Bip39HDWallet { return allTxHashes; } catch (e, s) { - Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error); + Logging.instance.log( + "$runtimeType._fetchHistory: $e\n$s", + level: LogLevel.Error, + ); rethrow; } } From cf565944e242b152fdbc5d6cfa9e8f98a284de30 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 May 2024 11:11:16 -0600 Subject: [PATCH 223/272] freeze possible firo masternode outputs --- lib/wallets/wallet/impl/firo_wallet.dart | 54 ++++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 13d9f324e..a0d735ac5 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -549,39 +549,39 @@ class FiroWallet extends Bip39HDWallet } @override - Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + Future< + ({ + String? blockedReason, + bool blocked, + String? utxoLabel, + })> checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map? jsonTX, String? utxoOwnerAddress, ) async { - final bool blocked = false; + bool blocked = false; String? blockedReason; - // - // if (jsonTX != null) { - // // check for bip47 notification - // final outputs = jsonTX["vout"] as List; - // for (final output in outputs) { - // List? scriptChunks = - // (output['scriptPubKey']?['asm'] as String?)?.split(" "); - // if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { - // final blindedPaymentCode = scriptChunks![1]; - // final bytes = blindedPaymentCode.toUint8ListFromHex; - // - // // https://en.bitcoin.it/wiki/BIP_0047#Sending - // if (bytes.length == 80 && bytes.first == 1) { - // blocked = true; - // blockedReason = "Paynym notification output. Incautious " - // "handling of outputs from notification transactions " - // "may cause unintended loss of privacy."; - // break; - // } - // } - // } - // } - // - return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); + String? label; + + if (jsonUTXO["value"] is int) { + // TODO: [prio=med] use special electrumx call to verify the 1000 Firo output is masternode + blocked = Amount.fromDecimal( + Decimal.fromInt( + 1000, // 1000 firo output is a possible master node + ), + fractionDigits: cryptoCurrency.fractionDigits, + ).raw == + BigInt.from(jsonUTXO["value"] as int); + + if (blocked) { + blockedReason = "Possible masternode output. " + "Unlock and spend at your own risk."; + label = "Possible masternode"; + } + } + + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: label); } @override From fa8829072ef97171f5a73859551d22e918aafaf0 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 May 2024 11:24:19 -0600 Subject: [PATCH 224/272] enable solana and disable frost on desktop in release builds --- .../add_wallet_view/add_wallet_view.dart | 7 +++-- .../global_settings_view/hidden_settings.dart | 31 ------------------- lib/utilities/prefs.dart | 22 ------------- 3 files changed, 4 insertions(+), 56 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 6d0582c5c..34048fcbb 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -11,6 +11,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -134,9 +135,9 @@ class _AddWalletViewState extends ConsumerState { _coins.remove(Coin.wownero); } - // Remove Solana from the list of coins based on our frostEnabled preference. - if (!ref.read(prefsChangeNotifierProvider).solanaEnabled) { - _coins.remove(Coin.solana); + if (Util.isDesktop && !kDebugMode) { + _coins.remove(Coin.bitcoinFrost); + _coins.remove(Coin.bitcoinFrostTestNet); } coinEntities.addAll(_coins.map((e) => CoinEntity(e))); diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 0073d94ac..6ebafad4a 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -10,7 +10,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -277,36 +276,6 @@ class HiddenSettings extends StatelessWidget { const SizedBox( height: 12, ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - ref - .read(prefsChangeNotifierProvider) - .solanaEnabled = - !(ref - .read(prefsChangeNotifierProvider) - .solanaEnabled); - if (kDebugMode) { - print( - "Solana enabled: ${ref.read(prefsChangeNotifierProvider).solanaEnabled}"); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Toggle Solana", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), Consumer( builder: (_, ref, __) { return GestureDetector( diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index fc3d29e71..07726bdf1 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -68,7 +68,6 @@ class Prefs extends ChangeNotifier { await _setMaxDecimals(); _useTor = await _getUseTor(); _fusionServerInfo = await _getFusionServerInfo(); - _solanaEnabled = await _getSolanaEnabled(); _initialized = true; } @@ -1009,25 +1008,4 @@ class Prefs extends ChangeNotifier { return actualMap; } - - // Solana - - bool _solanaEnabled = false; - - bool get solanaEnabled => _solanaEnabled; - - set solanaEnabled(bool solanaEnabled) { - if (_solanaEnabled != solanaEnabled) { - DB.instance.put( - boxName: DB.boxNamePrefs, key: "solanaEnabled", value: solanaEnabled); - _solanaEnabled = solanaEnabled; - notifyListeners(); - } - } - - Future _getSolanaEnabled() async { - return await DB.instance.get( - boxName: DB.boxNamePrefs, key: "solanaEnabled") as bool? ?? - false; - } } From a63e2c784ed140d1ba50332ffe186640a10020d4 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 May 2024 11:49:49 -0600 Subject: [PATCH 225/272] icon size fix --- .../subviews/contact_popup.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/pages/address_book_views/subviews/contact_popup.dart b/lib/pages/address_book_views/subviews/contact_popup.dart index ca91001c4..ae31f8c09 100644 --- a/lib/pages/address_book_views/subviews/contact_popup.dart +++ b/lib/pages/address_book_views/subviews/contact_popup.dart @@ -29,6 +29,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -39,10 +40,10 @@ final exchangeFromAddressBookAddressStateProvider = class ContactPopUp extends ConsumerWidget { const ContactPopUp({ - Key? key, + super.key, required this.contactId, this.clipboard = const ClipboardWrapper(), - }) : super(key: key); + }); final String contactId; final ClipboardInterface clipboard; @@ -384,13 +385,18 @@ class ContactPopUp extends ConsumerWidget { color: Theme.of(context) .extension()! .textFieldDefaultBG, - padding: - const EdgeInsets.all(4), + padding: EdgeInsets.all( + Util.isDesktop ? 4 : 6, + ), child: SvgPicture.asset( Assets .svg.circleArrowUpRight, - width: 12, - height: 12, + width: Util.isDesktop + ? 12 + : 16, + height: Util.isDesktop + ? 12 + : 16, color: Theme.of(context) .extension< StackColors>()! From c1b3e1baf69d85d6571d83ca06ce17c6f5849c47 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 May 2024 12:00:42 -0600 Subject: [PATCH 226/272] flutter (and dart) version --- docs/building.md | 6 +++--- pubspec.lock | 4 ++-- pubspec.yaml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/building.md b/docs/building.md index d7a357076..029c0827e 100644 --- a/docs/building.md +++ b/docs/building.md @@ -12,7 +12,7 @@ Here you will find instructions on how to install the necessary tools for buildi 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 beta (3.19.0-0.1.pre) by following these instructions: 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.0-0.1.pre` tag, and add its `flutter/bin` folder to your PATH. Run `flutter doctor` in a terminal to confirm its installation. +Install Flutter 3.19.6 by following these instructions: 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. 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: @@ -42,7 +42,7 @@ sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2- Flutter and the Dart SDK should have been set up by Android studio, but if running `flutter` doesn't work (try `flutter doctor`, too), follow the [guide to install Flutter on any of their supported platforms](https://docs.flutter.dev/get-started/install) or: - `git clone https://github.com/flutter/flutter` somewhere it can live (`/var`, `/opt`, `~`) - - `git checkout 3.16.0` after navigating into the `flutter` directory, and + - `git checkout 3.19.6` after navigating into the `flutter` directory, and - add `flutter/bin` to your PATH (on Ubuntu, add `PATH=$PATH:path/to/flutter/bin` to `~/.profile`). Run `flutter doctor` to install any missing dependencies and review and agree to any license agreements. @@ -283,7 +283,7 @@ Copy the resulting `dll`s to their respective positions on the Windows host: Frostdart will be built by the Windows host later. ### Install Flutter on Windows host -Install Flutter 3.19.5 on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk. You can also clone https://github.com/flutter/flutter, check out the `3.19.5` tag, and add its `flutter/bin` folder to your PATH. Run `flutter doctor` in PowerShell to confirm its installation. +Install Flutter 3.19.6 on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows/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. 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: diff --git a/pubspec.lock b/pubspec.lock index 7681556d6..65cbfdc6e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2109,5 +2109,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.3.3 <4.0.0" - flutter: ">=3.19.5" + dart: ">=3.3.4 <4.0.0" + flutter: ">=3.19.6" diff --git a/pubspec.yaml b/pubspec.yaml index 971751045..e3b9ad5ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,8 +14,8 @@ description: Stack Wallet version: 2.0.0+219 environment: - sdk: ">=3.3.3 <4.0.0" - flutter: ^3.19.5 + sdk: ">=3.3.4 <4.0.0" + flutter: ^3.19.6 dependencies: flutter: From 12c47fcbecee671cb360a84700fe300387861b67 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 May 2024 12:48:59 -0600 Subject: [PATCH 227/272] extract date picker to separate file --- .../restore_options_view.dart | 114 +---------- lib/pages/ordinals/ordinals_filter_view.dart | 112 +---------- .../transaction_search_filter_view.dart | 188 +++++------------- lib/widgets/date_picker/date_picker.dart | 78 ++++++++ 4 files changed, 142 insertions(+), 350 deletions(-) create mode 100644 lib/widgets/date_picker/date_picker.dart diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 44ac51aac..3c7bdfd9e 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -11,9 +11,7 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart'; @@ -24,7 +22,6 @@ import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widge import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/themes/theme_providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -33,6 +30,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/date_picker/date_picker.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/expandable.dart'; @@ -42,10 +40,10 @@ import 'package:tuple/tuple.dart'; class RestoreOptionsView extends ConsumerStatefulWidget { const RestoreOptionsView({ - Key? key, + super.key, required this.walletName, required this.coin, - }) : super(key: key); + }); static const routeName = "/restoreOptions"; @@ -68,7 +66,6 @@ class _RestoreOptionsViewState extends ConsumerState { final bool _nextEnabled = true; DateTime _restoreFromDate = DateTime.fromMillisecondsSinceEpoch(0); - late final Color baseColor; bool hidePassword = true; bool _expandedAdavnced = false; @@ -77,7 +74,6 @@ class _RestoreOptionsViewState extends ConsumerState { @override void initState() { - baseColor = ref.read(themeProvider.state).state.textSubtitle2; walletName = widget.walletName; coin = widget.coin; isDesktop = Util.isDesktop; @@ -99,52 +95,6 @@ class _RestoreOptionsViewState extends ConsumerState { super.dispose(); } - MaterialRoundedDatePickerStyle _buildDatePickerStyle() { - return MaterialRoundedDatePickerStyle( - paddingMonthHeader: const EdgeInsets.only(top: 11), - colorArrowNext: Theme.of(context).extension()!.textSubtitle1, - colorArrowPrevious: - Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), - textStyleDayHeader: STextStyles.datePicker600(context), - textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( - color: baseColor, - ), - textStyleDayOnCalendarDisabled: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle3, - ), - textStyleDayOnCalendarSelected: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.popupBG, - ), - textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textStyleYearButton: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - textStyleButtonAction: GoogleFonts.inter(), - ); - } - - MaterialRoundedYearPickerStyle _buildYearPickerStyle() { - return MaterialRoundedYearPickerStyle( - textStyleYear: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle2, - ), - textStyleYearSelected: STextStyles.datePicker600(context).copyWith( - fontSize: 18, - ), - ); - } - Future nextPressed() async { if (!isDesktop) { // hide keyboard if has focus @@ -169,67 +119,23 @@ class _RestoreOptionsViewState extends ConsumerState { } Future chooseDate() async { - final height = MediaQuery.of(context).size.height; - final fetchedColor = - Theme.of(context).extension()!.accentColorDark; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 125)); } - final date = await showRoundedDatePicker( - context: context, - initialDate: DateTime.now(), - height: height / 3.0, - theme: ThemeData( - primarySwatch: Util.createMaterialColor(fetchedColor), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _restoreFromDate = date; - _dateController.text = Format.formatDate(date); + if (mounted) { + final date = await showSWDatePicker(context); + if (date != null) { + _restoreFromDate = date; + _dateController.text = Format.formatDate(date); + } } } Future chooseDesktopDate() async { - final height = MediaQuery.of(context).size.height; - final fetchedColor = - Theme.of(context).extension()!.accentColorDark; - // check and hide keyboard - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 125)); - } - - final date = await showRoundedDatePicker( - context: context, - initialDate: DateTime.now(), - height: height / 3.0, - theme: ThemeData( - primarySwatch: Util.createMaterialColor(fetchedColor), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); + final date = await showSWDatePicker(context); if (date != null) { _restoreFromDate = date; _dateController.text = Format.formatDate(date); diff --git a/lib/pages/ordinals/ordinals_filter_view.dart b/lib/pages/ordinals/ordinals_filter_view.dart index 631a9833a..1d2f61695 100644 --- a/lib/pages/ordinals/ordinals_filter_view.dart +++ b/lib/pages/ordinals/ordinals_filter_view.dart @@ -10,7 +10,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/theme_providers.dart'; @@ -21,6 +20,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/date_picker/date_picker.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -69,8 +69,8 @@ final ordinalFilterProvider = StateProvider((_) => null); class OrdinalsFilterView extends ConsumerStatefulWidget { const OrdinalsFilterView({ - Key? key, - }) : super(key: key); + super.key, + }); static const String routeName = "/ordinalsFilterView"; @@ -146,56 +146,6 @@ class _OrdinalsFilterViewState extends ConsumerState { DateTime? _selectedFromDate = DateTime(2007); DateTime? _selectedToDate = DateTime.now(); - MaterialRoundedDatePickerStyle _buildDatePickerStyle() { - return MaterialRoundedDatePickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - // backgroundHeader: Theme.of(context).extension()!.textSubtitle2, - paddingMonthHeader: const EdgeInsets.only(top: 11), - colorArrowNext: Theme.of(context).extension()!.textSubtitle1, - colorArrowPrevious: - Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), - textStyleDayHeader: STextStyles.datePicker600(context), - textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( - color: baseColor, - ), - textStyleDayOnCalendarDisabled: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle3, - ), - textStyleDayOnCalendarSelected: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textStyleYearButton: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - // textStyleButtonAction: GoogleFonts.inter(), - ); - } - - MaterialRoundedYearPickerStyle _buildYearPickerStyle() { - return MaterialRoundedYearPickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - textStyleYear: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle2, - fontSize: 16, - ), - textStyleYearSelected: STextStyles.datePicker600(context).copyWith( - fontSize: 18, - ), - ); - } - Widget _buildDateRangePicker() { const middleSeparatorPadding = 2.0; const middleSeparatorWidth = 12.0; @@ -216,9 +166,6 @@ class _OrdinalsFilterViewState extends ConsumerState { child: GestureDetector( key: const Key("OrdinalsViewFromDatePickerKey"), onTap: () async { - final color = - Theme.of(context).extension()!.accentColorDark; - final height = MediaQuery.of(context).size.height; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -226,28 +173,7 @@ class _OrdinalsFilterViewState extends ConsumerState { } if (mounted) { - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - initialDate: DateTime.now(), - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); + final date = await showSWDatePicker(context); if (date != null) { _selectedFromDate = date; @@ -330,9 +256,6 @@ class _OrdinalsFilterViewState extends ConsumerState { child: GestureDetector( key: const Key("OrdinalsViewToDatePickerKey"), onTap: () async { - final color = - Theme.of(context).extension()!.accentColorDark; - final height = MediaQuery.of(context).size.height; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -340,28 +263,7 @@ class _OrdinalsFilterViewState extends ConsumerState { } if (mounted) { - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - initialDate: DateTime.now(), - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); + final date = await showSWDatePicker(context); if (date != null) { _selectedToDate = date; @@ -467,7 +369,7 @@ class _OrdinalsFilterViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 75)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -840,7 +742,7 @@ class _OrdinalsFilterViewState extends ConsumerState { ); } } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 1a1513c1d..10d34771c 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -11,7 +11,6 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/transaction_filter.dart'; import 'package:stackwallet/providers/global/locale_provider.dart'; @@ -29,6 +28,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/date_picker/date_picker.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -40,9 +40,9 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class TransactionSearchFilterView extends ConsumerStatefulWidget { const TransactionSearchFilterView({ - Key? key, + super.key, required this.coin, - }) : super(key: key); + }); static const String routeName = "/transactionSearchFilter"; @@ -137,56 +137,6 @@ class _TransactionSearchViewState DateTime? _selectedFromDate = DateTime(2007); DateTime? _selectedToDate = DateTime.now(); - MaterialRoundedDatePickerStyle _buildDatePickerStyle() { - return MaterialRoundedDatePickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - // backgroundHeader: Theme.of(context).extension()!.textSubtitle2, - paddingMonthHeader: const EdgeInsets.only(top: 11), - colorArrowNext: Theme.of(context).extension()!.textSubtitle1, - colorArrowPrevious: - Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), - textStyleDayHeader: STextStyles.datePicker600(context), - textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( - color: baseColor, - ), - textStyleDayOnCalendarDisabled: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle3, - ), - textStyleDayOnCalendarSelected: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textStyleYearButton: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - // textStyleButtonAction: GoogleFonts.inter(), - ); - } - - MaterialRoundedYearPickerStyle _buildYearPickerStyle() { - return MaterialRoundedYearPickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - textStyleYear: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle2, - fontSize: 16, - ), - textStyleYearSelected: STextStyles.datePicker600(context).copyWith( - fontSize: 18, - ), - ); - } - Widget _buildDateRangePicker() { const middleSeparatorPadding = 2.0; const middleSeparatorWidth = 12.0; @@ -207,58 +157,36 @@ class _TransactionSearchViewState child: GestureDetector( key: const Key("transactionSearchViewFromDatePickerKey"), onTap: () async { - final color = - Theme.of(context).extension()!.accentColorDark; - final height = MediaQuery.of(context).size.height; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 125)); } - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - initialDate: DateTime.now(), - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, + if (mounted) { + final date = await showSWDatePicker(context); + if (date != null) { + _selectedFromDate = date; - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _selectedFromDate = date; - - // flag to adjust date so from date is always before to date - final flag = _selectedToDate != null && - !_selectedFromDate!.isBefore(_selectedToDate!); - if (flag) { - _selectedToDate = DateTime.fromMillisecondsSinceEpoch( - _selectedFromDate!.millisecondsSinceEpoch); - } - - setState(() { + // flag to adjust date so from date is always before to date + final flag = _selectedToDate != null && + !_selectedFromDate!.isBefore(_selectedToDate!); if (flag) { - _toDateString = _selectedToDate == null - ? "" - : Format.formatDate(_selectedToDate!); + _selectedToDate = DateTime.fromMillisecondsSinceEpoch( + _selectedFromDate!.millisecondsSinceEpoch); } - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); - }); + + setState(() { + if (flag) { + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); + } + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + }); + } } }, child: Container( @@ -319,58 +247,36 @@ class _TransactionSearchViewState child: GestureDetector( key: const Key("transactionSearchViewToDatePickerKey"), onTap: () async { - final color = - Theme.of(context).extension()!.accentColorDark; - final height = MediaQuery.of(context).size.height; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 125)); } - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - initialDate: DateTime.now(), - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, + if (mounted) { + final date = await showSWDatePicker(context); + if (date != null) { + _selectedToDate = date; - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _selectedToDate = date; - - // flag to adjust date so from date is always before to date - final flag = _selectedFromDate != null && - !_selectedToDate!.isAfter(_selectedFromDate!); - if (flag) { - _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( - _selectedToDate!.millisecondsSinceEpoch); - } - - setState(() { + // flag to adjust date so from date is always before to date + final flag = _selectedFromDate != null && + !_selectedToDate!.isAfter(_selectedFromDate!); if (flag) { - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); + _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( + _selectedToDate!.millisecondsSinceEpoch); } - _toDateString = _selectedToDate == null - ? "" - : Format.formatDate(_selectedToDate!); - }); + + setState(() { + if (flag) { + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + } + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); + }); + } } }, child: Container( @@ -454,7 +360,7 @@ class _TransactionSearchViewState FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 75)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -908,7 +814,7 @@ class _TransactionSearchViewState ); } } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, diff --git a/lib/widgets/date_picker/date_picker.dart b/lib/widgets/date_picker/date_picker.dart new file mode 100644 index 000000000..d2880d41b --- /dev/null +++ b/lib/widgets/date_picker/date_picker.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; +import 'package:google_fonts/google_fonts.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'; + +Future showSWDatePicker(BuildContext context) async { + final date = await showRoundedDatePicker( + context: context, + initialDate: DateTime.now(), + height: MediaQuery.of(context).size.height / 3.0, + theme: ThemeData( + primarySwatch: Util.createMaterialColor( + Theme.of(context).extension()!.accentColorDark), + ), + //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(context), + styleYearPicker: _buildYearPickerStyle(context), + ); + return date; +} + +MaterialRoundedDatePickerStyle _buildDatePickerStyle(BuildContext context) { + final baseColor = Theme.of(context).extension()!.textSubtitle2; + return MaterialRoundedDatePickerStyle( + backgroundPicker: Theme.of(context).extension()!.popupBG, + paddingMonthHeader: const EdgeInsets.only(top: 11), + colorArrowNext: Theme.of(context).extension()!.textSubtitle1, + colorArrowPrevious: + Theme.of(context).extension()!.textSubtitle1, + textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( + color: baseColor, + ), + textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( + color: baseColor, + ), + textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), + textStyleDayHeader: STextStyles.datePicker600(context), + textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( + color: baseColor, + ), + textStyleDayOnCalendarDisabled: STextStyles.datePicker400(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle3, + ), + textStyleDayOnCalendarSelected: STextStyles.datePicker400(context).copyWith( + color: Theme.of(context).extension()!.textWhite, + ), + textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + textStyleYearButton: STextStyles.datePicker600(context).copyWith( + color: Theme.of(context).extension()!.textWhite, + ), + textStyleButtonAction: GoogleFonts.inter(), + ); +} + +MaterialRoundedYearPickerStyle _buildYearPickerStyle(BuildContext context) { + return MaterialRoundedYearPickerStyle( + backgroundPicker: Theme.of(context).extension()!.popupBG, + textStyleYear: STextStyles.datePicker600(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle2, + fontSize: 16, + ), + textStyleYearSelected: STextStyles.datePicker600(context).copyWith( + fontSize: 18, + ), + ); +} From c8691ac0cca20c5a24cfdd3c88501bfbfb9bbfd2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 7 May 2024 14:08:08 -0500 Subject: [PATCH 228/272] fix address book contact selection Thank you @julian for the patch, ``` Index: lib/pages/send_view/send_view.dart IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart --- a/lib/pages/send_view/send_view.dart (revision fa8829072ef97171f5a73859551d22e918aafaf0) +++ b/lib/pages/send_view/send_view.dart (date 1715103987498) @@ -906,6 +906,10 @@ sendToController.text = _data!.contactLabel; _address = _data!.address.trim(); _addressToggleFlag = true; + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _setValidAddressProviders(_address); + }); } if (isPaynymSend) { ``` --- lib/pages/send_view/send_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 0ffeaafdc..07cdcfd8a 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -906,6 +906,10 @@ class _SendViewState extends ConsumerState { sendToController.text = _data!.contactLabel; _address = _data!.address.trim(); _addressToggleFlag = true; + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _setValidAddressProviders(_address); + }); } if (isPaynymSend) { From 70e95661357b266802f57c12f676173c9a4b1986 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 May 2024 16:34:08 -0600 Subject: [PATCH 229/272] use nicer looking date picker --- lib/widgets/date_picker/date_picker.dart | 166 ++++++++++-------- lib/widgets/date_picker/sw_date_picker.dart | 184 ++++++++++++++++++++ pubspec.lock | 16 +- pubspec.yaml | 2 +- 4 files changed, 291 insertions(+), 77 deletions(-) create mode 100644 lib/widgets/date_picker/sw_date_picker.dart diff --git a/lib/widgets/date_picker/date_picker.dart b/lib/widgets/date_picker/date_picker.dart index d2880d41b..054f89ee5 100644 --- a/lib/widgets/date_picker/date_picker.dart +++ b/lib/widgets/date_picker/date_picker.dart @@ -1,78 +1,108 @@ +import 'dart:math'; + +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; -import 'package:google_fonts/google_fonts.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/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +part 'sw_date_picker.dart'; Future showSWDatePicker(BuildContext context) async { - final date = await showRoundedDatePicker( + final Size size; + if (Util.isDesktop) { + size = const Size(450, 450); + } else { + final _size = MediaQuery.of(context).size; + size = Size( + _size.width - 32, + _size.height >= 550 ? 450 : _size.height - 32, + ); + } + print("====================================="); + print(size); + + final now = DateTime.now(); + + final date = await _showDatePickerDialog( context: context, - initialDate: DateTime.now(), - height: MediaQuery.of(context).size.height / 3.0, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - Theme.of(context).extension()!.accentColorDark), - ), - //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(context), - styleYearPicker: _buildYearPickerStyle(context), - ); - return date; -} - -MaterialRoundedDatePickerStyle _buildDatePickerStyle(BuildContext context) { - final baseColor = Theme.of(context).extension()!.textSubtitle2; - return MaterialRoundedDatePickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - paddingMonthHeader: const EdgeInsets.only(top: 11), - colorArrowNext: Theme.of(context).extension()!.textSubtitle1, - colorArrowPrevious: - Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), - textStyleDayHeader: STextStyles.datePicker600(context), - textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( - color: baseColor, - ), - textStyleDayOnCalendarDisabled: STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle3, - ), - textStyleDayOnCalendarSelected: STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textStyleYearButton: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - textStyleButtonAction: GoogleFonts.inter(), - ); -} - -MaterialRoundedYearPickerStyle _buildYearPickerStyle(BuildContext context) { - return MaterialRoundedYearPickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - textStyleYear: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle2, - fontSize: 16, - ), - textStyleYearSelected: STextStyles.datePicker600(context).copyWith( - fontSize: 18, + value: [now], + dialogSize: size, + config: CalendarDatePicker2WithActionButtonsConfig( + firstDate: DateTime(2007), + lastDate: now, + currentDate: now, + buttonPadding: const EdgeInsets.only( + right: 16, + ), + centerAlignModePicker: true, + selectedDayHighlightColor: + Theme.of(context).extension()!.accentColorDark, + daySplashColor: Theme.of(context) + .extension()! + .accentColorDark + .withOpacity(0.6), ), ); + return date?.first; +} + +Future?> _showDatePickerDialog({ + required BuildContext context, + required CalendarDatePicker2WithActionButtonsConfig config, + required Size dialogSize, + List value = const [], + bool useRootNavigator = true, + bool barrierDismissible = true, + Color? barrierColor = Colors.black54, + bool useSafeArea = true, + RouteSettings? routeSettings, + String? barrierLabel, + TransitionBuilder? builder, +}) { + final dialog = Dialog( + insetPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + backgroundColor: Theme.of(context).extension()!.popupBG, + surfaceTintColor: Colors.transparent, + shadowColor: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + clipBehavior: Clip.antiAlias, + child: SizedBox( + width: dialogSize.width, + height: max(dialogSize.height, 410), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _SWDatePicker( + value: value, + config: config.copyWith(openedFromDialog: true), + ), + ], + ), + ), + ); + + return showDialog>( + context: context, + useRootNavigator: useRootNavigator, + routeSettings: routeSettings, + builder: (BuildContext context) { + return builder == null ? dialog : builder(context, dialog); + }, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel, + useSafeArea: useSafeArea, + ); } diff --git a/lib/widgets/date_picker/sw_date_picker.dart b/lib/widgets/date_picker/sw_date_picker.dart new file mode 100644 index 000000000..665787139 --- /dev/null +++ b/lib/widgets/date_picker/sw_date_picker.dart @@ -0,0 +1,184 @@ +part of 'date_picker.dart'; + +class _SWDatePicker extends StatefulWidget { + const _SWDatePicker({ + super.key, + required this.value, + required this.config, + this.onValueChanged, + this.onDisplayedMonthChanged, + this.onCancelTapped, + this.onOkTapped, + }); + final List value; + + /// Called when the user taps 'OK' button + final ValueChanged>? onValueChanged; + + /// Called when the user navigates to a new month/year in the picker. + final ValueChanged? onDisplayedMonthChanged; + + /// The calendar configurations including action buttons + final CalendarDatePicker2WithActionButtonsConfig config; + + /// The callback when cancel button is tapped + final Function? onCancelTapped; + + /// The callback when ok button is tapped + final Function? onOkTapped; + @override + State<_SWDatePicker> createState() => _SWDatePickerState(); +} + +class _SWDatePickerState extends State<_SWDatePicker> { + List _values = []; + List _editCache = []; + + @override + void initState() { + _values = widget.value; + _editCache = widget.value; + super.initState(); + } + + @override + void didUpdateWidget(covariant _SWDatePicker oldWidget) { + var isValueSame = oldWidget.value.length == widget.value.length; + + if (isValueSame) { + for (int i = 0; i < oldWidget.value.length; i++) { + final isSame = + (oldWidget.value[i] == null && widget.value[i] == null) || + DateUtils.isSameDay(oldWidget.value[i], widget.value[i]); + if (!isSame) { + isValueSame = false; + break; + } + } + } + + if (!isValueSame) { + _values = widget.value; + _editCache = widget.value; + } + + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + focusColor: Colors.transparent, + colorScheme: Theme.of(context).colorScheme.copyWith( + background: Theme.of(context).extension()!.popupBG, + onBackground: + Theme.of(context).extension()!.accentColorDark, + surface: Theme.of(context).extension()!.popupBG, + surfaceVariant: + Theme.of(context).extension()!.popupBG, + onSurface: + Theme.of(context).extension()!.accentColorDark, + onSurfaceVariant: + Theme.of(context).extension()!.accentColorDark, + surfaceTint: Colors.transparent, + shadow: Colors.transparent, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MediaQuery.removePadding( + context: context, + child: CalendarDatePicker2( + value: [..._editCache], + config: widget.config, + onValueChanged: (values) => _editCache = values, + onDisplayedMonthChanged: widget.onDisplayedMonthChanged, + ), + ), + SizedBox(height: widget.config.gapBetweenCalendarAndButtons ?? 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!Util.isDesktop) + SizedBox( + width: widget.config.buttonPadding?.right ?? 0, + ), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: Padding( + padding: widget.config.buttonPadding ?? EdgeInsets.zero, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => SizedBox( + width: 140, + child: child, + ), + child: SecondaryButton( + label: "Cancel", + buttonHeight: Util.isDesktop ? ButtonHeight.m : null, + onPressed: () { + setState( + () { + _editCache = _values; + widget.onCancelTapped?.call(); + if ((widget.config.openedFromDialog ?? false) && + (widget.config.closeDialogOnCancelTapped ?? + true)) { + Navigator.pop(context); + } + }, + ); + }, + ), + ), + ), + ), + if ((widget.config.gapBetweenCalendarAndButtons ?? 0) > 0) + SizedBox(width: widget.config.gapBetweenCalendarAndButtons), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: Padding( + padding: widget.config.buttonPadding ?? EdgeInsets.zero, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => SizedBox( + width: 140, + child: child, + ), + child: PrimaryButton( + buttonHeight: Util.isDesktop ? ButtonHeight.m : null, + label: "Ok", + onPressed: () { + setState( + () { + _values = _editCache; + widget.onValueChanged?.call(_values); + widget.onOkTapped?.call(); + if ((widget.config.openedFromDialog ?? false) && + (widget.config.closeDialogOnOkTapped ?? true)) { + Navigator.pop(context, _values); + } + }, + ); + }, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 65cbfdc6e..e937fb4d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -238,6 +238,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.6.2" + calendar_date_picker2: + dependency: "direct main" + description: + name: calendar_date_picker2 + sha256: "7ff3f372faff6814a2ba69427d116fb9a3d52e28644b9de4b06db6638fdac798" + url: "https://pub.dev" + source: hosted + version: "1.0.2" characters: dependency: transitive description: @@ -757,14 +765,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - flutter_rounded_date_picker: - dependency: "direct main" - description: - name: flutter_rounded_date_picker - sha256: e6aa2dc5d3b44e8bbe85ef901be69eac59ba4136427f11f4c8b2a303e1e774e7 - url: "https://pub.dev" - source: hosted - version: "3.0.4" flutter_secure_storage: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e3b9ad5ac..b3bb574a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -124,7 +124,6 @@ dependencies: decimal: ^2.1.0 event_bus: ^2.0.0 uuid: ^3.0.5 - flutter_rounded_date_picker: ^3.0.1 crypto: ^3.0.2 barcode_scan2: ^4.2.3 wakelock: ^0.6.2 @@ -178,6 +177,7 @@ dependencies: url: https://github.com/cypherstack/espresso-cash-public.git ref: a83e375678eb22fe544dc125d29bbec0fb833882 path: packages/solana + calendar_date_picker2: ^1.0.2 dev_dependencies: flutter_test: From 7bde8b70d9832bce9f8ae9f9fad18d372d334e4d Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 10:31:38 -0600 Subject: [PATCH 230/272] update default themes --- assets/default_themes/dark.zip | Bin 676128 -> 950935 bytes assets/default_themes/light.zip | Bin 624020 -> 898825 bytes lib/themes/theme_service.dart | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip index eb20d7e6e6279c6c6ca50a76aef686d968861b47..f69e936f2c0e0dd169ffef1b0e4cb82e347b83f7 100644 GIT binary patch delta 284180 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 E;Ev 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 JBmL) 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 delta 7401 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

gJV!6aDjV03g5as_V0kr~pa!+U8zNrFwPVEaDNq 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⁡82=5(FPDEwB#Lw&$F8cUh6b{>qXj7M#XgBX7o@QRksVaF=h>r_$l%sIjz~Q+yDstF2)2JW6){%S0=1%x`V7BhA$-a#CAPk zH`m5`L@7(&NZg^d6B8)RrcY@^okp_62S;3xV}65UE|*Ds zUnsjTA11N=EwWk;>edt*Hdlt8Scu%OI@3}W{|@f(c&5p&A%O8U_U>Kr*-q&A8c4;u z{T7gWc!MV8uZf-Yw5*gFo~8X0i_DgnqY`Y>Bwj95dgFz1V3B60zv#N&JZL&kmYc|KAC_zc>hg9>?l#J@l0z!UUP zT6sXMtI30Y)I1UIiJJ_EWCo37hGW;Z>MSmhCPHX!>2hPNjhgM;A_5%yT=!%ASW-9E$cieO_=jG(Zq0Djvb!aG&nTp0~b~YiN zhH7?D3=mJhaor$tbQcDzj#UcN>V~SBZ1(D*7+}Wb0!!;mR56LDC>1C~QQsZ)L!Y;R#8t(( zTS4+#8{(5<7&nqBY>O~FiiKNr%fPeodpV(N=n%v%V+2djl!1P?QH9;sv0NPdAM@YP z8wj|r-GP37gK<7ie_B`K4>6tdbDcSV+b+zZr3rgthM+3Apz`mZwDkOW;1xw*%NYy) z0Tpl<4Z8%P7_16}a?@^94j&HR7r^^^rTh_$_#PDeo31PQU(kcFN$(93lhGxz+)j%5 zkYk(vS)mzO`knESX9GW6*dpEcd?;S*6B;52dZJseMAU%IxWz3wc^g>t+fWIP7H2)? zvLFvmc5WSAYtv5pa5!Uh|ER;*%0L{#5dB+1xwmbvJb!Iga|T;6 zpja=+;}(d0B4$m3%?W1q2fkBMk2((sHrm2_(P~H zI!Pp4CxcxC2wi{g!h?U#rT*!@D5i>S9!wKwB@LZ)NKNG>4A k>xL9)&b)!#z4$j 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 delta 7431 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 T6~(SW{zL=-V23aA!U6mbyn*OO diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index 01134da66..b41775ee6 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -29,7 +29,7 @@ final pThemeService = Provider((ref) { }); class ThemeService { - static const _currentDefaultThemeVersion = 4; + static const _currentDefaultThemeVersion = 8; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._(); From 4f8cbbd8d899b17c4ae46c7c77e56687b12c43c4 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 10:36:47 -0600 Subject: [PATCH 231/272] address type display name tweaks --- lib/models/isar/models/blockchain_data/address.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 76d69f104..e3314d754 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -170,11 +170,11 @@ enum AddressType { 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: @@ -200,7 +200,7 @@ enum AddressType { case AddressType.solana: return "Solana"; case AddressType.p2tr: - return "Taproot"; // Why not use P2TR, P2PKH, etc.? + return "P2TR (taproot)"; } } } From 9ec40fdc2e56ebc0bca3f250d0419a65a0056fea Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 10:39:09 -0600 Subject: [PATCH 232/272] don't show p2pkh addresses on generate address option for bitcoin wallets --- lib/pages/receive_view/receive_view.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index d1f9e23c4..a03a4934a 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -33,6 +33,7 @@ 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'; @@ -198,6 +199,10 @@ class _ReceiveViewState extends ConsumerState { } } + if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) { + _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); + } + _addressMap[_walletAddressTypes[_currentIndex]] = ref.read(pWalletReceivingAddress(walletId)); From 9ff14ea4711c36b394b2fe1384453f583a7d06cb Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 10:53:47 -0600 Subject: [PATCH 233/272] remove old date picker leftovers --- lib/widgets/rounded_date_picker/LICENSE | 5 - .../flutter_rounded_date_picker_dialog.dart | 342 ------------------ .../flutter_rounded_date_picker_widget.dart | 226 ------------ 3 files changed, 573 deletions(-) delete mode 100644 lib/widgets/rounded_date_picker/LICENSE delete mode 100644 lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart delete mode 100644 lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart diff --git a/lib/widgets/rounded_date_picker/LICENSE b/lib/widgets/rounded_date_picker/LICENSE deleted file mode 100644 index 58665fbd2..000000000 --- a/lib/widgets/rounded_date_picker/LICENSE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at: - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart deleted file mode 100644 index 3c8ff39bb..000000000 --- a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart +++ /dev/null @@ -1,342 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'package:flutter/material.dart'; -import 'package:flutter/semantics.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; -import 'package:flutter_rounded_date_picker/src/flutter_rounded_button_action.dart'; -import 'package:flutter_rounded_date_picker/src/material_rounded_date_picker_style.dart'; -import 'package:flutter_rounded_date_picker/src/material_rounded_year_picker_style.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_date_picker_header.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_day_picker.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_month_picker.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_year_picker.dart'; -import 'package:stackwallet/utilities/util.dart'; - -/// -/// This file uses code taken from https://github.com/benznest/flutter_rounded_date_picker -/// - -class FlutterRoundedDatePickerDialog extends StatefulWidget { - const FlutterRoundedDatePickerDialog( - {Key? key, - this.height, - required this.initialDate, - required this.firstDate, - required this.lastDate, - this.selectableDayPredicate, - required this.initialDatePickerMode, - required this.era, - this.locale, - required this.borderRadius, - this.imageHeader, - this.description = "", - this.fontFamily, - this.textNegativeButton, - this.textPositiveButton, - this.textActionButton, - this.onTapActionButton, - this.styleDatePicker, - this.styleYearPicker, - this.customWeekDays, - this.builderDay, - this.listDateDisabled, - this.onTapDay, - this.onMonthChange}) - : super(key: key); - - final DateTime initialDate; - final DateTime firstDate; - final DateTime lastDate; - final SelectableDayPredicate? selectableDayPredicate; - final DatePickerMode initialDatePickerMode; - - /// double height. - final double? height; - - /// Custom era year. - final EraMode era; - final Locale? locale; - - /// Border - final double borderRadius; - - /// Header; - final ImageProvider? imageHeader; - final String description; - - /// Font - final String? fontFamily; - - /// Button - final String? textNegativeButton; - final String? textPositiveButton; - final String? textActionButton; - - final VoidCallback? onTapActionButton; - - /// Style - final MaterialRoundedDatePickerStyle? styleDatePicker; - final MaterialRoundedYearPickerStyle? styleYearPicker; - - /// Custom Weekday - final List? customWeekDays; - - final BuilderDayOfDatePicker? builderDay; - - final List? listDateDisabled; - final OnTapDay? onTapDay; - - final Function? onMonthChange; - - @override - _FlutterRoundedDatePickerDialogState createState() => - _FlutterRoundedDatePickerDialogState(); -} - -class _FlutterRoundedDatePickerDialogState - extends State { - @override - void initState() { - super.initState(); - _selectedDate = widget.initialDate; - _mode = widget.initialDatePickerMode; - } - - bool _announcedInitialDate = false; - - late MaterialLocalizations localizations; - late TextDirection textDirection; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - localizations = MaterialLocalizations.of(context); - textDirection = Directionality.of(context); - if (!_announcedInitialDate) { - _announcedInitialDate = true; - SemanticsService.announce( - localizations.formatFullDate(_selectedDate), - textDirection, - ); - } - } - - late DateTime _selectedDate; - late DatePickerMode _mode; - final GlobalKey _pickerKey = GlobalKey(); - - void _vibrate() { - switch (Theme.of(context).platform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - HapticFeedback.vibrate(); - break; - case TargetPlatform.iOS: - default: - break; - } - } - - void _handleModeChanged(DatePickerMode mode) { - _vibrate(); - setState(() { - _mode = mode; - if (_mode == DatePickerMode.day) { - SemanticsService.announce( - localizations.formatMonthYear(_selectedDate), - textDirection, - ); - } else { - SemanticsService.announce( - localizations.formatYear(_selectedDate), - textDirection, - ); - } - }); - } - - Future _handleYearChanged(DateTime value) async { - if (value.isBefore(widget.firstDate)) { - value = widget.firstDate; - } else if (value.isAfter(widget.lastDate)) { - value = widget.lastDate; - } - if (value == _selectedDate) return; - - if (widget.onMonthChange != null) await widget.onMonthChange!(value); - - _vibrate(); - setState(() { - _mode = DatePickerMode.day; - _selectedDate = value; - }); - } - - void _handleDayChanged(DateTime value) { - _vibrate(); - setState(() { - _selectedDate = value; - }); - } - - void _handleCancel() { - Navigator.of(context).pop(); - } - - void _handleOk() { - Navigator.of(context).pop(_selectedDate); - } - - Widget _buildPicker() { - switch (_mode) { - case DatePickerMode.year: - return FlutterRoundedYearPicker( - key: _pickerKey, - selectedDate: _selectedDate, - onChanged: (DateTime date) async => await _handleYearChanged(date), - firstDate: widget.firstDate, - lastDate: widget.lastDate, - era: widget.era, - fontFamily: widget.fontFamily, - style: widget.styleYearPicker, - ); - case DatePickerMode.day: - default: - return FlutterRoundedMonthPicker( - key: _pickerKey, - selectedDate: _selectedDate, - onChanged: _handleDayChanged, - firstDate: widget.firstDate, - lastDate: widget.lastDate, - era: widget.era, - locale: widget.locale, - selectableDayPredicate: widget.selectableDayPredicate, - fontFamily: widget.fontFamily, - style: widget.styleDatePicker, - borderRadius: widget.borderRadius, - customWeekDays: widget.customWeekDays, - builderDay: widget.builderDay, - listDateDisabled: widget.listDateDisabled, - onTapDay: widget.onTapDay, - onMonthChange: widget.onMonthChange); - } - } - - @override - Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - final Widget picker = _buildPicker(); - final isDesktop = Util.isDesktop; - - final Widget actions = FlutterRoundedButtonAction( - textButtonNegative: widget.textNegativeButton, - textButtonPositive: widget.textPositiveButton, - onTapButtonNegative: _handleCancel, - onTapButtonPositive: _handleOk, - textActionButton: widget.textActionButton, - onTapButtonAction: widget.onTapActionButton, - localizations: localizations, - textStyleButtonNegative: widget.styleDatePicker?.textStyleButtonNegative, - textStyleButtonPositive: widget.styleDatePicker?.textStyleButtonPositive, - textStyleButtonAction: widget.styleDatePicker?.textStyleButtonAction, - borderRadius: widget.borderRadius, - paddingActionBar: widget.styleDatePicker?.paddingActionBar, - background: widget.styleDatePicker?.backgroundActionBar, - ); - - Color backgroundPicker = theme.dialogBackgroundColor; - if (_mode == DatePickerMode.day) { - backgroundPicker = widget.styleDatePicker?.backgroundPicker ?? - theme.dialogBackgroundColor; - } else { - backgroundPicker = widget.styleYearPicker?.backgroundPicker ?? - theme.dialogBackgroundColor; - } - - final Dialog dialog = Dialog( - child: OrientationBuilder( - builder: (BuildContext context, Orientation orientation) { - final Widget header = FlutterRoundedDatePickerHeader( - selectedDate: _selectedDate, - mode: _mode, - onModeChanged: _handleModeChanged, - orientation: orientation, - era: widget.era, - borderRadius: widget.borderRadius, - imageHeader: widget.imageHeader, - description: widget.description, - fontFamily: widget.fontFamily, - style: widget.styleDatePicker); - switch (orientation) { - case Orientation.landscape: - return Container( - height: isDesktop ? 600 : null, - width: isDesktop ? 700 : null, - decoration: BoxDecoration( - color: backgroundPicker, - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible(flex: 1, child: header), - Flexible( - flex: 2, // have the picker take up 2/3 of the dialog width - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox( - height: isDesktop ? 530 : null, - width: isDesktop ? 700 : null, - child: picker), - actions, - ], - ), - ), - ], - ), - ); - case Orientation.portrait: - default: - return Container( - decoration: BoxDecoration( - color: backgroundPicker, - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - header, - if (widget.height == null) - Flexible(child: picker) - else - SizedBox( - height: widget.height, - child: picker, - ), - actions, - ], - ), - ); - } - }), - ); - - return Theme( - data: theme.copyWith(dialogBackgroundColor: Colors.transparent), - child: dialog, - ); - } -} diff --git a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart deleted file mode 100644 index 6f6db0580..000000000 --- a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart +++ /dev/null @@ -1,226 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -// import 'package:flutter_rounded_date_picker/src/dialogs/flutter_rounded_date_picker_dialog.dart'; -import 'package:flutter_rounded_date_picker/src/era_mode.dart'; -import 'package:flutter_rounded_date_picker/src/material_rounded_date_picker_style.dart'; -import 'package:flutter_rounded_date_picker/src/material_rounded_year_picker_style.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_day_picker.dart'; -import 'package:stackwallet/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart'; - -/// -/// This file uses code taken from https://github.com/benznest/flutter_rounded_date_picker -/// - -// Examples can assume: -// BuildContext context; - -/// Initial display mode of the date picker dialog. -/// -/// Date picker UI mode for either showing a list of available years or a -/// monthly calendar initially in the dialog shown by calling [showDatePicker]. -/// - -// Shows the selected date in large font and toggles between year and day mode - -/// Signature for predicating dates for enabled date selections. -/// -/// See [showDatePicker]. -typedef SelectableDayPredicate = bool Function(DateTime day); - -/// Shows a dialog containing a material design date picker. -/// -/// The returned [Future] resolves to the date selected by the user when the -/// user closes the dialog. If the user cancels the dialog, null is returned. -/// -/// An optional [selectableDayPredicate] function can be passed in to customize -/// the days to enable for selection. If provided, only the days that -/// [selectableDayPredicate] returned true for will be selectable. -/// -/// An optional [initialDatePickerMode] argument can be used to display the -/// date picker initially in the year or month+day picker mode. It defaults -/// to month+day, and must not be null. -/// -/// An optional [locale] argument can be used to set the locale for the date -/// picker. It defaults to the ambient locale provided by [Localizations]. -/// -/// An optional [textDirection] argument can be used to set the text direction -/// (RTL or LTR) for the date picker. It defaults to the ambient text direction -/// provided by [Directionality]. If both [locale] and [textDirection] are not -/// null, [textDirection] overrides the direction chosen for the [locale]. -/// -/// The [context] argument is passed to [showDialog], the documentation for -/// which discusses how it is used. -/// -/// The [builder] parameter can be used to wrap the dialog widget -/// to add inherited widgets like [Theme]. -/// -/// {@tool sample} -/// Show a date picker with the dark theme. -/// -/// ```dart -/// Future selectedDate = showDatePicker( -/// context: context, -/// initialDate: DateTime.now(), -/// firstDate: DateTime(2018), -/// lastDate: DateTime(2030), -/// builder: (BuildContext context, Widget child) { -/// return Theme( -/// data: ThemeData.dark(), -/// child: child, -/// ); -/// }, -/// ); -/// ``` -/// {@end-tool} -/// -/// The [context], [initialDate], [firstDate], and [lastDate] parameters must -/// not be null. -/// -/// See also: -/// -/// * [showTimePicker], which shows a dialog that contains a material design -/// time picker. -/// * [DayPicker], which displays the days of a given month and allows -/// choosing a day. -/// * [MonthPicker], which displays a scrollable list of months to allow -/// picking a month. -/// * [YearPicker], which displays a scrollable list of years to allow picking -/// a year. -/// - -Future showRoundedDatePicker( - {required BuildContext context, - double? height, - DateTime? initialDate, - DateTime? firstDate, - DateTime? lastDate, - SelectableDayPredicate? selectableDayPredicate, - DatePickerMode initialDatePickerMode = DatePickerMode.day, - Locale? locale, - TextDirection? textDirection, - ThemeData? theme, - double borderRadius = 16, - EraMode era = EraMode.CHRIST_YEAR, - ImageProvider? imageHeader, - String description = "", - String? fontFamily, - bool barrierDismissible = false, - Color background = Colors.transparent, - String? textNegativeButton, - String? textPositiveButton, - String? textActionButton, - VoidCallback? onTapActionButton, - MaterialRoundedDatePickerStyle? styleDatePicker, - MaterialRoundedYearPickerStyle? styleYearPicker, - List? customWeekDays, - BuilderDayOfDatePicker? builderDay, - List? listDateDisabled, - OnTapDay? onTapDay, - Function? onMonthChange}) async { - initialDate ??= DateTime.now(); - firstDate ??= DateTime(initialDate.year - 1); - lastDate ??= DateTime(initialDate.year + 1); - theme ??= ThemeData(); - - assert( - !initialDate.isBefore(firstDate), - 'initialDate must be on or after firstDate', - ); - assert( - !initialDate.isAfter(lastDate), - 'initialDate must be on or before lastDate', - ); - assert( - !firstDate.isAfter(lastDate), - 'lastDate must be on or after firstDate', - ); - assert( - selectableDayPredicate == null || selectableDayPredicate(initialDate), - 'Provided initialDate must satisfy provided selectableDayPredicate', - ); - assert( - (onTapActionButton != null && textActionButton != null) || - onTapActionButton == null, - "If you provide onLeftBtn, you must provide leftBtn", - ); - assert(debugCheckHasMaterialLocalizations(context)); - - Widget child = GestureDetector( - onTap: () { - if (!barrierDismissible) { - Navigator.pop(context); - } - }, - child: Container( - color: background, - child: GestureDetector( - onTap: () { - // - }, - child: FlutterRoundedDatePickerDialog( - height: height, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - selectableDayPredicate: selectableDayPredicate, - initialDatePickerMode: initialDatePickerMode, - era: era, - locale: locale, - borderRadius: borderRadius, - imageHeader: imageHeader, - description: description, - fontFamily: fontFamily, - textNegativeButton: textNegativeButton, - textPositiveButton: textPositiveButton, - textActionButton: textActionButton, - onTapActionButton: onTapActionButton, - styleDatePicker: styleDatePicker, - styleYearPicker: styleYearPicker, - customWeekDays: customWeekDays, - builderDay: builderDay, - listDateDisabled: listDateDisabled, - onTapDay: onTapDay, - onMonthChange: onMonthChange, - ), - ), - ), - ); - - if (textDirection != null) { - child = Directionality( - textDirection: textDirection, - child: child, - ); - } - - if (locale != null) { - child = Localizations.override( - context: context, - locale: locale, - child: child, - ); - } - - return await showDialog( - context: context, - barrierDismissible: barrierDismissible, - builder: (_) => Theme(data: theme!, child: child), - ); -} From 8a076a8d5b977cc1f4ce0a9e79816d9d0d2aa5e4 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 10:58:14 -0600 Subject: [PATCH 234/272] init state provider fix --- lib/pages/wallet_view/wallet_view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index a211cddd8..20bd28575 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -170,7 +170,9 @@ class _WalletViewState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(walletId); coin = wallet.info.coin; - ref.read(currentWalletIdProvider.notifier).state = wallet.walletId; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(currentWalletIdProvider.notifier).state = wallet.walletId; + }); if (!wallet.shouldAutoSync) { // enable auto sync if it wasn't enabled when loading wallet From bf14dd09f421e16798191ce6acfa6f64fab61696 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 11:01:52 -0600 Subject: [PATCH 235/272] display "transparent" instead of "p2pkh" on firo address type --- lib/pages/receive_view/receive_view.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index a03a4934a..f96e83274 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -394,7 +394,11 @@ class _ReceiveViewState extends ConsumerState { DropdownMenuItem( value: i, child: Text( - "${_walletAddressTypes[i].readableName} address", + _supportsSpark && + _walletAddressTypes[i] == + AddressType.p2pkh + ? "Transparent address" + : "${_walletAddressTypes[i].readableName} address", style: STextStyles.w500_14(context), ), ), From d8f6ff23d47677bb99a8a320525b5e7840a24906 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 13:26:51 -0600 Subject: [PATCH 236/272] don't show lelantus balance on mobile if zero --- .../firo_balance_selection_sheet.dart | 152 +++++++++--------- .../wallet_balance_toggle_sheet.dart | 15 +- 2 files changed, 89 insertions(+), 78 deletions(-) diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart index 8c01cac9a..6206f13f7 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -173,85 +173,91 @@ class _FiroBalanceSelectionSheetState ), ), ), - const SizedBox( - height: 16, - ), - GestureDetector( - onTap: () { - final state = - ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != FiroType.lelantus) { - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.lelantus; - } - Navigator.of(context).pop(); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: FiroType.lelantus, - groupValue: ref - .watch( - publicPrivateBalanceStateProvider.state) - .state, - onChanged: (x) { - ref - .read(publicPrivateBalanceStateProvider - .state) - .state = FiroType.lelantus; - - Navigator.of(context).pop(); - }, - ), - ), - ], - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (firoWallet.info.cachedBalanceSecondary.spendable.raw > + BigInt.zero) + const SizedBox( + height: 16, + ), + if (firoWallet.info.cachedBalanceSecondary.spendable.raw > + BigInt.zero) + GestureDetector( + onTap: () { + final state = ref + .read(publicPrivateBalanceStateProvider.state) + .state; + if (state != FiroType.lelantus) { + ref + .read(publicPrivateBalanceStateProvider.state) + .state = FiroType.lelantus; + } + Navigator.of(context).pop(); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ - // Row( - // children: [ - Text( - "Lelantus balance", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - width: 2, - ), - Text( - ref.watch(pAmountFormatter(coin)).format( - firoWallet.info.cachedBalanceSecondary - .spendable, - ), - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: FiroType.lelantus, + groupValue: ref + .watch(publicPrivateBalanceStateProvider + .state) + .state, + onChanged: (x) { + ref + .read(publicPrivateBalanceStateProvider + .state) + .state = FiroType.lelantus; + + Navigator.of(context).pop(); + }, + ), ), ], ), - // ], - // ), - ) - ], + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row( + // children: [ + Text( + "Lelantus balance", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + width: 2, + ), + Text( + ref.watch(pAmountFormatter(coin)).format( + firoWallet.info.cachedBalanceSecondary + .spendable, + ), + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + // ], + // ), + ) + ], + ), ), ), - ), const SizedBox( height: 16, ), diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index 45483e9d0..af0eca1c6 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -33,9 +33,9 @@ enum _BalanceType { class WalletBalanceToggleSheet extends ConsumerWidget { const WalletBalanceToggleSheet({ - Key? key, + super.key, required this.walletId, - }) : super(key: key); + }); final String walletId; @@ -46,7 +46,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { final coin = ref.watch(pWalletCoin(walletId)); final isFiro = coin == Coin.firo || coin == Coin.firoTestNet; - Balance balance = ref.watch(pWalletBalance(walletId)); + final balance = ref.watch(pWalletBalance(walletId)); _BalanceType _bal = ref.watch(walletBalanceToggleStateProvider.state).state == @@ -77,6 +77,11 @@ class WalletBalanceToggleSheet extends ConsumerWidget { // already set above break; } + + // hack to not show lelantus balance in ui if zero + if (balanceSecondary?.spendable.raw == BigInt.zero) { + balanceSecondary = null; + } } return Container( @@ -289,7 +294,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { class BalanceSelector extends ConsumerWidget { const BalanceSelector({ - Key? key, + super.key, required this.title, required this.coin, required this.balance, @@ -297,7 +302,7 @@ class BalanceSelector extends ConsumerWidget { required this.onChanged, required this.value, required this.groupValue, - }) : super(key: key); + }); final String title; final Coin coin; From ee1c9f132bee821f2c62ff33035796fdd9866697 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 13:28:27 -0600 Subject: [PATCH 237/272] update liblelantus ref --- crypto_plugins/flutter_liblelantus | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index 032407f0f..b654bf448 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit 032407f0f7734f3cec3eefba76c5dc587b9a252d +Subproject commit b654bf4488357c8a104900e11f9468d54a39f22b From 9b4e247b4f6e97c60bfe215a6ccbfc511edeb3fd Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 13:31:07 -0600 Subject: [PATCH 238/272] update spark lib --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e937fb4d4..c4e4adb85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -696,8 +696,8 @@ packages: dependency: "direct main" description: path: "." - ref: "65a6676e37f1bcaf2e293afbd50e50c81394276c" - resolved-ref: "65a6676e37f1bcaf2e293afbd50e50c81394276c" + ref: "439727b278250c61a291f5335c298c0f2d952517" + resolved-ref: "439727b278250c61a291f5335c298c0f2d952517" url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index b3bb574a2..54c52e8f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git - ref: 65a6676e37f1bcaf2e293afbd50e50c81394276c + ref: 439727b278250c61a291f5335c298c0f2d952517 flutter_libmonero: path: ./crypto_plugins/flutter_libmonero From 213f78b36cd84d6b3635ec5b1ea26a1b3b47309d Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 8 May 2024 16:26:04 -0600 Subject: [PATCH 239/272] quick vSize calc --- lib/utilities/extensions/extensions.dart | 4 ++ .../extensions/impl/cl_transaction.dart | 53 +++++++++++++++++++ .../electrumx_interface.dart | 2 +- .../paynym_interface.dart | 2 +- 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 lib/utilities/extensions/impl/cl_transaction.dart diff --git a/lib/utilities/extensions/extensions.dart b/lib/utilities/extensions/extensions.dart index 4a9aac287..a798c002a 100644 --- a/lib/utilities/extensions/extensions.dart +++ b/lib/utilities/extensions/extensions.dart @@ -9,5 +9,9 @@ */ export 'impl/big_int.dart'; +export 'impl/box_shadow.dart'; +export 'impl/cl_transaction.dart'; +export 'impl/contract_abi.dart'; +export 'impl/gradient.dart'; export 'impl/string.dart'; export 'impl/uint8_list.dart'; diff --git a/lib/utilities/extensions/impl/cl_transaction.dart b/lib/utilities/extensions/impl/cl_transaction.dart new file mode 100644 index 000000000..1c117cf8c --- /dev/null +++ b/lib/utilities/extensions/impl/cl_transaction.dart @@ -0,0 +1,53 @@ +import 'dart:typed_data'; + +import 'package:coinlib_flutter/coinlib_flutter.dart'; + +extension CLTransactionExt on Transaction { + int weight() { + final base = _byteLength(false); + final total = _byteLength(true); + return base * 3 + total; + } + + int vSize() => (weight() / 4).ceil(); + + int _byteLength(final bool allowWitness) { + final hasWitness = allowWitness && isWitness; + return (hasWitness ? 10 : 8) + + _encodingLength(inputs.length) + + _encodingLength(outputs.length) + + inputs.fold(0, (sum, input) => sum + input.size) + + outputs.fold(0, (sum, output) => sum + output.size) + + (hasWitness + ? inputs.fold(0, (sum, input) { + if (input is! WitnessInput) { + return sum; + } else { + return sum + _vectorSize(input.witness); + } + }) + : 0); + } + + int _varSliceSize(Uint8List someScript) { + final length = someScript.length; + return _encodingLength(length) + length; + } + + int _vectorSize(List someVector) { + final length = someVector.length; + return _encodingLength(length) + + someVector.fold( + 0, + (sum, witness) => sum + _varSliceSize(witness), + ); + } + + int _encodingLength(int number) => number < 0xfd + ? 1 + : number <= 0xffff + ? 3 + : number <= 0xffffffff + ? 5 + : 9; +} diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 2d84eb420..b4fdb7a9d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -768,7 +768,7 @@ mixin ElectrumXInterface on Bip39HDWallet { return txData.copyWith( raw: clTx.toHex(), - vSize: clTx.size, + vSize: clTx.vSize(), tempTx: TransactionV2( walletId: walletId, blockHash: null, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index c17965584..202b22943 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -866,7 +866,7 @@ mixin PaynymInterface ); } - return Tuple2(clTx.toHex(), clTx.size); + return Tuple2(clTx.toHex(), clTx.vSize()); } catch (e, s) { Logging.instance.log( "_createNotificationTx(): $e\n$s", From 2ab019f2014433651d80a7969bafc584b7a5b697 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 8 May 2024 23:35:55 -0500 Subject: [PATCH 240/272] add linux and windows secp256k1 build scripts in case coinlib's scripts aren't working TODO macos and ios (android?) --- scripts/linux/build_all.sh | 6 ++++-- scripts/windows/build_all.sh | 6 ++++-- scripts/windows/build_secp256k1.bat | 10 ++++++++++ scripts/windows/build_secp256k1.sh | 10 ++++++++++ scripts/windows/build_secp256k1_wsl.sh | 10 ++++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 scripts/windows/build_secp256k1.bat create mode 100644 scripts/windows/build_secp256k1.sh create mode 100644 scripts/windows/build_secp256k1_wsl.sh diff --git a/scripts/linux/build_all.sh b/scripts/linux/build_all.sh index 2b6bd1ffd..d7c29e1b1 100755 --- a/scripts/linux/build_all.sh +++ b/scripts/linux/build_all.sh @@ -14,9 +14,11 @@ mkdir -p build ./build_secure_storage_deps.sh & (cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_monero_all.sh && ./build_sharedfile.sh ) & -set_rust_to_1720 & +(cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_monero_all.sh && ./build_sharedfile.sh ) +set_rust_to_1720 (cd ../../crypto_plugins/frostdart/scripts/linux && ./build_all.sh ) & +./build_secp256k1.sh + wait echo "Done building" diff --git a/scripts/windows/build_all.sh b/scripts/windows/build_all.sh index 1a585e276..c055cb6c3 100755 --- a/scripts/windows/build_all.sh +++ b/scripts/windows/build_all.sh @@ -9,9 +9,11 @@ set_rust_to_1671 mkdir -p build (cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_liblelantus/scripts/windows && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/windows && ./build_all.sh) & -set_rust_to_1720 & +(cd ../../crypto_plugins/flutter_libmonero/scripts/windows && ./build_all.sh) +set_rust_to_1720 (cd ../../crypto_plugins/frostdart/scripts/windows && ./build_all.sh ) & +./build_secp256k1_wsl.sh + wait echo "Done building" diff --git a/scripts/windows/build_secp256k1.bat b/scripts/windows/build_secp256k1.bat new file mode 100644 index 000000000..9e7433032 --- /dev/null +++ b/scripts/windows/build_secp256k1.bat @@ -0,0 +1,10 @@ +if not exist "build" mkdir "build" +cd build +rem git clone https://github.com/bitcoin-core/secp256k1 +cd secp256k1 +rem cmake -G "Visual Studio 17 2022" -A x64 -S . -B build +cd build +rem cmake --build . +if not exist "..\..\..\..\build\" mkdir "..\..\..\..\build\" +xcopy src\Debug\libsecp256k1-2.dll "..\..\..\..\build\secp256k1.dll" /Y +cd ..\..\..\ diff --git a/scripts/windows/build_secp256k1.sh b/scripts/windows/build_secp256k1.sh new file mode 100644 index 000000000..6fdd9f58c --- /dev/null +++ b/scripts/windows/build_secp256k1.sh @@ -0,0 +1,10 @@ +mkdir -p build +cd build +git clone https://github.com/bitcoin-core/secp256k1 +cd secp256k1 +mkdir -p build && cd build +cmake .. +cmake --build . +mkdir -p ../../../../../build +cp src/libsecp256k1.so.2.2.1 "../../../../../build/libsecp256k1.so" +cd ../../../ diff --git a/scripts/windows/build_secp256k1_wsl.sh b/scripts/windows/build_secp256k1_wsl.sh new file mode 100644 index 000000000..b5d2e281f --- /dev/null +++ b/scripts/windows/build_secp256k1_wsl.sh @@ -0,0 +1,10 @@ +mkdir -p build +cd build +git clone https://github.com/bitcoin-core/secp256k1 +cd secp256k1 +mkdir -p build && cd build +cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/x86_64-w64-mingw32.toolchain.cmake +cmake --build . +mkdir -p ../../../../../build +cp src/libsecp256k1-2.dll "../../../../../build/secp256k1.dll" +cd ../../../ From f1cccfea650e71dfcbfcd2a003d86449e414598d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 9 May 2024 00:10:27 -0500 Subject: [PATCH 241/272] unix line endings for WSL2 script, move to linux dir --- scripts/{windows => linux}/build_secp256k1.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{windows => linux}/build_secp256k1.sh (100%) diff --git a/scripts/windows/build_secp256k1.sh b/scripts/linux/build_secp256k1.sh similarity index 100% rename from scripts/windows/build_secp256k1.sh rename to scripts/linux/build_secp256k1.sh From 66b14717aa2d593e8720b3d00aef826d8969fdad Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 8 May 2024 19:12:47 -0500 Subject: [PATCH 242/272] chmod +x build_secp256k1.sh --- scripts/linux/build_secp256k1.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/linux/build_secp256k1.sh diff --git a/scripts/linux/build_secp256k1.sh b/scripts/linux/build_secp256k1.sh old mode 100644 new mode 100755 From b91792a101f96efe04061574a055d68c690e37c0 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 8 May 2024 20:51:38 -0700 Subject: [PATCH 243/272] Replace vanished ATiltedTree/setup-rust with dtolnay/rust-toolchain. Update flutter version in github actions workflow to 3.19.6. Remove invalid inputs flutter-version and channel from checkout action. Update checkout action to v4. Consolidate redundant dependencies installation in GH actions. Don't install cargo and rustc debs when using rustup. --- .github/workflows/test.yaml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 15573b3a8..702940c60 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,19 +6,15 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Prepare repository - uses: actions/checkout@v3 - with: - flutter-version: '3.10.6' - channel: 'stable' + uses: actions/checkout@v4 - name: Install Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.16.0' + flutter-version: '3.19.6' channel: 'stable' - name: Setup | Rust - uses: ATiltedTree/setup-rust@v1 + uses: dtolnay/rust-toolchain@stable with: - rust-version: stable components: clippy - name: Checkout submodules run: git submodule update --init --recursive @@ -28,12 +24,7 @@ jobs: rustup target add x86_64-unknown-linux-gnu sudo apt clean sudo apt update - sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm - sudo apt install -y debhelper libclang-dev cargo rustc opencl-headers libssl-dev ocl-icd-opencl-dev - sudo apt install -y libc6-dev-i386 - sudo apt install -y build-essential cmake git libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev pkg-config llvm - sudo apt install -y build-essential debhelper cmake libclang-dev libncurses5-dev clang libncursesw5-dev cargo rustc opencl-headers libssl-dev pkg-config ocl-icd-opencl-dev - sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless + sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm debhelper libclang-dev opencl-headers libssl-dev ocl-icd-opencl-dev libc6-dev-i386 - name: Build Lelantus run: | cd crypto_plugins/flutter_liblelantus/scripts/linux/ From 370cb3f4a65655676af979bc810c49f98b9e071e Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 9 May 2024 10:10:34 -0600 Subject: [PATCH 244/272] require_trailing_commas lint rule --- analysis_options.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index cc16ffa53..c363d17cd 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -92,6 +92,7 @@ linter: 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 From 9bfac51926206c7d6824f5178007fe575379b7c9 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 9 May 2024 10:51:28 -0600 Subject: [PATCH 245/272] use a `switch` to exhaustively ensure we didn't forget to include a newly added Coin value --- .../sub_widgets/wallet_list_item.dart | 4 +- .../my_stack_view/wallet_summary_table.dart | 10 +- lib/supported_coins.dart | 113 ++++++++++-------- 3 files changed, 67 insertions(+), 60 deletions(-) diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index fada0b998..a708267a9 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -63,8 +63,8 @@ class WalletListItem extends ConsumerWidget { // Check if Tor is enabled... if (ref.read(prefsChangeNotifierProvider).useTor) { // ... and if the coin supports Tor. - final cryptocurrency = SupportedCoins.coins[coin]; - if (cryptocurrency != null && !cryptocurrency!.torSupport) { + final cryptocurrency = SupportedCoins.getCryptoCurrencyFor(coin); + if (!cryptocurrency.torSupport) { // If not, show a Tor warning dialog. final shouldContinue = await showDialog( context: context, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index 8785c7ac6..3b3c6f32f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -29,7 +29,7 @@ import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletSummaryTable extends ConsumerStatefulWidget { - const WalletSummaryTable({Key? key}) : super(key: key); + const WalletSummaryTable({super.key}); @override ConsumerState createState() => _WalletTableState(); @@ -70,10 +70,10 @@ class _WalletTableState extends ConsumerState { class DesktopWalletSummaryRow extends ConsumerStatefulWidget { const DesktopWalletSummaryRow({ - Key? key, + super.key, required this.coin, required this.walletCount, - }) : super(key: key); + }); final Coin coin; final int walletCount; @@ -91,8 +91,8 @@ class _DesktopWalletSummaryRowState // Check if Tor is enabled... if (ref.read(prefsChangeNotifierProvider).useTor) { // ... and if the coin supports Tor. - final cryptocurrency = SupportedCoins.coins[widget.coin]; - if (cryptocurrency != null && !cryptocurrency!.torSupport) { + final cryptocurrency = SupportedCoins.getCryptoCurrencyFor(widget.coin); + if (!cryptocurrency.torSupport) { // If not, show a Tor warning dialog. final shouldContinue = await showDialog( context: context, diff --git a/lib/supported_coins.dart b/lib/supported_coins.dart index ef28d8916..48960c03e 100644 --- a/lib/supported_coins.dart +++ b/lib/supported_coins.dart @@ -13,66 +13,73 @@ import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; -/// The supported coins. +/// The supported coins. Eventually move away from the Coin enum class SupportedCoins { - /// A List of our supported coins. - static final List cryptocurrencies = [ - // Mainnet coins. - Bitcoin(CryptoCurrencyNetwork.main), - Monero(CryptoCurrencyNetwork.main), - Banano(CryptoCurrencyNetwork.main), - Bitcoincash(CryptoCurrencyNetwork.main), - BitcoinFrost(CryptoCurrencyNetwork.main), - Dogecoin(CryptoCurrencyNetwork.main), - Ecash(CryptoCurrencyNetwork.main), - Epiccash(CryptoCurrencyNetwork.main), - Ethereum(CryptoCurrencyNetwork.main), - Firo(CryptoCurrencyNetwork.main), - Litecoin(CryptoCurrencyNetwork.main), - Namecoin(CryptoCurrencyNetwork.main), - Nano(CryptoCurrencyNetwork.main), - Particl(CryptoCurrencyNetwork.main), - Stellar(CryptoCurrencyNetwork.main), - Tezos(CryptoCurrencyNetwork.main), - Wownero(CryptoCurrencyNetwork.main), + /// A List of our supported coins. Piggy back on [Coin] for now + static final List cryptocurrencies = + Coin.values.map((e) => getCryptoCurrencyFor(e)).toList(growable: false); - /// Testnet coins. - Bitcoin(CryptoCurrencyNetwork.test), - Banano(CryptoCurrencyNetwork.test), - Bitcoincash(CryptoCurrencyNetwork.test), - BitcoinFrost(CryptoCurrencyNetwork.test), - Dogecoin(CryptoCurrencyNetwork.test), - Stellar(CryptoCurrencyNetwork.test), - Firo(CryptoCurrencyNetwork.test), - Litecoin(CryptoCurrencyNetwork.test), - Stellar(CryptoCurrencyNetwork.test), - ]; - - /// A Map linking a CryptoCurrency with its associated Coin. + /// A getter function linking a [CryptoCurrency] with its associated [Coin]. /// /// Temporary: Remove when the Coin enum is removed. - static final Map coins = { - Coin.bitcoin: Bitcoin(CryptoCurrencyNetwork.main), - Coin.monero: Monero(CryptoCurrencyNetwork.main), - Coin.banano: Banano(CryptoCurrencyNetwork.main), - Coin.bitcoincash: Bitcoincash(CryptoCurrencyNetwork.main), - Coin.bitcoinFrost: BitcoinFrost(CryptoCurrencyNetwork.main), - Coin.dogecoin: Dogecoin(CryptoCurrencyNetwork.main), - Coin.eCash: Ecash(CryptoCurrencyNetwork.main), - Coin.epicCash: Epiccash(CryptoCurrencyNetwork.main), - Coin.ethereum: Ethereum(CryptoCurrencyNetwork.main), - Coin.firo: Firo(CryptoCurrencyNetwork.main), - Coin.litecoin: Litecoin(CryptoCurrencyNetwork.main), - Coin.namecoin: Namecoin(CryptoCurrencyNetwork.main), - Coin.nano: Nano(CryptoCurrencyNetwork.main), - Coin.particl: Particl(CryptoCurrencyNetwork.main), - Coin.stellar: Stellar(CryptoCurrencyNetwork.main), - Coin.tezos: Tezos(CryptoCurrencyNetwork.main), - Coin.wownero: Wownero(CryptoCurrencyNetwork.main), - }; + static CryptoCurrency getCryptoCurrencyFor(Coin coin) { + switch (coin) { + case Coin.bitcoin: + return Bitcoin(CryptoCurrencyNetwork.main); + case Coin.bitcoinFrost: + return BitcoinFrost(CryptoCurrencyNetwork.main); + case Coin.litecoin: + return Litecoin(CryptoCurrencyNetwork.main); + case Coin.bitcoincash: + return Bitcoincash(CryptoCurrencyNetwork.main); + case Coin.dogecoin: + return Dogecoin(CryptoCurrencyNetwork.main); + case Coin.epicCash: + return Epiccash(CryptoCurrencyNetwork.main); + case Coin.eCash: + return Ecash(CryptoCurrencyNetwork.main); + case Coin.ethereum: + return Ethereum(CryptoCurrencyNetwork.main); + case Coin.firo: + return Firo(CryptoCurrencyNetwork.main); + case Coin.monero: + return Monero(CryptoCurrencyNetwork.main); + case Coin.particl: + return Particl(CryptoCurrencyNetwork.main); + case Coin.solana: + return Solana(CryptoCurrencyNetwork.main); + case Coin.stellar: + return Stellar(CryptoCurrencyNetwork.main); + case Coin.tezos: + return Tezos(CryptoCurrencyNetwork.main); + case Coin.wownero: + return Wownero(CryptoCurrencyNetwork.main); + case Coin.namecoin: + return Namecoin(CryptoCurrencyNetwork.main); + case Coin.nano: + return Nano(CryptoCurrencyNetwork.main); + case Coin.banano: + return Banano(CryptoCurrencyNetwork.main); + case Coin.bitcoinTestNet: + return Bitcoin(CryptoCurrencyNetwork.test); + case Coin.bitcoinFrostTestNet: + return BitcoinFrost(CryptoCurrencyNetwork.test); + case Coin.litecoinTestNet: + return Litecoin(CryptoCurrencyNetwork.test); + case Coin.bitcoincashTestnet: + return Bitcoincash(CryptoCurrencyNetwork.test); + case Coin.firoTestNet: + return Firo(CryptoCurrencyNetwork.test); + case Coin.dogecoinTestNet: + return Dogecoin(CryptoCurrencyNetwork.test); + case Coin.stellarTestnet: + return Stellar(CryptoCurrencyNetwork.test); + } + } } From 12a0e4c289428998c751bd2b11e51d8660807adc Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 9 May 2024 11:56:42 -0600 Subject: [PATCH 246/272] lints --- .../spark_interface.dart | 265 ++++++++++-------- 1 file changed, 146 insertions(+), 119 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 83e064dc9..b552a07d0 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -188,11 +188,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final transparentSumOut = (txData.recipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e); + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e, + ); // See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17 // and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17 @@ -203,16 +204,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { fractionDigits: cryptoCurrency.fractionDigits, )) { throw Exception( - "Spend to transparent address limit exceeded (10,000 Firo per transaction)."); + "Spend to transparent address limit exceeded (10,000 Firo per transaction).", + ); } final sparkSumOut = (txData.sparkRecipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e); + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e, + ); final txAmount = transparentSumOut + sparkSumOut; @@ -239,12 +242,14 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // prepare coin data for ffi final serializedCoins = coins - .map((e) => ( - serializedCoin: e.serializedCoinB64!, - serializedCoinContext: e.contextB64!, - groupId: e.groupId, - height: e.height!, - )) + .map( + (e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + ), + ) .toList(); final currentId = await electrumXClient.getSparkLatestCoinId(); @@ -266,16 +271,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final allAnonymitySets = setMaps - .map((e) => ( - setId: e["coinGroupID"] as int, - setHash: e["setHash"] as String, - set: (e["coins"] as List) - .map((e) => ( - serializedCoin: e[0] as String, - txHash: e[1] as String, - )) - .toList(), - )) + .map( + (e) => ( + setId: e["coinGroupID"] as int, + setHash: e["setHash"] as String, + set: (e["coins"] as List) + .map( + (e) => ( + serializedCoin: e[0] as String, + txHash: e[1] as String, + ), + ) + .toList(), + ), + ) .toList(); final root = await getRootHDNode(); @@ -437,27 +446,32 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { privateKeyHex: privateKey.toHex, index: kDefaultSparkIndex, recipients: txData.recipients - ?.map((e) => ( - address: e.address, - amount: e.amount.raw.toInt(), - subtractFeeFromAmount: isSendAll, - )) + ?.map( + (e) => ( + address: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + ), + ) .toList() ?? [], privateRecipients: txData.sparkRecipients - ?.map((e) => ( - sparkAddress: e.address, - amount: e.amount.raw.toInt(), - subtractFeeFromAmount: isSendAll, - memo: e.memo, - )) + ?.map( + (e) => ( + sparkAddress: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + memo: e.memo, + ), + ) .toList() ?? [], serializedCoins: serializedCoins, allAnonymitySets: allAnonymitySets, idAndBlockHashes: idAndBlockHashes .map( - (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash))) + (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash)), + ) .toList(), txHash: extractedTx.getHash(), ), @@ -504,16 +518,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (final usedCoin in spend.usedCoins) { try { - usedSparkCoins.add(coins - .firstWhere((e) => - usedCoin.height == e.height && - usedCoin.groupId == e.groupId && - base64Decode(e.serializedCoinB64!) - .toHex - .startsWith(base64Decode(usedCoin.serializedCoin).toHex)) - .copyWith( - isUsed: true, - )); + usedSparkCoins.add( + coins + .firstWhere( + (e) => + usedCoin.height == e.height && + usedCoin.groupId == e.groupId && + base64Decode(e.serializedCoinB64!).toHex.startsWith( + base64Decode(usedCoin.serializedCoin).toHex, + ), + ) + .copyWith( + isUsed: true, + ), + ); } catch (_) { throw Exception( "Unexpectedly did not find used spark coin. This should never happen.", @@ -576,8 +594,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { return await updateSentCachedTxData(txData: txData); } catch (e, s) { - Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", - level: LogLevel.Error); + Logging.instance.log( + "Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error, + ); rethrow; } } @@ -695,9 +715,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); final spendable = Amount( rawValue: unusedCoins - .where((e) => - e.height != null && - e.height! + cryptoCurrency.minConfirms <= currentHeight) + .where( + (e) => + e.height != null && + e.height! + cryptoCurrency.minConfirms <= currentHeight, + ) .map((e) => e.value) .fold(BigInt.zero, (prev, e) => prev + e), fractionDigits: cryptoCurrency.fractionDigits, @@ -802,7 +824,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } // organise utxos - Map> utxosByAddress = {}; + final Map> utxosByAddress = {}; for (final utxo in availableUtxos) { utxosByAddress[utxo.address!] ??= []; utxosByAddress[utxo.address!]!.add(utxo); @@ -811,7 +833,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // setup some vars int nChangePosInOut = -1; - int nChangePosRequest = nChangePosInOut; + final int nChangePosRequest = nChangePosInOut; List outputs_ = outputs .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) .toList(); // deep copy @@ -934,11 +956,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // Generate dummy mint coins to save time final dummyRecipients = LibSpark.createSparkMintRecipients( outputs: singleTxOutputs - .map((e) => ( - sparkAddress: e.address, - value: e.value.toInt(), - memo: "", - )) + .map( + (e) => ( + sparkAddress: e.address, + value: e.value.toInt(), + memo: "", + ), + ) .toList(), serialContext: Uint8List(0), generate: false, @@ -952,11 +976,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { if (recipient.amount < cryptoCurrency.dustLimit.raw.toInt()) { throw Exception("Output amount too small"); } - vout.add(( - recipient.scriptPubKey, - recipient.amount, - singleTxOutputs[i].address, - )); + vout.add( + ( + recipient.scriptPubKey, + recipient.amount, + singleTxOutputs[i].address, + ), + ); } // Choose coins to use @@ -973,7 +999,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // priority stuff??? - BigInt nChange = nValueIn - nValueToSelect; + final BigInt nChange = nValueIn - nValueToSelect; if (nChange > BigInt.zero) { if (nChange < cryptoCurrency.dustLimit.raw) { nChangePosInOut = -1; @@ -1106,10 +1132,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // Generate real mint coins final serialContext = LibSpark.serializeMintContext( inputs: setCoins - .map((e) => ( - e.utxo.txid, - e.utxo.vout, - )) + .map( + (e) => ( + e.utxo.txid, + e.utxo.vout, + ), + ) .toList(), ); final recipients = LibSpark.createSparkMintRecipients( @@ -1126,7 +1154,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { generate: true, ); - int i = 0; + final int i = 0; for (int i = 0; i < recipients.length; i++) { final recipient = recipients[i]; final out = ( @@ -1267,9 +1295,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .where() .walletIdEqualTo(walletId) .filter() - .valueEqualTo(addressOrScript is Uint8List - ? output.$3! - : addressOrScript as String) + .valueEqualTo( + addressOrScript is Uint8List + ? output.$3! + : addressOrScript as String, + ) .valueProperty() .findFirst()) != null, @@ -1643,42 +1673,38 @@ Future< String serializedCoinContext })> usedCoins, })> _createSparkSend( - ({ - String privateKeyHex, - int index, - List< - ({ - String address, - int amount, - bool subtractFeeFromAmount - })> recipients, - List< - ({ - String sparkAddress, - int amount, - bool subtractFeeFromAmount, - String memo - })> privateRecipients, - List< - ({ - String serializedCoin, - String serializedCoinContext, - int groupId, - int height, - })> serializedCoins, - List< - ({ - int setId, - String setHash, - List<({String serializedCoin, String txHash})> set - })> allAnonymitySets, - List< - ({ - int setId, - Uint8List blockHash, - })> idAndBlockHashes, - Uint8List txHash, - }) args) async { + ({ + String privateKeyHex, + int index, + List<({String address, int amount, bool subtractFeeFromAmount})> recipients, + List< + ({ + String sparkAddress, + int amount, + bool subtractFeeFromAmount, + String memo + })> privateRecipients, + List< + ({ + String serializedCoin, + String serializedCoinContext, + int groupId, + int height, + })> serializedCoins, + List< + ({ + int setId, + String setHash, + List<({String serializedCoin, String txHash})> set + })> allAnonymitySets, + List< + ({ + int setId, + Uint8List blockHash, + })> idAndBlockHashes, + Uint8List txHash, + }) args, +) async { final spend = LibSpark.createSparkSendTransaction( privateKeyHex: args.privateKeyHex, index: args.index, @@ -1695,14 +1721,15 @@ Future< /// Top level function which should be called wrapped in [compute] Future> _identifyCoins( - ({ - List anonymitySetCoins, - int groupId, - Set spentCoinTags, - Set privateKeyHexSet, - String walletId, - bool isTestNet, - }) args) async { + ({ + List anonymitySetCoins, + int groupId, + Set spentCoinTags, + Set privateKeyHexSet, + String walletId, + bool isTestNet, + }) args, +) async { final List myCoins = []; for (final privateKeyHex in args.privateKeyHexSet) { From cccf1a7012f86b4581637a5a991dc79d34670765 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 9 May 2024 12:25:32 -0600 Subject: [PATCH 247/272] don't attempt to identify already checked used coin tags and add some more logging --- .../spark_interface.dart | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index b552a07d0..f25f22323 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -602,9 +602,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } } - // TODO lots of room for performance improvements here. Should be similar to - // recoverSparkWallet but only fetch and check anonymity set data that we - // have not yet parsed. Future refreshSparkData() async { final sparkAddresses = await mainDB.isar.addresses .where() @@ -620,6 +617,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); final blockHash = await _getCachedSparkBlockHash(); + final startNumber = await _getSparkCoinsStartNumber(); final anonymitySetFuture = blockHash == null ? electrumXCachedClient.getSparkAnonymitySet( @@ -630,9 +628,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { coinGroupId: latestSparkCoinId.toString(), startBlockHash: blockHash, ); - final spentCoinTagsFuture = - electrumXClient.getSparkUsedCoinsTags(startNumber: 0); - // electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin); + + final spentCoinTagsFuture = startNumber == null + ? electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin) + : electrumXClient.getSparkUsedCoinsTags(startNumber: startNumber); final futureResults = await Future.wait([ anonymitySetFuture, @@ -670,7 +669,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // update blockHash in cache final String newBlockHash = base64ToReverseHex(anonymitySet["blockHash"] as String); - await _setCachedSparkBlockHash(newBlockHash); + + await Future.wait([ + _setCachedSparkBlockHash(newBlockHash), + _setSparkCoinsStartNumber(spentCoinTags.length - 1), + ]); } // check current coins @@ -692,8 +695,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // refresh spark balance await refreshSparkBalance(); } catch (e, s) { - // todo logging - + Logging.instance.log( + "$runtimeType $walletId ${info.name}: $e\n$s", + level: LogLevel.Error, + ); rethrow; } } @@ -790,8 +795,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // refresh spark balance await refreshSparkBalance(); } catch (e, s) { - // todo logging - + Logging.instance.log( + "$runtimeType $walletId ${info.name}: $e\n$s", + level: LogLevel.Error, + ); rethrow; } } @@ -1154,7 +1161,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { generate: true, ); - final int i = 0; for (int i = 0; i < recipients.length; i++) { final recipient = recipients[i]; final out = ( @@ -1597,6 +1603,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // ====================== Private ============================================ final _kSparkAnonSetCachedBlockHashKey = "SparkAnonSetCachedBlockHashKey"; + final _kSparkCoinsStartNumberKey = "SparkCoinsStartNumberKey"; Future _getCachedSparkBlockHash() async { return info.otherData[_kSparkAnonSetCachedBlockHashKey] as String?; @@ -1609,6 +1616,17 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); } + Future _getSparkCoinsStartNumber() async { + return info.otherData[_kSparkCoinsStartNumberKey] as int?; + } + + Future _setSparkCoinsStartNumber(int startNumber) async { + await info.updateOtherData( + newEntries: {_kSparkCoinsStartNumberKey: startNumber}, + isar: mainDB.isar, + ); + } + Future _addOrUpdateSparkCoins(List coins) async { if (coins.isNotEmpty) { await mainDB.isar.writeTxn(() async { From 51807ac8e69fd7f3bda6ab82606c4294d2bec1de Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 9 May 2024 12:44:56 -0600 Subject: [PATCH 248/272] lints clean up --- lib/electrumx_rpc/electrumx_client.dart | 143 +++++++++++++++--------- 1 file changed, 93 insertions(+), 50 deletions(-) diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 768b90fc5..a5fcf5605 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -210,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 { @@ -392,7 +396,7 @@ class ElectrumXClient { } try { - var futures = >[]; + final futures = >[]; getElectrumAdapter()!.peer.withBatch(() { for (final arg in args) { futures.add(getElectrumAdapter()!.request(command, arg)); @@ -765,12 +769,16 @@ class ElectrumXClient { bool verbose = true, String? requestID, }) async { - Logging.instance.log("attempting to fetch blockchain.transaction.get...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch blockchain.transaction.get...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); - dynamic response = await getElectrumAdapter()!.getTransaction(txHash); - Logging.instance.log("Fetching blockchain.transaction.get finished", - level: LogLevel.Info); + 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}; @@ -798,14 +806,18 @@ class ElectrumXClient { String blockhash = "", String? requestID, }) async { - Logging.instance.log("attempting to fetch lelantus.getanonymityset...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch lelantus.getanonymityset...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); - Map response = + final Map response = await (getElectrumAdapter() as FiroElectrumClient) .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); - Logging.instance.log("Fetching lelantus.getanonymityset finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching lelantus.getanonymityset finished", + level: LogLevel.Info, + ); return response; } @@ -817,13 +829,17 @@ class ElectrumXClient { dynamic mints, String? requestID, }) async { - Logging.instance.log("attempting to fetch lelantus.getmintmetadata...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch lelantus.getmintmetadata...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); - dynamic response = await (getElectrumAdapter() as FiroElectrumClient) + 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; } @@ -833,8 +849,10 @@ class ElectrumXClient { String? requestID, required int startNumber, }) async { - Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch lelantus.getusedcoinserials...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); int retryCount = 3; @@ -844,8 +862,10 @@ class ElectrumXClient { 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--; } @@ -857,13 +877,17 @@ class ElectrumXClient { /// /// ex: 1 Future getLelantusLatestCoinId({String? requestID}) async { - Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch lelantus.getlatestcoinid...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); - int response = + final int response = await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId(); - Logging.instance.log("Fetching lelantus.getlatestcoinid finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching lelantus.getlatestcoinid finished", + level: LogLevel.Info, + ); return response; } @@ -888,15 +912,21 @@ class ElectrumXClient { String? requestID, }) async { try { - Logging.instance.log("attempting to fetch spark.getsparkanonymityset...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch spark.getsparkanonymityset...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); - Map response = + final Map response = await (getElectrumAdapter() as FiroElectrumClient) .getSparkAnonymitySet( - coinGroupId: coinGroupId, startBlockHash: startBlockHash); - Logging.instance.log("Fetching spark.getsparkanonymityset finished", - level: LogLevel.Info); + coinGroupId: coinGroupId, + startBlockHash: startBlockHash, + ); + Logging.instance.log( + "Fetching spark.getsparkanonymityset finished", + level: LogLevel.Info, + ); return response; } catch (e) { rethrow; @@ -911,16 +941,20 @@ class ElectrumXClient { }) async { try { // Use electrum_adapter package's getSparkUsedCoinsTags method. - Logging.instance.log("attempting to fetch spark.getusedcoinstags...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch spark.getusedcoinstags...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); - Map response = + final Map response = await (getElectrumAdapter() as FiroElectrumClient) .getUsedCoinsTags(startNumber: startNumber); // TODO: Add 2 minute timeout. // Why 2 minutes? - Logging.instance.log("Fetching spark.getusedcoinstags finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching spark.getusedcoinstags finished", + level: LogLevel.Info, + ); final map = Map.from(response); final set = Set.from(map["tags"] as List); return await compute(_ffiHashTagsComputeWrapper, set); @@ -945,14 +979,18 @@ class ElectrumXClient { required List sparkCoinHashes, }) async { try { - Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch spark.getsparkmintmetadata...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); - List response = + final List response = await (getElectrumAdapter() as FiroElectrumClient) .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes); - Logging.instance.log("Fetching spark.getsparkmintmetadata finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching spark.getsparkmintmetadata finished", + level: LogLevel.Info, + ); return List>.from(response); } catch (e) { Logging.instance.log(e, level: LogLevel.Error); @@ -967,13 +1005,17 @@ class ElectrumXClient { String? requestID, }) async { try { - Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch spark.getsparklatestcoinid...", + level: LogLevel.Info, + ); await _checkElectrumAdapter(); - int response = await (getElectrumAdapter() as FiroElectrumClient) + 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); @@ -995,7 +1037,8 @@ class ElectrumXClient { 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: From 9f4df0368ae3c9ea8408609d40851b4a57c0f65f Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 9 May 2024 16:12:22 -0600 Subject: [PATCH 249/272] add extra info to spark transaction generating dialog and some linter clean up --- .../exchange_step_views/step_4_view.dart | 53 ++- lib/pages/exchange_view/send_from_view.dart | 82 ++-- lib/pages/send_view/send_view.dart | 434 ++++++++++-------- .../building_transaction_dialog.dart | 49 +- lib/pages/send_view/token_send_view.dart | 185 +++++--- .../wallet_view/sub_widgets/desktop_send.dart | 137 +++--- .../sub_widgets/desktop_token_send.dart | 87 ++-- 7 files changed, 620 insertions(+), 407 deletions(-) diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index c36e88bbf..8fa0eedc6 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -225,6 +225,7 @@ class _Step4ViewState extends ConsumerState { builder: (context) { return BuildingTransactionDialog( coin: wallet.info.coin, + isSpark: wallet is FiroWallet && !firoPublicSend, onCancel: () { wasCancelled = true; }, @@ -249,7 +250,7 @@ class _Step4ViewState extends ConsumerState { address: address, amount: amount, isChange: false, - ) + ), ], note: "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", @@ -472,10 +473,10 @@ class _Step4ViewState extends ConsumerState { GestureDetector( onTap: () async { final data = ClipboardData( - text: - model.sendAmount.toString()); + text: model.sendAmount.toString(), + ); await clipboard.setData(data); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -535,9 +536,10 @@ class _Step4ViewState extends ConsumerState { GestureDetector( onTap: () async { final data = ClipboardData( - text: model.trade!.payInAddress); + text: model.trade!.payInAddress, + ); await clipboard.setData(data); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -598,10 +600,10 @@ class _Step4ViewState extends ConsumerState { GestureDetector( onTap: () async { final data = ClipboardData( - text: - model.trade!.payInExtraId); + text: model.trade!.payInExtraId, + ); await clipboard.setData(data); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -670,9 +672,10 @@ class _Step4ViewState extends ConsumerState { GestureDetector( onTap: () async { final data = ClipboardData( - text: model.trade!.tradeId); + text: model.trade!.tradeId, + ); await clipboard.setData(data); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -689,9 +692,9 @@ class _Step4ViewState extends ConsumerState { .infoItemIcons, width: 12, ), - ) + ), ], - ) + ), ], ), ), @@ -739,7 +742,8 @@ class _Step4ViewState extends ConsumerState { child: Text( "Send ${model.sendTicker} to this address", style: STextStyles.pageTitleH2( - context), + context, + ), ), ), const SizedBox( @@ -773,12 +777,13 @@ class _Step4ViewState extends ConsumerState { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle( - context), + context, + ), child: Text( "Cancel", style: STextStyles.button( - context) - .copyWith( + context, + ).copyWith( color: Theme.of(context) .extension< StackColors>()! @@ -788,7 +793,7 @@ class _Step4ViewState extends ConsumerState { ), ), ], - ) + ), ], ), ); @@ -814,8 +819,9 @@ class _Step4ViewState extends ConsumerState { final tuple = ref .read( - exchangeSendFromWalletIdStateProvider - .state) + exchangeSendFromWalletIdStateProvider + .state, + ) .state; if (tuple != null && model.sendTicker.toLowerCase() == @@ -845,8 +851,8 @@ class _Step4ViewState extends ConsumerState { (BuildContext context) { final coin = coinFromTickerCaseInsensitive( - model.trade! - .payInCurrency); + model.trade!.payInCurrency, + ); return SendFromView( coin: coin, amount: model.sendAmount @@ -868,7 +874,8 @@ class _Step4ViewState extends ConsumerState { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle( - context), + context, + ), child: Text( buttonTitle, style: diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 655f2afe7..a97b94a28 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -205,13 +205,13 @@ class _SendFromViewState extends ConsumerState { class SendFromCard extends ConsumerStatefulWidget { const SendFromCard({ - Key? key, + super.key, required this.walletId, required this.amount, required this.address, required this.trade, this.fromDesktopStep4 = false, - }) : super(key: key); + }); final String walletId; final Amount amount; @@ -235,6 +235,8 @@ class _SendFromCardState extends ConsumerState { try { bool wasCancelled = false; + final wallet = ref.read(pWallets).getWallet(walletId); + unawaited( showDialog( context: context, @@ -253,6 +255,8 @@ class _SendFromCardState extends ConsumerState { ), child: BuildingTransactionDialog( coin: coin, + isSpark: + wallet is FiroWallet && shouldSendPublicFiroFunds != true, onCancel: () { wasCancelled = true; @@ -273,8 +277,6 @@ class _SendFromCardState extends ConsumerState { TxData txData; Future txDataFuture; - final wallet = ref.read(pWallets).getWallet(walletId); - // if not firo then do normal send if (shouldSendPublicFiroFunds == null) { final memo = coin == Coin.stellar || coin == Coin.stellarTestnet @@ -371,38 +373,38 @@ class _SendFromCardState extends ConsumerState { } } } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); + if (mounted) { + // pop building dialog + Navigator.of(context).pop(); - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), ), + onPressed: () { + Navigator.of(context).pop(); + }, ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - ); - // } + ); + }, + ); + } } } @@ -420,7 +422,8 @@ class _SendFromCardState extends ConsumerState { final wallet = ref.watch(pWallets).getWallet(walletId); final locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale)); + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); final coin = ref.watch(pWalletCoin(walletId)); @@ -483,9 +486,11 @@ class _SendFromCardState extends ConsumerState { style: STextStyles.itemSubtitle(context), ), Text( - ref.watch(pAmountFormatter(coin)).format(ref - .watch(pWalletBalanceTertiary(walletId)) - .spendable), + ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pWalletBalanceTertiary(walletId)) + .spendable, + ), style: STextStyles.itemSubtitle(context), ), ], @@ -637,7 +642,8 @@ class _SendFromCardState extends ConsumerState { if (!isFiro) Text( ref.watch(pAmountFormatter(coin)).format( - ref.watch(pWalletBalance(walletId)).spendable), + ref.watch(pWalletBalance(walletId)).spendable, + ), style: STextStyles.itemSubtitle(context), ), ], diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 07cdcfd8a..fc0b89dc3 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -154,8 +154,10 @@ class _SendViewState extends ConsumerState { // .state = true, // ); - Logging.instance.log("qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); final results = AddressUtils.parseUri(qrResult.rawContent); @@ -213,8 +215,9 @@ class _SendViewState extends ConsumerState { // here we ignore the exception caused by not giving permission // to use the camera to scan a qr code Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } } @@ -280,8 +283,10 @@ class _SendViewState extends ConsumerState { return; } _cachedAmountToSend = amount; - Logging.instance.log("it changed $amount $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $amount $_cachedAmountToSend", + level: LogLevel.Info, + ); final price = ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; @@ -572,9 +577,10 @@ class _SendViewState extends ConsumerState { child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), onPressed: () { Navigator.of(context).pop(false); @@ -616,6 +622,9 @@ class _SendViewState extends ConsumerState { builder: (context) { return BuildingTransactionDialog( coin: wallet.info.coin, + isSpark: wallet is FiroWallet && + ref.read(publicPrivateBalanceStateProvider.state).state == + FiroType.spark, onCancel: () { wasCancelled = true; @@ -645,7 +654,7 @@ class _SendViewState extends ConsumerState { address: widget.accountLite!.code, amount: amount, isChange: false, - ) + ), ], satsPerVByte: isCustomFee ? customFeeRate : null, feeRateType: feeRate, @@ -668,7 +677,7 @@ class _SendViewState extends ConsumerState { amount: amount, memo: memoController.text, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -687,7 +696,7 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -709,7 +718,7 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], ), ); @@ -725,7 +734,7 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], sparkRecipients: ref.read(pValidSparkSendToAddress) ? [ @@ -734,7 +743,7 @@ class _SendViewState extends ConsumerState { amount: amount, memo: memoController.text, isChange: false, - ) + ), ] : null, ), @@ -752,7 +761,7 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], memo: memo, feeRateType: ref.read(feeRateTypeStateProvider), @@ -827,9 +836,10 @@ class _SendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), onPressed: () { Navigator.of(context).pop(); @@ -981,7 +991,8 @@ class _SendViewState extends ConsumerState { debugPrint("BUILD: $runtimeType"); final wallet = ref.watch(pWallets).getWallet(walletId); final String locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale)); + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); final showCoinControl = wallet is CoinControlInterface && ref.watch( @@ -1033,7 +1044,7 @@ class _SendViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 50)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -1118,82 +1129,93 @@ class _SendViewState extends ConsumerState { ], ), const Spacer(), - Builder(builder: (context) { - final Amount amount; - if (isFiro) { - switch (ref - .watch( + Builder( + builder: (context) { + final Amount amount; + if (isFiro) { + switch (ref + .watch( publicPrivateBalanceStateProvider - .state) - .state) { - case FiroType.public: - amount = ref - .read(pWalletBalance(walletId)) - .spendable; - break; - case FiroType.lelantus: - amount = ref - .read(pWalletBalanceSecondary( - walletId)) - .spendable; - break; - case FiroType.spark: - amount = ref - .read(pWalletBalanceTertiary( - walletId)) - .spendable; - break; - } - } else { - amount = ref - .read(pWalletBalance(walletId)) - .spendable; - } - - return GestureDetector( - onTap: () { - cryptoAmountController.text = ref - .read(pAmountFormatter(coin)) - .format( - amount, - withUnitName: false, - ); - }, - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - Text( - ref - .watch(pAmountFormatter(coin)) - .format(amount), - style: STextStyles.titleBold12( - context) - .copyWith( - fontSize: 10, - ), - textAlign: TextAlign.right, - ), - Text( - "${(amount.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", - style: - STextStyles.subtitle(context) - .copyWith( - fontSize: 8, - ), - textAlign: TextAlign.right, + .state, ) - ], + .state) { + case FiroType.public: + amount = ref + .read(pWalletBalance(walletId)) + .spendable; + break; + case FiroType.lelantus: + amount = ref + .read( + pWalletBalanceSecondary( + walletId, + ), + ) + .spendable; + break; + case FiroType.spark: + amount = ref + .read( + pWalletBalanceTertiary( + walletId, + ), + ) + .spendable; + break; + } + } else { + amount = ref + .read(pWalletBalance(walletId)) + .spendable; + } + + return GestureDetector( + onTap: () { + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format( + amount, + withUnitName: false, + ); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + ref + .watch( + pAmountFormatter(coin), + ) + .format(amount), + style: STextStyles.titleBold12( + context, + ).copyWith( + fontSize: 10, + ), + textAlign: TextAlign.right, + ), + Text( + "${(amount.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount( + fractionDigits: 2, + ).fiatString( + locale: locale, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.subtitle( + context, + ).copyWith( + fontSize: 8, + ), + textAlign: TextAlign.right, + ), + ], + ), ), - ), - ); - }), + ); + }, + ), ], ), ), @@ -1295,12 +1317,14 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Clear Button. Clears The Address Field Input.", key: const Key( - "sendViewClearAddressFieldButtonKey"), + "sendViewClearAddressFieldButtonKey", + ), onTap: () { sendToController.text = ""; _address = ""; _setValidAddressProviders( - _address); + _address, + ); setState(() { _addressToggleFlag = false; @@ -1312,12 +1336,13 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Paste Button. Pastes From Clipboard To Address Field Input.", key: const Key( - "sendViewPasteAddressFieldButtonKey"), + "sendViewPasteAddressFieldButtonKey", + ), onTap: () async { final ClipboardData? data = await clipboard.getData( - Clipboard - .kTextPlain); + Clipboard.kTextPlain, + ); if (data?.text != null && data! .text!.isNotEmpty) { @@ -1327,23 +1352,27 @@ class _SendViewState extends ConsumerState { .contains("\n")) { content = content.substring( - 0, - content.indexOf( - "\n")); + 0, + content.indexOf( + "\n", + ), + ); } if (coin == Coin.epicCash) { // strip http:// and https:// if content contains @ content = formatAddress( - content); + content, + ); } sendToController.text = content.trim(); _address = content.trim(); _setValidAddressProviders( - _address); + _address, + ); setState(() { _addressToggleFlag = sendToController @@ -1362,7 +1391,8 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Address Book Button. Opens Address Book For Address Field.", key: const Key( - "sendViewAddressBookButtonKey"), + "sendViewAddressBookButtonKey", + ), onTap: () { Navigator.of(context).pushNamed( AddressBookView.routeName, @@ -1376,7 +1406,8 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Scan QR Button. Opens Camera For Scanning QR Code.", key: const Key( - "sendViewScanQrButtonKey"), + "sendViewScanQrButtonKey", + ), onTap: _scanQr, child: const QrCodeIcon(), ), @@ -1393,7 +1424,8 @@ class _SendViewState extends ConsumerState { if (isStellar || (ref.watch(pValidSparkSendToAddress) && ref.watch( - publicPrivateBalanceStateProvider) != + publicPrivateBalanceStateProvider, + ) != FiroType.lelantus)) ClipRRect( borderRadius: BorderRadius.circular( @@ -1440,7 +1472,8 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Clear Button. Clears The Memo Field Input.", key: const Key( - "sendViewClearMemoFieldButtonKey"), + "sendViewClearMemoFieldButtonKey", + ), onTap: () { memoController.text = ""; setState(() {}); @@ -1451,16 +1484,17 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Paste Button. Pastes From Clipboard To Memo Field Input.", key: const Key( - "sendViewPasteMemoFieldButtonKey"), + "sendViewPasteMemoFieldButtonKey", + ), onTap: () async { final ClipboardData? data = await clipboard.getData( - Clipboard - .kTextPlain); + Clipboard.kTextPlain, + ); if (data?.text != null && data! .text!.isNotEmpty) { - String content = + final String content = data.text!.trim(); memoController.text = @@ -1486,13 +1520,15 @@ class _SendViewState extends ConsumerState { error = null; } else if (isFiro) { if (ref.watch( - publicPrivateBalanceStateProvider) == + publicPrivateBalanceStateProvider, + ) == FiroType.lelantus) { if (_data != null && _data!.contactLabel == _address) { error = SparkInterface.validateSparkAddress( - address: _data!.address, - isTestNet: coin.isTestNet) + address: _data!.address, + isTestNet: coin.isTestNet, + ) ? "Unsupported" : null; } else if (ref @@ -1611,51 +1647,65 @@ class _SendViewState extends ConsumerState { Text( "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", style: STextStyles.itemSubtitle12( - context), + context, + ), ), const SizedBox( width: 10, ), - Builder(builder: (_) { - final Amount amount; - switch (ref - .read( + Builder( + builder: (_) { + final Amount amount; + switch (ref + .read( publicPrivateBalanceStateProvider - .state) - .state) { - case FiroType.public: - amount = ref - .watch(pWalletBalance( - walletId)) - .spendable; - break; - case FiroType.lelantus: - amount = ref - .watch( + .state, + ) + .state) { + case FiroType.public: + amount = ref + .watch( + pWalletBalance( + walletId, + ), + ) + .spendable; + break; + case FiroType.lelantus: + amount = ref + .watch( pWalletBalanceSecondary( - walletId)) - .spendable; - break; - case FiroType.spark: - amount = ref - .watch( + walletId, + ), + ) + .spendable; + break; + case FiroType.spark: + amount = ref + .watch( pWalletBalanceTertiary( - walletId)) - .spendable; - break; - } + walletId, + ), + ) + .spendable; + break; + } - return Text( - ref - .watch( - pAmountFormatter(coin)) - .format( - amount, - ), - style: STextStyles.itemSubtitle( - context), - ); - }), + return Text( + ref + .watch( + pAmountFormatter(coin), + ) + .format( + amount, + ), + style: + STextStyles.itemSubtitle( + context, + ), + ); + }, + ), ], ), SvgPicture.asset( @@ -1669,7 +1719,7 @@ class _SendViewState extends ConsumerState { ], ), ), - ) + ), ], ), const SizedBox( @@ -1691,8 +1741,9 @@ class _SendViewState extends ConsumerState { final Amount amount; switch (ref .read( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state) { case FiroType.public: amount = ref @@ -1701,14 +1752,20 @@ class _SendViewState extends ConsumerState { break; case FiroType.lelantus: amount = ref - .read(pWalletBalanceSecondary( - walletId)) + .read( + pWalletBalanceSecondary( + walletId, + ), + ) .spendable; break; case FiroType.spark: amount = ref - .read(pWalletBalanceTertiary( - walletId)) + .read( + pWalletBalanceTertiary( + walletId, + ), + ) .spendable; break; } @@ -1793,9 +1850,10 @@ class _SendViewState extends ConsumerState { .unitForCoin(coin), style: STextStyles.smallMed14(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1855,13 +1913,16 @@ class _SendViewState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(12), child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), + ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + ), style: STextStyles.smallMed14(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1898,7 +1959,7 @@ class _SendViewState extends ConsumerState { ); } - if (mounted) { + if (context.mounted) { final spendable = ref .read(pWalletBalance(walletId)) .spendable; @@ -2096,8 +2157,9 @@ class _SendViewState extends ConsumerState { onPressed: isFiro && ref .watch( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state != FiroType.public ? null @@ -2117,8 +2179,9 @@ class _SendViewState extends ConsumerState { TransactionFeeSelectionSheet( walletId: walletId, amount: (Decimal.tryParse( - cryptoAmountController - .text) ?? + cryptoAmountController + .text, + ) ?? ref .watch(pSendAmount) ?.decimal ?? @@ -2154,8 +2217,9 @@ class _SendViewState extends ConsumerState { child: (isFiro && ref .watch( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state != FiroType.public) ? Row( @@ -2175,7 +2239,8 @@ class _SendViewState extends ConsumerState { "~${snapshot.data!}", style: STextStyles .itemSubtitle( - context), + context, + ), ); } else { return AnimatedText( @@ -2187,7 +2252,8 @@ class _SendViewState extends ConsumerState { ], style: STextStyles .itemSubtitle( - context), + context, + ), ); } }, @@ -2203,13 +2269,15 @@ class _SendViewState extends ConsumerState { Text( ref .watch( - feeRateTypeStateProvider - .state) + feeRateTypeStateProvider + .state, + ) .state .prettyName, style: STextStyles .itemSubtitle12( - context), + context, + ), ), const SizedBox( width: 10, @@ -2233,7 +2301,8 @@ class _SendViewState extends ConsumerState { : "~${snapshot.data!}", style: STextStyles .itemSubtitle( - context), + context, + ), ); } else { return AnimatedText( @@ -2245,7 +2314,8 @@ class _SendViewState extends ConsumerState { ], style: STextStyles .itemSubtitle( - context), + context, + ), ); } }, @@ -2263,7 +2333,7 @@ class _SendViewState extends ConsumerState { ], ), ), - ) + ), ], ), if (isCustomFee) diff --git a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart index a8dfe643b..8529925d6 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -12,8 +12,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/coin_image_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -23,13 +23,15 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; class BuildingTransactionDialog extends ConsumerStatefulWidget { const BuildingTransactionDialog({ - Key? key, + super.key, required this.onCancel, required this.coin, - }) : super(key: key); + required this.isSpark, + }); final VoidCallback onCancel; final Coin coin; + final bool isSpark; @override ConsumerState createState() => @@ -62,13 +64,24 @@ class _RestoringDialogState extends ConsumerState { "Generating transaction", style: STextStyles.desktopH3(context), ), + if (widget.isSpark) + const SizedBox( + height: 16, + ), + if (widget.isSpark) + Text( + "This may take a few minutes...", + style: STextStyles.desktopSubtitleH2(context), + ), const SizedBox( height: 40, ), assetPath.endsWith(".gif") - ? Image.file(File( - assetPath, - )) + ? Image.file( + File( + assetPath, + ), + ) : const RotatingArrows( width: 40, height: 40, @@ -82,7 +95,7 @@ class _RestoringDialogState extends ConsumerState { onPressed: () { onCancel.call(); }, - ) + ), ], ); } else { @@ -96,14 +109,26 @@ class _RestoringDialogState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - Image.file(File( - assetPath, - )), + Image.file( + File( + assetPath, + ), + ), Text( "Generating transaction", textAlign: TextAlign.center, style: STextStyles.pageTitleH2(context), ), + if (widget.isSpark) + const SizedBox( + height: 12, + ), + if (widget.isSpark) + Text( + "This may take a few minutes...", + textAlign: TextAlign.center, + style: STextStyles.w500_16(context), + ), const SizedBox( height: 32, ), @@ -124,7 +149,7 @@ class _RestoringDialogState extends ConsumerState { onCancel.call(); }, ), - ) + ), ], ), ], @@ -132,6 +157,8 @@ class _RestoringDialogState extends ConsumerState { ) : StackDialog( title: "Generating transaction", + message: + widget.isSpark ? "This may take a few minutes..." : null, icon: const RotatingArrows( width: 24, height: 24, diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 0cba2bf17..028538d63 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -58,14 +58,14 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class TokenSendView extends ConsumerStatefulWidget { const TokenSendView({ - Key? key, + super.key, required this.walletId, required this.coin, required this.tokenContract, this.autoFillData, this.clipboard = const ClipboardWrapper(), this.barcodeScanner = const BarcodeScannerWrapper(), - }) : super(key: key); + }); static const String routeName = "/tokenSendView"; @@ -156,8 +156,10 @@ class _TokenSendViewState extends ConsumerState { // .state = true, // ); - Logging.instance.log("qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); final results = AddressUtils.parseUri(qrResult.rawContent); @@ -216,8 +218,9 @@ class _TokenSendViewState extends ConsumerState { // here we ignore the exception caused by not giving permission // to use the camera to scan a qr code Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } } @@ -239,15 +242,19 @@ class _TokenSendViewState extends ConsumerState { ? Amount.zero : Amount.fromDecimal( (baseAmount.decimal / _price).toDecimal( - scaleOnInfinitePrecision: tokenContract.decimals), - fractionDigits: tokenContract.decimals); + scaleOnInfinitePrecision: tokenContract.decimals, + ), + fractionDigits: tokenContract.decimals, + ); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; } _cachedAmountToSend = _amountToSend; - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info, + ); _cryptoAmountChangeLock = true; cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( @@ -282,8 +289,10 @@ class _TokenSendViewState extends ConsumerState { return; } _cachedAmountToSend = _amountToSend; - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info, + ); final price = ref .read(priceAnd24hChangeNotifierProvider) @@ -457,6 +466,7 @@ class _TokenSendViewState extends ConsumerState { builder: (context) { return BuildingTransactionDialog( coin: wallet.info.coin, + isSpark: false, onCancel: () { wasCancelled = true; @@ -484,7 +494,7 @@ class _TokenSendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), note: noteController.text, @@ -502,20 +512,22 @@ class _TokenSendViewState extends ConsumerState { // pop building dialog Navigator.of(context).pop(); - unawaited(Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: txData, - walletId: walletId, - isTokenTx: true, - onSuccess: clearSendForm, - ), - settings: const RouteSettings( - name: ConfirmTransactionView.routeName, + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => ConfirmTransactionView( + txData: txData, + walletId: walletId, + isTokenTx: true, + onSuccess: clearSendForm, + ), + settings: const RouteSettings( + name: ConfirmTransactionView.routeName, + ), ), ), - )); + ); } } catch (e) { if (mounted) { @@ -538,9 +550,10 @@ class _TokenSendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), onPressed: () { Navigator.of(context).pop(); @@ -626,7 +639,8 @@ class _TokenSendViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final String locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale)); + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); return Background( child: Scaffold( @@ -638,7 +652,7 @@ class _TokenSendViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 50)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -712,11 +726,15 @@ class _TokenSendViewState extends ConsumerState { .watch(pAmountFormatter(coin)) .format( ref - .read(pTokenBalance(( - walletId: widget.walletId, - contractAddress: - tokenContract.address, - ))) + .read( + pTokenBalance( + ( + walletId: widget.walletId, + contractAddress: + tokenContract.address, + ), + ), + ) .spendable, ethContract: tokenContract, withUnitName: false, @@ -734,13 +752,17 @@ class _TokenSendViewState extends ConsumerState { .watch(pAmountFormatter(coin)) .format( ref - .watch(pTokenBalance(( - walletId: - widget.walletId, - contractAddress: - tokenContract - .address, - ))) + .watch( + pTokenBalance( + ( + walletId: + widget.walletId, + contractAddress: + tokenContract + .address, + ), + ), + ) .spendable, ethContract: tokenContract, ), @@ -752,13 +774,17 @@ class _TokenSendViewState extends ConsumerState { textAlign: TextAlign.right, ), Text( - "${(ref.watch(pTokenBalance(( + "${(ref.watch( + pTokenBalance( + ( walletId: widget.walletId, contractAddress: tokenContract .address, - ))).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( + ), + ), + ).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( fractionDigits: 2, ).fiatString( locale: locale, @@ -768,7 +794,7 @@ class _TokenSendViewState extends ConsumerState { fontSize: 8, ), textAlign: TextAlign.right, - ) + ), ], ), ), @@ -807,7 +833,9 @@ class _TokenSendViewState extends ConsumerState { onChanged: (newValue) { _address = newValue.trim(); _updatePreviewButtonState( - _address, _amountToSend); + _address, + _amountToSend, + ); setState(() { _addressToggleFlag = newValue.isNotEmpty; @@ -838,12 +866,15 @@ class _TokenSendViewState extends ConsumerState { _addressToggleFlag ? TextFieldIconButton( key: const Key( - "tokenSendViewClearAddressFieldButtonKey"), + "tokenSendViewClearAddressFieldButtonKey", + ), onTap: () { sendToController.text = ""; _address = ""; _updatePreviewButtonState( - _address, _amountToSend); + _address, + _amountToSend, + ); setState(() { _addressToggleFlag = false; }); @@ -852,7 +883,8 @@ class _TokenSendViewState extends ConsumerState { ) : TextFieldIconButton( key: const Key( - "tokenSendViewPasteAddressFieldButtonKey"), + "tokenSendViewPasteAddressFieldButtonKey", + ), onTap: _onTokenSendViewPasteAddressFieldButtonPressed, child: sendToController @@ -863,7 +895,8 @@ class _TokenSendViewState extends ConsumerState { if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key( - "sendViewAddressBookButtonKey"), + "sendViewAddressBookButtonKey", + ), onTap: () { Navigator.of(context).pushNamed( AddressBookView.routeName, @@ -875,11 +908,12 @@ class _TokenSendViewState extends ConsumerState { if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key( - "sendViewScanQrButtonKey"), + "sendViewScanQrButtonKey", + ), onTap: _onTokenSendViewScanQrButtonPressed, child: const QrCodeIcon(), - ) + ), ], ), ), @@ -997,9 +1031,10 @@ class _TokenSendViewState extends ConsumerState { tokenContract.symbol, style: STextStyles.smallMed14(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1058,13 +1093,16 @@ class _TokenSendViewState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(12), child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), + ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + ), style: STextStyles.smallMed14(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1169,8 +1207,8 @@ class _TokenSendViewState extends ConsumerState { walletId: walletId, isToken: true, amount: (Decimal.tryParse( - cryptoAmountController - .text) ?? + cryptoAmountController.text, + ) ?? Decimal.zero) .toAmount( fractionDigits: @@ -1193,12 +1231,15 @@ class _TokenSendViewState extends ConsumerState { children: [ Text( ref - .watch(feeRateTypeStateProvider - .state) + .watch( + feeRateTypeStateProvider + .state, + ) .state .prettyName, style: STextStyles.itemSubtitle12( - context), + context, + ), ), const SizedBox( width: 10, @@ -1213,7 +1254,8 @@ class _TokenSendViewState extends ConsumerState { "~${snapshot.data!}", style: STextStyles.itemSubtitle( - context), + context, + ), ); } else { return AnimatedText( @@ -1225,7 +1267,8 @@ class _TokenSendViewState extends ConsumerState { ], style: STextStyles.itemSubtitle( - context), + context, + ), ); } }, @@ -1243,7 +1286,7 @@ class _TokenSendViewState extends ConsumerState { ], ), ), - ) + ), ], ), const Spacer(), @@ -1253,13 +1296,15 @@ class _TokenSendViewState extends ConsumerState { TextButton( onPressed: ref .watch( - previewTokenTxButtonStateProvider.state) + previewTokenTxButtonStateProvider.state, + ) .state ? _previewTransaction : null, style: ref .watch( - previewTokenTxButtonStateProvider.state) + previewTokenTxButtonStateProvider.state, + ) .state ? Theme.of(context) .extension()! diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 160de0367..2614a58d4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -272,6 +272,11 @@ class _DesktopSendState extends ConsumerState { padding: const EdgeInsets.all(32), child: BuildingTransactionDialog( coin: wallet.info.coin, + isSpark: wallet is FiroWallet && + ref + .read(publicPrivateBalanceStateProvider.state) + .state == + FiroType.spark, onCancel: () { wasCancelled = true; @@ -306,7 +311,7 @@ class _DesktopSendState extends ConsumerState { address: widget.accountLite!.code, amount: amount, isChange: false, - ) + ), ], satsPerVByte: isCustomFee ? customFeeRate : null, feeRateType: feeRate, @@ -329,7 +334,7 @@ class _DesktopSendState extends ConsumerState { amount: amount, memo: memoController.text, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -348,7 +353,7 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -370,7 +375,7 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], ), ); @@ -386,7 +391,7 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], sparkRecipients: ref.read(pValidSparkSendToAddress) ? [ @@ -395,7 +400,7 @@ class _DesktopSendState extends ConsumerState { amount: amount, memo: memoController.text, isChange: false, - ) + ), ] : null, ), @@ -411,7 +416,7 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], memo: memo, feeRateType: ref.read(feeRateTypeStateProvider), @@ -577,8 +582,10 @@ class _DesktopSendState extends ConsumerState { if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } - Logging.instance.log("it changed $amount $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $amount $_cachedAmountToSend", + level: LogLevel.Info, + ); _cachedAmountToSend = amount; final price = @@ -627,8 +634,10 @@ class _DesktopSendState extends ConsumerState { final qrResult = await scanner.scan(); - Logging.instance.log("qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); final results = AddressUtils.parseUri(qrResult.rawContent); @@ -679,8 +688,9 @@ class _DesktopSendState extends ConsumerState { // here we ignore the exception caused by not giving permission // to use the camera to scan a qr code Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } } @@ -734,7 +744,7 @@ class _DesktopSendState extends ConsumerState { } else { final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); if (data?.text != null && data!.text!.isNotEmpty) { - String content = data.text!.trim(); + final String content = data.text!.trim(); setState(() { memoController.text = content; @@ -865,7 +875,8 @@ class _DesktopSendState extends ConsumerState { if (isPaynymSend) { sendToController.text = widget.accountLite!.nymName; WidgetsBinding.instance.addPostFrameCallback( - (_) => _setValidAddressProviders(sendToController.text)); + (_) => _setValidAddressProviders(sendToController.text), + ); } _cryptoFocus.addListener(() { @@ -912,7 +923,8 @@ class _DesktopSendState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final String locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale)); + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); // add listener for epic cash to strip http:// and https:// prefixes if the address also ocntains an @ symbol (indicating an epicbox address) if (coin == Coin.epicCash) { @@ -975,9 +987,11 @@ class _DesktopSendState extends ConsumerState { width: 10, ), Text( - ref.watch(pAmountFormatter(coin)).format(ref - .watch(pWalletBalanceTertiary(walletId)) - .spendable), + ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pWalletBalanceTertiary(walletId)) + .spendable, + ), style: STextStyles.itemSubtitle(context), ), ], @@ -995,9 +1009,11 @@ class _DesktopSendState extends ConsumerState { width: 10, ), Text( - ref.watch(pAmountFormatter(coin)).format(ref - .watch(pWalletBalanceSecondary(walletId)) - .spendable), + ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pWalletBalanceSecondary(walletId)) + .spendable, + ), style: STextStyles.itemSubtitle(context), ), ], @@ -1016,7 +1032,8 @@ class _DesktopSendState extends ConsumerState { ), Text( ref.watch(pAmountFormatter(coin)).format( - ref.watch(pWalletBalance(walletId)).spendable), + ref.watch(pWalletBalance(walletId)).spendable, + ), style: STextStyles.itemSubtitle(context), ), ], @@ -1164,9 +1181,10 @@ class _DesktopSendState extends ConsumerState { child: Text( ref.watch(pAmountUnit(coin)).unitForCoin(coin), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1223,12 +1241,15 @@ class _DesktopSendState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(12), child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), + ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + ), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1337,7 +1358,8 @@ class _DesktopSendState extends ConsumerState { _addressToggleFlag ? TextFieldIconButton( key: const Key( - "sendViewClearAddressFieldButtonKey"), + "sendViewClearAddressFieldButtonKey", + ), onTap: () { sendToController.text = ""; _address = ""; @@ -1350,7 +1372,8 @@ class _DesktopSendState extends ConsumerState { ) : TextFieldIconButton( key: const Key( - "sendViewPasteAddressFieldButtonKey"), + "sendViewPasteAddressFieldButtonKey", + ), onTap: pasteAddress, child: sendToController.text.isEmpty ? const ClipboardIcon() @@ -1380,7 +1403,8 @@ class _DesktopSendState extends ConsumerState { child: Text( "Address book", style: STextStyles.desktopH3( - context), + context, + ), ), ), const DesktopDialogCloseButton(), @@ -1436,7 +1460,9 @@ class _DesktopSendState extends ConsumerState { FiroType.lelantus) { if (_data != null && _data!.contactLabel == _address) { error = SparkInterface.validateSparkAddress( - address: _data!.address, isTestNet: coin.isTestNet) + address: _data!.address, + isTestNet: coin.isTestNet, + ) ? "Lelantus to Spark not supported" : null; } else if (ref.watch(pValidSparkSendToAddress)) { @@ -1662,8 +1688,9 @@ class _DesktopSendState extends ConsumerState { if (coin == Coin.monero || coin == Coin.wownero) { final fee = await wallet.estimateFeeFor( - amount, - MoneroTransactionPriority.regular.raw!); + amount, + MoneroTransactionPriority.regular.raw!, + ); ref .read(feeSheetSessionCacheProvider) .average[amount] = fee; @@ -1671,16 +1698,18 @@ class _DesktopSendState extends ConsumerState { coin == Coin.firoTestNet) && ref .read( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state != FiroType.public) { final firoWallet = wallet as FiroWallet; if (ref .read( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state == FiroType.lelantus) { ref @@ -1690,8 +1719,9 @@ class _DesktopSendState extends ConsumerState { .estimateFeeForLelantus(amount); } else if (ref .read( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state == FiroType.spark) { ref @@ -1705,7 +1735,9 @@ class _DesktopSendState extends ConsumerState { .read(feeSheetSessionCacheProvider) .average[amount] = await wallet.estimateFeeFor( - amount, feeRate); + amount, + feeRate, + ); } } return ref @@ -1720,8 +1752,8 @@ class _DesktopSendState extends ConsumerState { AnimatedText( stringsToLoopThrough: stringsToLoopThrough, style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( + context, + ).copyWith( color: Theme.of(context) .extension()! .textFieldActiveText, @@ -1735,7 +1767,8 @@ class _DesktopSendState extends ConsumerState { : (coin == Coin.firo || coin == Coin.firoTestNet) && ref .watch( - publicPrivateBalanceStateProvider.state) + publicPrivateBalanceStateProvider.state, + ) .state == FiroType.lelantus ? Text( @@ -1760,8 +1793,8 @@ class _DesktopSendState extends ConsumerState { Text( feeSelectionResult?.$2 ?? "", style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( + context, + ).copyWith( color: Theme.of(context) .extension()! .textFieldActiveText, @@ -1771,8 +1804,8 @@ class _DesktopSendState extends ConsumerState { Text( feeSelectionResult?.$3 ?? "", style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( + context, + ).copyWith( color: Theme.of(context) .extension()! .textFieldActiveSearchIconRight, @@ -1803,7 +1836,7 @@ class _DesktopSendState extends ConsumerState { enabled: ref.watch(pPreviewTxButtonEnabled(coin)), onPressed: ref.watch(pPreviewTxButtonEnabled(coin)) ? previewSend : null, - ) + ), ], ); } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 0887036a6..eef67e694 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -56,13 +56,13 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class DesktopTokenSend extends ConsumerStatefulWidget { const DesktopTokenSend({ - Key? key, + super.key, required this.walletId, this.autoFillData, this.clipboard = const ClipboardWrapper(), this.barcodeScanner = const BarcodeScannerWrapper(), this.accountLite, - }) : super(key: key); + }); final String walletId; final SendViewAutoFillData? autoFillData; @@ -108,10 +108,14 @@ class _DesktopTokenSendState extends ConsumerState { final Amount amount = _amountToSend!; final Amount availableBalance = ref - .read(pTokenBalance(( - walletId: walletId, - contractAddress: tokenWallet.tokenContract.address - ))) + .read( + pTokenBalance( + ( + walletId: walletId, + contractAddress: tokenWallet.tokenContract.address + ), + ), + ) .spendable; // confirm send all @@ -221,6 +225,7 @@ class _DesktopTokenSendState extends ConsumerState { padding: const EdgeInsets.all(32), child: BuildingTransactionDialog( coin: tokenWallet.cryptoCurrency.coin, + isSpark: false, onCancel: () { wasCancelled = true; @@ -250,7 +255,7 @@ class _DesktopTokenSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), nonce: int.tryParse(nonceController.text), @@ -405,8 +410,10 @@ class _DesktopTokenSendState extends ConsumerState { _cachedAmountToSend == _amountToSend) { return; } - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info, + ); _cachedAmountToSend = _amountToSend; final price = ref @@ -468,8 +475,10 @@ class _DesktopTokenSendState extends ConsumerState { final qrResult = await scanner.scan(); - Logging.instance.log("qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); final results = AddressUtils.parseUri(qrResult.rawContent); @@ -524,8 +533,9 @@ class _DesktopTokenSendState extends ConsumerState { // here we ignore the exception caused by not giving permission // to use the camera to scan a qr code Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } } @@ -579,8 +589,10 @@ class _DesktopTokenSendState extends ConsumerState { return; } _cachedAmountToSend = _amountToSend; - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info, + ); final amountString = ref.read(pAmountFormatter(coin)).format( _amountToSend!, @@ -603,10 +615,15 @@ class _DesktopTokenSendState extends ConsumerState { Future sendAllTapped() async { cryptoAmountController.text = ref - .read(pTokenBalance(( - walletId: walletId, - contractAddress: ref.read(pCurrentTokenWallet)!.tokenContract.address - ))) + .read( + pTokenBalance( + ( + walletId: walletId, + contractAddress: + ref.read(pCurrentTokenWallet)!.tokenContract.address + ), + ), + ) .spendable .decimal .toStringAsFixed( @@ -788,9 +805,10 @@ class _DesktopTokenSendState extends ConsumerState { child: Text( tokenContract.symbol, style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -850,12 +868,15 @@ class _DesktopTokenSendState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(12), child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), + ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + ), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -936,12 +957,15 @@ class _DesktopTokenSendState extends ConsumerState { _addressToggleFlag ? TextFieldIconButton( key: const Key( - "sendTokenViewClearAddressFieldButtonKey"), + "sendTokenViewClearAddressFieldButtonKey", + ), onTap: () { sendToController.text = ""; _address = ""; _updatePreviewButtonState( - _address, _amountToSend); + _address, + _amountToSend, + ); setState(() { _addressToggleFlag = false; }); @@ -950,7 +974,8 @@ class _DesktopTokenSendState extends ConsumerState { ) : TextFieldIconButton( key: const Key( - "sendTokenViewPasteAddressFieldButtonKey"), + "sendTokenViewPasteAddressFieldButtonKey", + ), onTap: pasteAddress, child: sendToController.text.isEmpty ? const ClipboardIcon() @@ -1129,7 +1154,7 @@ class _DesktopTokenSendState extends ConsumerState { onPressed: ref.watch(previewTokenTxButtonStateProvider.state).state ? previewSend : null, - ) + ), ], ); } From 87c9f0414d983ffce24fd88648f2b96f551a9551 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Thu, 9 May 2024 17:09:03 -0600 Subject: [PATCH 250/272] Update version (v2.0.0, build 220) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 54c52e8f9..607d7b920 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.0.0+219 +version: 2.0.0+220 environment: sdk: ">=3.3.4 <4.0.0" From ecadefef638d1e79729fe0c08039cbc171431d67 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 09:52:21 -0600 Subject: [PATCH 251/272] update frost info dialogs --- .../new/create_new_frost_ms_wallet_view.dart | 23 ++++++++- .../select_new_frost_import_type_view.dart | 49 ++----------------- 2 files changed, 25 insertions(+), 47 deletions(-) diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index 620d9df06..c8bf16e26 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -114,7 +114,6 @@ class _NewFrostMsWalletViewState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // TODO: [prio=high] need text from designers! Text( "What is a threshold?", style: STextStyles.w600_20(context), @@ -123,7 +122,27 @@ class _NewFrostMsWalletViewState height: 12, ), Text( - "Text here", + "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), ), ], diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index 5596534a3..b438baac5 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -292,7 +292,6 @@ class _FrostJoinInfoDialog extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // TODO: [prio=high] need text from designers! Text( "Join a group", style: STextStyles.w600_20(context), @@ -301,58 +300,18 @@ class _FrostJoinInfoDialog extends StatelessWidget { height: 12, ), Text( - "Text here", - style: STextStyles.w400_16(context), - ), - const SizedBox( - height: 8, - ), - Text( - "What is resharing?", + "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: 8, - ), - Text( - "In cryptocurrency, you are your own bank." - " Imagine keeping cash at home. If that cash" - " burns down or gets stolen, you lose it and" - " nobody will help you get your money back.", - style: STextStyles.w400_16(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Since cryptocurrency is digital money, your " - "wallet key is like that “cash” you keep at " - "home. If you lose your phone or if you " - "forget your wallet PIN, but you have your " - "wallet key, your crypto money will be safe. " - "That is why you should keep your wallet key " - "safe.", - style: STextStyles.w400_16(context), - ), const SizedBox( height: 12, ), Text( - "Why write it down?", + "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), ), - const SizedBox( - height: 8, - ), - Text( - "You do not put your cash on display, do you?" - " Keeping your wallet key on a digital device" - " is like having it on display for thieves - " - "malicious software and hackers. Write your " - "wallet key down on paper in multiple copies " - "and keep them in a real, physical safe.", - style: STextStyles.w400_16(context), - ), ], ), ); From cb70b5c92f2c0c82476a2335b6a1e5e06ac92055 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 10:09:30 -0600 Subject: [PATCH 252/272] show current spark address as my stack contact address for firo wallets --- .../address_book_views/address_book_view.dart | 80 ++++++++++++------ .../desktop_address_book.dart | 82 +++++++++++++------ 2 files changed, 115 insertions(+), 47 deletions(-) diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index f302c287c..40e71434e 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -11,9 +11,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; @@ -23,6 +25,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/address_book_card.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -34,10 +37,10 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddressBookView extends ConsumerStatefulWidget { const AddressBookView({ - Key? key, + super.key, this.coin, this.filterTerm, - }) : super(key: key); + }); static const String routeName = "/addressBook"; @@ -61,10 +64,11 @@ class _AddressBookViewState extends ConsumerState { ref.refresh(addressBookFilterProvider); if (widget.coin == null) { - List coins = Coin.values.toList(); + final List coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); - bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; + final bool showTestNet = + ref.read(prefsChangeNotifierProvider).showTestNetCoins; if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); @@ -78,13 +82,26 @@ class _AddressBookViewState extends ConsumerState { } WidgetsBinding.instance.addPostFrameCallback((_) async { - List addresses = []; + final List addresses = []; final wallets = ref.read(pWallets).wallets; for (final wallet in wallets) { + final String addressString; + if (wallet is SparkInterface) { + Address? address = await wallet.getCurrentReceivingSparkAddress(); + if (address == null) { + address = await wallet.generateNextSparkAddress(); + await ref.read(mainDBProvider).updateOrPutAddresses([address]); + } + addressString = address.value; + } else { + final address = await wallet.getCurrentReceivingAddress(); + addressString = address?.value ?? wallet.info.cachedReceivingAddress; + } + addresses.add( ContactAddressEntry() ..coinName = wallet.info.coin.name - ..address = (await wallet.getCurrentReceivingAddress())!.value + ..address = addressString ..label = "Current Receiving" ..other = wallet.info.name, ); @@ -302,15 +319,24 @@ class _AddressBookViewState extends ConsumerState { child: Column( children: [ ...contacts - .where((element) => element.addressesSorted - .where((e) => ref.watch(addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(widget.filterTerm ?? _searchTerm, e)) + .where( + (element) => element.addressesSorted + .where( + (e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), + ), + ) + .isNotEmpty, + ) + .where( + (e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e), + ) .where((element) => element.isFavorite) .map( (e) => AddressBookCard( @@ -350,14 +376,22 @@ class _AddressBookViewState extends ConsumerState { child: Column( children: [ ...contacts - .where((element) => element.addressesSorted - .where((e) => ref.watch( - addressBookFilterProvider.select((value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(widget.filterTerm ?? _searchTerm, e)) + .where( + (element) => element.addressesSorted + .where( + (e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), + ), + ) + .isNotEmpty, + ) + .where( + (e) => ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e), + ) .map( (e) => AddressBookCard( key: diff --git a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart index d5390e3fd..aba315c64 100644 --- a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart @@ -11,11 +11,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; @@ -25,6 +27,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/address_book_card.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -38,7 +41,7 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; class DesktopAddressBook extends ConsumerStatefulWidget { - const DesktopAddressBook({Key? key}) : super(key: key); + const DesktopAddressBook({super.key}); static const String routeName = "/desktopAddressBook"; @@ -93,10 +96,11 @@ class _DesktopAddressBook extends ConsumerState { ref.refresh(addressBookFilterProvider); // if (widget.coin == null) { - List coins = Coin.values.toList(); + final List coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); - bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; + final bool showTestNet = + ref.read(prefsChangeNotifierProvider).showTestNetCoins; if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); @@ -110,13 +114,26 @@ class _DesktopAddressBook extends ConsumerState { // } WidgetsBinding.instance.addPostFrameCallback((_) async { - List addresses = []; + final List addresses = []; final wallets = ref.read(pWallets).wallets; for (final wallet in wallets) { + final String addressString; + if (wallet is SparkInterface) { + Address? address = await wallet.getCurrentReceivingSparkAddress(); + if (address == null) { + address = await wallet.generateNextSparkAddress(); + await ref.read(mainDBProvider).updateOrPutAddresses([address]); + } + addressString = address.value; + } else { + final address = await wallet.getCurrentReceivingAddress(); + addressString = address?.value ?? wallet.info.cachedReceivingAddress; + } + addresses.add( ContactAddressEntry() ..coinName = wallet.info.coin.name - ..address = wallet.info.cachedReceivingAddress + ..address = addressString ..label = "Current Receiving" ..other = wallet.info.name, ); @@ -148,26 +165,41 @@ class _DesktopAddressBook extends ConsumerState { ref.watch(addressBookServiceProvider.select((value) => value.contacts)); final allContacts = contacts - .where((element) => - element.addresses.isEmpty || - element.addresses - .where((e) => ref.watch(addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)))) - .isNotEmpty) .where( - (e) => ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + (element) => + element.addresses.isEmpty || + element.addresses + .where( + (e) => ref.watch( + addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)), + ), + ) + .isNotEmpty, + ) + .where( + (e) => ref.read(addressBookServiceProvider).matches(_searchTerm, e), + ) .toList(); final favorites = contacts - .where((element) => - element.addresses.isEmpty || - element.addresses - .where((e) => ref.watch(addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + .where( + (element) => + element.addresses.isEmpty || + element.addresses + .where( + (e) => ref.watch( + addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)), + ), + ) + .isNotEmpty, + ) + .where( + (e) => + e.isFavorite && + ref.read(addressBookServiceProvider).matches(_searchTerm, e), + ) .where((element) => element.isFavorite) .toList(); @@ -182,7 +214,7 @@ class _DesktopAddressBook extends ConsumerState { Text( "Address Book", style: STextStyles.desktopH3(context), - ) + ), ], ), ), @@ -354,7 +386,8 @@ class _DesktopAddressBook extends ConsumerState { ), child: AddressBookCard( key: Key( - "favContactCard_${favorites[i].customId}_key"), + "favContactCard_${favorites[i].customId}_key", + ), contactId: favorites[i].customId, desktopSendFrom: false, ), @@ -426,7 +459,8 @@ class _DesktopAddressBook extends ConsumerState { ), child: AddressBookCard( key: Key( - "favContactCard_${allContacts[i].customId}_key"), + "favContactCard_${allContacts[i].customId}_key", + ), contactId: allContacts[i].customId, desktopSendFrom: false, ), From d92b712146c3604e6c4bfb8c722c8b1e88d406d0 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 14:32:15 -0600 Subject: [PATCH 253/272] speed up spark sends --- .../cached_electrumx_client.dart | 4 ++ lib/wallets/wallet/impl/firo_wallet.dart | 1 + .../spark_interface.dart | 61 +++---------------- 3 files changed, 13 insertions(+), 53 deletions(-) diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index 3a93bb5d8..559526f17 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -109,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); @@ -126,6 +127,9 @@ class CachedElectrumXClient { }; } else { set = Map.from(cachedSet); + if (useOnlyCacheIfNotEmpty) { + return set; + } } final newSet = await electrumXClient.getSparkAnonymitySet( diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index a0d735ac5..ea109fe61 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -621,6 +621,7 @@ class FiroWallet extends Bip39HDWallet final sparkAnonSetFuture = electrumXCachedClient.getSparkAnonymitySet( groupId: latestSparkCoinId.toString(), coin: info.coin, + useOnlyCacheIfNotEmpty: false, ); final sparkUsedCoinTagsFuture = electrumXCachedClient.getSparkUsedCoinsTags( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index f25f22323..7aaa1d3ed 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -259,6 +259,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final set = await electrumXCachedClient.getSparkAnonymitySet( groupId: i.toString(), coin: info.coin, + useOnlyCacheIfNotEmpty: true, ); set["coinGroupID"] = i; setMaps.add(set); @@ -616,22 +617,14 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { try { final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); - final blockHash = await _getCachedSparkBlockHash(); - final startNumber = await _getSparkCoinsStartNumber(); + final anonymitySetFuture = electrumXCachedClient.getSparkAnonymitySet( + groupId: latestSparkCoinId.toString(), + coin: info.coin, + useOnlyCacheIfNotEmpty: false, + ); - final anonymitySetFuture = blockHash == null - ? electrumXCachedClient.getSparkAnonymitySet( - groupId: latestSparkCoinId.toString(), - coin: info.coin, - ) - : electrumXClient.getSparkAnonymitySet( - coinGroupId: latestSparkCoinId.toString(), - startBlockHash: blockHash, - ); - - final spentCoinTagsFuture = startNumber == null - ? electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin) - : electrumXClient.getSparkUsedCoinsTags(startNumber: startNumber); + final spentCoinTagsFuture = + electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin); final futureResults = await Future.wait([ anonymitySetFuture, @@ -665,15 +658,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); myCoins.addAll(identifiedCoins); - - // update blockHash in cache - final String newBlockHash = - base64ToReverseHex(anonymitySet["blockHash"] as String); - - await Future.wait([ - _setCachedSparkBlockHash(newBlockHash), - _setSparkCoinsStartNumber(spentCoinTags.length - 1), - ]); } // check current coins @@ -788,10 +772,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // update wallet spark coins in isar await _addOrUpdateSparkCoins(myCoins); - // update blockHash in cache - final String newBlockHash = anonymitySet["blockHash"] as String; - await _setCachedSparkBlockHash(newBlockHash); - // refresh spark balance await refreshSparkBalance(); } catch (e, s) { @@ -1602,31 +1582,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // ====================== Private ============================================ - final _kSparkAnonSetCachedBlockHashKey = "SparkAnonSetCachedBlockHashKey"; - final _kSparkCoinsStartNumberKey = "SparkCoinsStartNumberKey"; - - Future _getCachedSparkBlockHash() async { - return info.otherData[_kSparkAnonSetCachedBlockHashKey] as String?; - } - - Future _setCachedSparkBlockHash(String blockHash) async { - await info.updateOtherData( - newEntries: {_kSparkAnonSetCachedBlockHashKey: blockHash}, - isar: mainDB.isar, - ); - } - - Future _getSparkCoinsStartNumber() async { - return info.otherData[_kSparkCoinsStartNumberKey] as int?; - } - - Future _setSparkCoinsStartNumber(int startNumber) async { - await info.updateOtherData( - newEntries: {_kSparkCoinsStartNumberKey: startNumber}, - isar: mainDB.isar, - ); - } - Future _addOrUpdateSparkCoins(List coins) async { if (coins.isNotEmpty) { await mainDB.isar.writeTxn(() async { From 667560372da77192a59244568bbb29b712add27c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 9 May 2024 17:44:49 -0500 Subject: [PATCH 254/272] peercoin WIP --- .../add_edit_node_view.dart | 7 +- .../manage_nodes_views/node_details_view.dart | 2 + lib/supported_coins.dart | 5 + lib/themes/color_theme.dart | 5 + lib/themes/stack_colors.dart | 3 + lib/utilities/address_utils.dart | 6 + lib/utilities/amount/amount_unit.dart | 2 + lib/utilities/block_explorers.dart | 5 + lib/utilities/constants.dart | 20 ++ lib/utilities/default_nodes.dart | 34 +- lib/utilities/enums/coin_enum.dart | 35 +++ .../enums/derive_path_type_enum.dart | 2 + .../crypto_currency/coins/peercoin.dart | 186 +++++++++++ lib/wallets/wallet/impl/peercoin_wallet.dart | 292 ++++++++++++++++++ lib/wallets/wallet/wallet.dart | 6 + lib/widgets/node_card.dart | 2 + lib/widgets/node_options_sheet.dart | 2 + 17 files changed, 612 insertions(+), 2 deletions(-) create mode 100644 lib/wallets/crypto_currency/coins/peercoin.dart create mode 100644 lib/wallets/wallet/impl/peercoin_wallet.dart diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index ef7ae7f31..7a656c288 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -169,6 +169,8 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: case Coin.bitcoinTestNet: @@ -221,7 +223,8 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.solana: try { RpcClient rpcClient; - if (formData.host!.startsWith("http") || formData.host!.startsWith("https")) { + if (formData.host!.startsWith("http") || + formData.host!.startsWith("https")) { rpcClient = RpcClient("${formData.host}:${formData.port}"); } else { rpcClient = RpcClient("http://${formData.host}:${formData.port}"); @@ -761,6 +764,8 @@ class _NodeFormState extends ConsumerState { case Coin.namecoin: case Coin.bitcoincash: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.tezos: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index c5516db55..1034d986c 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -142,6 +142,8 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.dogecoin: case Coin.firo: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: diff --git a/lib/supported_coins.dart b/lib/supported_coins.dart index 48960c03e..70ee44ad8 100644 --- a/lib/supported_coins.dart +++ b/lib/supported_coins.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/peercoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; @@ -52,6 +53,8 @@ class SupportedCoins { return Monero(CryptoCurrencyNetwork.main); case Coin.particl: return Particl(CryptoCurrencyNetwork.main); + case Coin.peercoin: + return Peercoin(CryptoCurrencyNetwork.main); case Coin.solana: return Solana(CryptoCurrencyNetwork.main); case Coin.stellar: @@ -80,6 +83,8 @@ class SupportedCoins { return Dogecoin(CryptoCurrencyNetwork.test); case Coin.stellarTestnet: return Stellar(CryptoCurrencyNetwork.test); + case Coin.peercoinTestNet: + return Peercoin(CryptoCurrencyNetwork.test); } } } diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index c67f08ce8..e4dbcabc3 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -28,6 +28,7 @@ class CoinThemeColorDefault { Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); Color get particl => const Color(0xFF8175BD); + Color get peercoin => const Color(0xFF3CB054); Color get solana => const Color(0xFFC696FF); Color get stellar => const Color(0xFF6600FF); Color get nano => const Color(0xFF209CE9); @@ -67,6 +68,10 @@ class CoinThemeColorDefault { return wownero; case Coin.particl: return particl; + case Coin.peercoin: + return peercoin; + case Coin.peercoinTestNet: + return peercoin; case Coin.solana: return solana; case Coin.stellar: diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index 1f994934e..11e146d1b 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1709,6 +1709,9 @@ class StackColors extends ThemeExtension { return _coin.wownero; case Coin.particl: return _coin.particl; + case Coin.peercoin: + case Coin.peercoinTestNet: + return _coin.peercoin; case Coin.solana: return _coin.solana; case Coin.stellar: diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 722d9f9af..65e0231fe 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -32,6 +32,8 @@ import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import '../wallets/crypto_currency/coins/peercoin.dart'; + class AddressUtils { static String condenseAddress(String address) { return '${address.substring(0, 5)}...${address.substring(address.length - 5)}'; @@ -68,6 +70,8 @@ class AddressUtils { return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.particl: return Particl(CryptoCurrencyNetwork.main).validateAddress(address); + case Coin.peercoin: + return Peercoin(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.solana: return Solana(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.stellar: @@ -91,6 +95,8 @@ class AddressUtils { return Firo(CryptoCurrencyNetwork.test).validateAddress(address); case Coin.dogecoinTestNet: return Dogecoin(CryptoCurrencyNetwork.test).validateAddress(address); + case Coin.peercoinTestNet: + return Peercoin(CryptoCurrencyNetwork.test).validateAddress(address); case Coin.stellarTestnet: return Stellar(CryptoCurrencyNetwork.test).validateAddress(address); } diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index cfa9fec30..e74de6510 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -39,6 +39,7 @@ enum AmountUnit { case Coin.firo: case Coin.litecoin: case Coin.particl: + case Coin.peercoin: case Coin.namecoin: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: @@ -47,6 +48,7 @@ enum AmountUnit { case Coin.bitcoincashTestnet: case Coin.dogecoinTestNet: case Coin.firoTestNet: + case Coin.peercoinTestNet: case Coin.bitcoin: case Coin.bitcoincash: case Coin.dogecoin: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index fcb2f5717..cf67697b6 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -68,6 +68,11 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://tzstats.com/$txid"); case Coin.solana: return Uri.parse("https://explorer.solana.com/tx/$txid"); + case Coin.peercoin: + return Uri.parse("https://chainz.cryptoid.info/ppc/tx.dws?$txid.htm"); + case Coin.peercoinTestNet: + return Uri.parse( + "https://chainz.cryptoid.info/ppc-test/search.dws?q=$txid.htm"); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index f60a0fe1e..d3fedc666 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -47,6 +47,7 @@ abstract class Constants { static final BigInt _satsPerCoin = BigInt.from(100000000); static final BigInt _satsPerCoinTezos = BigInt.from(1000000); static final BigInt _satsPerCoinSolana = BigInt.from(1000000000); + static final BigInt _satsPerCoinPeercoin = BigInt.from(1000000); // 1*10^6. static const int _decimalPlaces = 8; static const int _decimalPlacesNano = 30; static const int _decimalPlacesBanano = 29; @@ -57,6 +58,7 @@ abstract class Constants { static const int _decimalPlacesStellar = 7; static const int _decimalPlacesTezos = 6; static const int _decimalPlacesSolana = 9; + static const int _decimalPlacesPeercoin = 6; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -114,6 +116,10 @@ abstract class Constants { case Coin.solana: return _satsPerCoinSolana; + + case Coin.peercoin: + case Coin.peercoinTestNet: + return _satsPerCoinPeercoin; } } @@ -163,6 +169,10 @@ abstract class Constants { case Coin.solana: return _decimalPlacesSolana; + + case Coin.peercoin: + case Coin.peercoinTestNet: + return _decimalPlacesPeercoin; } } @@ -184,6 +194,8 @@ abstract class Constants { case Coin.ethereum: case Coin.namecoin: case Coin.particl: + values.addAll([12, 24]); + break; case Coin.solana: case Coin.nano: case Coin.stellar: @@ -206,6 +218,10 @@ abstract class Constants { case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: throw ArgumentError("Frost mnemonic lengths unsupported"); + case Coin.peercoin: + case Coin.peercoinTestNet: + values.addAll([12, /*15, 18, 21,*/ 24]); // TODO [prio=low]: Test rest. + break; } return values; } @@ -220,6 +236,8 @@ abstract class Constants { case Coin.bitcoincash: case Coin.bitcoincashTestnet: case Coin.eCash: + case Coin.peercoin: + case Coin.peercoinTestNet: return 600; case Coin.dogecoin: @@ -291,6 +309,8 @@ abstract class Constants { case Coin.nano: case Coin.banano: case Coin.epicCash: + case Coin.peercoin: // TODO [prio=low]: Verify default seed length. + case Coin.peercoinTestNet: case Coin.stellar: case Coin.stellarTestnet: case Coin.tezos: diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 25d730c43..b6fffa789 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -30,6 +30,7 @@ abstract class DefaultNodes { namecoin, wownero, particl, + peercoin, stellar, nano, banano, @@ -188,8 +189,21 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get peercoin => NodeModel( + host: "electrum.peercoinexplorer.net", + port: 50004, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.peercoin), + useSSL: true, + enabled: true, + coinName: Coin.peercoin.name, + isFailover: true, + isDown: false, + ); + static NodeModel get solana => NodeModel( - host: "https://api.mainnet-beta.solana.com", // TODO: Change this to stack wallet one + host: + "https://api.mainnet-beta.solana.com", // TODO: Change this to stack wallet one port: 443, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(Coin.solana), @@ -309,6 +323,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get peercoinTestNet => NodeModel( + host: "testnet-electrum.peercoinexplorer.net", + port: 50009, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.peercoinTestNet), + useSSL: true, + enabled: true, + coinName: Coin.peercoinTestNet.name, + isFailover: true, + isDown: false, + ); + static NodeModel get stellarTestnet => NodeModel( host: "https://horizon-testnet.stellar.org/", port: 50022, @@ -360,6 +386,12 @@ abstract class DefaultNodes { case Coin.particl: return particl; + case Coin.peercoin: + return peercoin; + + case Coin.peercoinTestNet: + return peercoinTestNet; + case Coin.solana: return solana; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 6551dbc62..591deb61a 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -26,6 +26,7 @@ enum Coin { namecoin, nano, particl, + peercoin, solana, stellar, tezos, @@ -42,6 +43,7 @@ enum Coin { dogecoinTestNet, firoTestNet, litecoinTestNet, + peercoinTestNet, stellarTestnet, } @@ -70,6 +72,8 @@ extension CoinExt on Coin { return "Monero"; case Coin.particl: return "Particl"; + case Coin.peercoin: + return "Peercoin"; case Coin.solana: return "Solana"; case Coin.stellar: @@ -96,6 +100,8 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; + case Coin.peercoinTestNet: + return "tPeercoin"; case Coin.stellarTestnet: return "tStellar"; } @@ -124,6 +130,8 @@ extension CoinExt on Coin { return "XMR"; case Coin.particl: return "PART"; + case Coin.peercoin: + return "PPC"; case Coin.solana: return "SOL"; case Coin.stellar: @@ -149,6 +157,8 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; + case Coin.peercoinTestNet: + return "tPPC"; case Coin.stellarTestnet: return "tXLM"; } @@ -178,6 +188,8 @@ extension CoinExt on Coin { return "monero"; case Coin.particl: return "particl"; + case Coin.peercoin: + return "peercoin"; case Coin.solana: return "solana"; case Coin.stellar: @@ -203,6 +215,8 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; + case Coin.peercoinTestNet: + return "peercoin"; case Coin.stellarTestnet: return "stellar"; } @@ -222,6 +236,8 @@ extension CoinExt on Coin { case Coin.firoTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.ethereum: case Coin.eCash: case Coin.stellar: @@ -255,6 +271,8 @@ extension CoinExt on Coin { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.eCash: case Coin.epicCash: case Coin.monero: @@ -284,6 +302,7 @@ extension CoinExt on Coin { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: case Coin.epicCash: case Coin.ethereum: case Coin.monero: @@ -302,6 +321,7 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: + case Coin.peercoinTestNet: case Coin.stellarTestnet: return true; } @@ -328,6 +348,7 @@ extension CoinExt on Coin { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: case Coin.epicCash: case Coin.ethereum: case Coin.monero: @@ -358,6 +379,9 @@ extension CoinExt on Coin { case Coin.firoTestNet: return Coin.firo; + case Coin.peercoinTestNet: + return Coin.peercoin; + case Coin.stellarTestnet: return Coin.stellar; } @@ -387,6 +411,8 @@ extension CoinExt on Coin { case Coin.firo: case Coin.firoTestNet: case Coin.dogecoinTestNet: + case Coin.peercoin: + case Coin.peercoinTestNet: return AddressType.p2pkh; case Coin.monero: @@ -462,6 +488,15 @@ Coin coinFromPrettyName(String name) { case "particl": return Coin.particl; + case "Peercoin": + case "peercoin": + return Coin.peercoin; + + case "tPeercoin": + case "Peercoin Testnet": + case "peercoinTestNet": + return Coin.peercoinTestNet; + case "Solana": case "solana": return Coin.solana; diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 50ec1ab59..4dcaef022 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -63,6 +63,8 @@ extension DerivePathTypeExt on DerivePathType { case Coin.litecoinTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: return DerivePathType.bip84; case Coin.eCash: diff --git a/lib/wallets/crypto_currency/coins/peercoin.dart b/lib/wallets/crypto_currency/coins/peercoin.dart new file mode 100644 index 000000000..f149959b6 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/peercoin.dart @@ -0,0 +1,186 @@ +import 'package:coinlib/src/network.dart'; +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; + +class Peercoin extends Bip39HDCurrency { + Peercoin(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.peercoin; + case CryptoCurrencyNetwork.test: + coin = Coin.peercoinTestNet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + bool get torSupport => true; + + @override + String constructDerivePath( + {required DerivePathType derivePathType, + int account = 0, + required int chain, + required int index}) { + String coinType; + switch (networkParams.wifPrefix) { + case 183: // PPC mainnet wif. + coinType = "10"; // PPC mainnet. + break; + // TODO: [prio=low] Add testnet. + default: + throw Exception("Invalid Peercoin network wif used!"); + } + + int purpose; + switch (derivePathType) { + case DerivePathType.bip44: + purpose = 44; + break; + case DerivePathType.bip84: + purpose = 84; + break; + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + + return "m/$purpose'/$coinType'/$account'/$chain/$index"; + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "electrum.peercoinexplorer.net", + port: 50004, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.peercoin), + useSSL: true, + enabled: true, + coinName: Coin.peercoin.name, + isFailover: true, + isDown: false, + ); + case CryptoCurrencyNetwork.test: + return NodeModel( + host: "testnet-electrum.peercoinexplorer.net", + port: 50009, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.peercoinTestNet), + useSSL: false, // TODO [prio=med]: Is this safe? + enabled: true, + coinName: Coin.peercoinTestNet.name, + isFailover: true, + isDown: false, + ); + default: + throw UnimplementedError(); + } + } + + @override + Amount get dustLimit => Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.peercoin.decimals, + ); + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "0000000032fe677166d54963b62a4677d8957e87c508eaa4fd7eb1c880cd27e3"; + case CryptoCurrencyNetwork.test: + return "00000001f757bb737f6596503e17cd17b0658ce630cc727c0cca81aec47c9f06"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey( + {required coinlib.ECPublicKey publicKey, + required DerivePathType derivePathType}) { + switch (derivePathType) { + // case DerivePathType.bip16: + + case DerivePathType.bip44: + final addr = coinlib.P2PKHAddress.fromPublicKey( + publicKey, + version: networkParams.p2pkhPrefix, + ); + + return (address: addr, addressType: AddressType.p2pkh); + + case DerivePathType.bip49: + final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ).program.script; + + final addr = coinlib.P2SHAddress.fromRedeemScript( + p2wpkhScript, + version: networkParams.p2shPrefix, + ); + + return (address: addr, addressType: AddressType.p2sh); + + case DerivePathType.bip84: + final addr = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ); + + return (address: addr, addressType: AddressType.p2wpkh); + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + } + + @override + coinlib.Network get networkParams { + switch (network) { + case CryptoCurrencyNetwork.main: + return Network.mainnet; + case CryptoCurrencyNetwork.test: + return Network.testnet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + List get supportedDerivationPathTypes => [ + DerivePathType.bip44, + ]; + + @override + bool validateAddress(String address) { + try { + coinlib.Address.fromString(address, networkParams); + return true; + } catch (_) { + return false; + } + } + + @override + bool operator ==(Object other) { + return other is Peercoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Peercoin, network); +} diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart new file mode 100644 index 000000000..9e12066e8 --- /dev/null +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -0,0 +1,292 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/peercoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; + +class PeercoinWallet extends Bip39HDWallet + with ElectrumXInterface, CoinControlInterface { + @override + int get isarTransactionVersion => 2; + + PeercoinWallet(CryptoCurrencyNetwork network) : super(Peercoin(network)); + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + // =========================================================================== + + @override + Future> fetchAddressesForElectrumXScan() async { + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + + // =========================================================================== + + @override + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } + + // =========================================================================== + + @override + Future< + ({ + bool blocked, + String? blockedReason, + String? utxoLabel, + })> checkBlockUTXO( + Map jsonUTXO, + String? scriptPubKeyHex, + Map jsonTX, + String? utxoOwnerAddress, + ) async { + // TODO [prio=high]: Check if Peercoin has outputs (eg stakes etc) to block. + return (blocked: false, blockedReason: null, utxoLabel: null); + } + + @override + Future updateTransactions() async { + // Get all addresses. + List

allAddressesOld = await fetchAddressesForElectrumXScan(); + + // Separate receiving and change addresses. + Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); + + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + // Fetch history from ElectrumX. + final List> allTxHashes = + await fetchHistory(allAddressesSet); + + // Only parse new txs (not in db yet). + List> allTransactions = []; + for (final txHash in allTxHashes) { + // Check for duplicates by searching for tx by tx_hash in db. + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); + + if (storedTx == null || + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { + // Tx not in db yet. + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: cryptoCurrency.coin, + ); + + // Only tx to list once. + if (allTransactions + .indexWhere((e) => e["txid"] == tx["txid"] as String) == + -1) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + coin: cryptoCurrency.coin, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) + as Map); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + scriptSigAsm: map["scriptSig"]?["asm"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + TransactionSubType subType = TransactionSubType.none; + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + + // Namecoin doesn't have special outputs like tokens, ordinals, etc. + // But this is where you'd check for special outputs. + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.log( + "Unexpected tx found (ignoring it): $txData", + level: LogLevel.Error, + ); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 850e2449e..549bfefb2 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -39,6 +39,7 @@ import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/namecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/particl_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/peercoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/solana_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/stellar_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; @@ -363,6 +364,11 @@ abstract class Wallet { case Coin.particl: return ParticlWallet(CryptoCurrencyNetwork.main); + case Coin.peercoin: + return PeercoinWallet(CryptoCurrencyNetwork.main); + case Coin.peercoinTestNet: + return PeercoinWallet(CryptoCurrencyNetwork.test); + case Coin.solana: return SolanaWallet(CryptoCurrencyNetwork.main); diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 006364342..a6ac18302 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -173,6 +173,8 @@ class _NodeCardState extends ConsumerState { case Coin.eCash: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: + case Coin.peercoin: + case Coin.peercoinTestNet: try { testPassed = await checkElectrumServer( host: node.host, diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index a91f63fba..31bdec456 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -154,6 +154,8 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.eCash: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: + case Coin.peercoin: + case Coin.peercoinTestNet: try { testPassed = await checkElectrumServer( host: node.host, From f6decc2fb4378a19c5102be2f0868edfb19d1788 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 10:39:34 -0600 Subject: [PATCH 255/272] add supported deviation path --- lib/wallets/crypto_currency/coins/peercoin.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/peercoin.dart b/lib/wallets/crypto_currency/coins/peercoin.dart index f149959b6..97ac2fa7f 100644 --- a/lib/wallets/crypto_currency/coins/peercoin.dart +++ b/lib/wallets/crypto_currency/coins/peercoin.dart @@ -28,11 +28,12 @@ class Peercoin extends Bip39HDCurrency { bool get torSupport => true; @override - String constructDerivePath( - {required DerivePathType derivePathType, - int account = 0, - required int chain, - required int index}) { + String constructDerivePath({ + required DerivePathType derivePathType, + int account = 0, + required int chain, + required int index, + }) { String coinType; switch (networkParams.wifPrefix) { case 183: // PPC mainnet wif. @@ -164,6 +165,7 @@ class Peercoin extends Bip39HDCurrency { @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, + DerivePathType.bip84, ]; @override From 5b573c579c14ad5d1c66a9bdb9619f8d3af4a88e Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 11:21:07 -0600 Subject: [PATCH 256/272] fix server port and add testnet derivation path construction support --- lib/utilities/default_nodes.dart | 2 +- .../crypto_currency/coins/peercoin.dart | 31 +++++-------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index b6fffa789..87bddfc81 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -191,7 +191,7 @@ abstract class DefaultNodes { static NodeModel get peercoin => NodeModel( host: "electrum.peercoinexplorer.net", - port: 50004, + port: 50002, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(Coin.peercoin), useSSL: true, diff --git a/lib/wallets/crypto_currency/coins/peercoin.dart b/lib/wallets/crypto_currency/coins/peercoin.dart index 97ac2fa7f..4e54329de 100644 --- a/lib/wallets/crypto_currency/coins/peercoin.dart +++ b/lib/wallets/crypto_currency/coins/peercoin.dart @@ -37,9 +37,12 @@ class Peercoin extends Bip39HDCurrency { String coinType; switch (networkParams.wifPrefix) { case 183: // PPC mainnet wif. - coinType = "10"; // PPC mainnet. + coinType = + "6"; // according to https://github.com/satoshilabs/slips/blob/master/slip-0044.md + break; + case 239: // PPC testnet wif. + coinType = "1"; break; - // TODO: [prio=low] Add testnet. default: throw Exception("Invalid Peercoin network wif used!"); } @@ -63,29 +66,9 @@ class Peercoin extends Bip39HDCurrency { NodeModel get defaultNode { switch (network) { case CryptoCurrencyNetwork.main: - return NodeModel( - host: "electrum.peercoinexplorer.net", - port: 50004, - name: DefaultNodes.defaultName, - id: DefaultNodes.buildId(Coin.peercoin), - useSSL: true, - enabled: true, - coinName: Coin.peercoin.name, - isFailover: true, - isDown: false, - ); + return DefaultNodes.peercoin; case CryptoCurrencyNetwork.test: - return NodeModel( - host: "testnet-electrum.peercoinexplorer.net", - port: 50009, - name: DefaultNodes.defaultName, - id: DefaultNodes.buildId(Coin.peercoinTestNet), - useSSL: false, // TODO [prio=med]: Is this safe? - enabled: true, - coinName: Coin.peercoinTestNet.name, - isFailover: true, - isDown: false, - ); + return DefaultNodes.peercoinTestNet; default: throw UnimplementedError(); } From 5cda658bd243ab1f5f91d047fd79804cd9277c73 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 11:38:16 -0600 Subject: [PATCH 257/272] fix ppc testnet port and map coins to nodes instead oif manually --- lib/utilities/default_nodes.dart | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 87bddfc81..2e2bd71cc 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -17,31 +17,9 @@ abstract class DefaultNodes { static const String defaultName = "Stack Default"; @Deprecated("old and decrepit") - static List get all => [ - bitcoin, - litecoin, - dogecoin, - firo, - monero, - eCash, - epicCash, - ethereum, - bitcoincash, - namecoin, - wownero, - particl, - peercoin, - stellar, - nano, - banano, - tezos, - bitcoinTestnet, - litecoinTestNet, - bitcoincashTestnet, - dogecoinTestnet, - firoTestnet, - stellarTestnet, - ]; + static List get all => Coin.values + .map((e) => DefaultNodes.getNodeFor(e)) + .toList(growable: false); static NodeModel get bitcoin => NodeModel( host: "bitcoin.stackwallet.com", @@ -325,7 +303,7 @@ abstract class DefaultNodes { static NodeModel get peercoinTestNet => NodeModel( host: "testnet-electrum.peercoinexplorer.net", - port: 50009, + port: 50002, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(Coin.peercoinTestNet), useSSL: true, From a05287bae465761ddc665d2ca51719aa398cf31a Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 11:45:23 -0600 Subject: [PATCH 258/272] lints --- lib/wallets/wallet/impl/peercoin_wallet.dart | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart index 9e12066e8..d418ec485 100644 --- a/lib/wallets/wallet/impl/peercoin_wallet.dart +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -51,8 +51,9 @@ class PeercoinWallet extends Bip39HDWallet Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { return Amount( rawValue: BigInt.from( - ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil()), + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(), + ), fractionDigits: cryptoCurrency.fractionDigits, ); } @@ -83,14 +84,15 @@ class PeercoinWallet extends Bip39HDWallet @override Future updateTransactions() async { // Get all addresses. - List
allAddressesOld = await fetchAddressesForElectrumXScan(); + final List
allAddressesOld = + await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - Set receivingAddresses = allAddressesOld + final Set receivingAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.receiving) .map((e) => e.value) .toSet(); - Set changeAddresses = allAddressesOld + final Set changeAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.change) .map((e) => e.value) .toSet(); @@ -103,7 +105,7 @@ class PeercoinWallet extends Bip39HDWallet await fetchHistory(allAddressesSet); // Only parse new txs (not in db yet). - List> allTransactions = []; + final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. final storedTx = await mainDB.isar.transactionV2s @@ -164,8 +166,8 @@ class PeercoinWallet extends Bip39HDWallet ); final prevOutJson = Map.from( - (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) - as Map); + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map, + ); final prevOut = OutputV2.fromElectrumXJson( prevOutJson, @@ -239,7 +241,7 @@ class PeercoinWallet extends Bip39HDWallet .fold(BigInt.zero, (value, element) => value + element); TransactionType type; - TransactionSubType subType = TransactionSubType.none; + const TransactionSubType subType = TransactionSubType.none; // At least one input was owned by this wallet. if (wasSentFromThisWallet) { From 968f7e61cd9abdb34605535d3e32c867d6fb406d Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 11:45:33 -0600 Subject: [PATCH 259/272] use segwit primary --- lib/utilities/enums/coin_enum.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 591deb61a..903054b09 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -398,6 +398,8 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: return AddressType.p2wpkh; case Coin.bitcoinFrost: @@ -411,8 +413,6 @@ extension CoinExt on Coin { case Coin.firo: case Coin.firoTestNet: case Coin.dogecoinTestNet: - case Coin.peercoin: - case Coin.peercoinTestNet: return AddressType.p2pkh; case Coin.monero: From 68c5ec4f44fa93e1867e8ecef8ecf02e3d62302f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 11:58:06 -0600 Subject: [PATCH 260/272] update default themes with fixed peercoin svg --- assets/default_themes/dark.zip | Bin 950935 -> 950719 bytes assets/default_themes/light.zip | Bin 898825 -> 898609 bytes lib/themes/theme_service.dart | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip index f69e936f2c0e0dd169ffef1b0e4cb82e347b83f7..45b8dc11a18a8de1c2fd2e2c2fbe9e00740a3635 100644 GIT binary patch delta 3424 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 diff --git a/assets/default_themes/light.zip b/assets/default_themes/light.zip index 672fe6d494c6577982113a947594e02d02393af9..3f0e3e96105f4ecfcc2e8ae8c4e855ac738ec405 100644 GIT binary patch delta 3193 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 S%E`+DNvolSa8wS&?tcN2szRLr diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index b41775ee6..b5882ecc5 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -29,7 +29,7 @@ final pThemeService = Provider((ref) { }); class ThemeService { - static const _currentDefaultThemeVersion = 8; + static const _currentDefaultThemeVersion = 9; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._(); From c34791ea9618e90ce0b61d1ed2192fdebffda7a5 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 12:00:16 -0600 Subject: [PATCH 261/272] add todo stubs --- lib/wallets/wallet/impl/peercoin_wallet.dart | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart index d418ec485..535f537b9 100644 --- a/lib/wallets/wallet/impl/peercoin_wallet.dart +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -49,18 +49,22 @@ class PeercoinWallet extends Bip39HDWallet @override Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return Amount( - rawValue: BigInt.from( - ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), - ), - fractionDigits: cryptoCurrency.fractionDigits, - ); + // TODO: check ppc fee stuff + throw Exception("TODO"); + // return Amount( + // rawValue: BigInt.from( + // ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + // (feeRatePerKB / 1000).ceil(), + // ), + // fractionDigits: cryptoCurrency.fractionDigits, + // ); } @override int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + // TODO: check ppc fee stuff + throw Exception("TODO"); + // return vSize * (feeRatePerKB / 1000).ceil(); } // =========================================================================== From 1b8fb18c7495961ac60f48ee5e621ba82ab9955d Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 15:04:10 -0600 Subject: [PATCH 262/272] fix missing theme version number update --- assets/default_themes/dark.zip | Bin 950719 -> 949779 bytes assets/default_themes/light.zip | Bin 898609 -> 897669 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip index 45b8dc11a18a8de1c2fd2e2c2fbe9e00740a3635..c6d417500341f8b5116761339875b0ab24860283 100644 GIT binary patch delta 3160 zcmZWs3s6+o89w*kWp~;2oJu7i>_WIJkOf!R4H`puOpp*0#|QE%De^GNbHOEoB9G-! zGf6ZhLKZk^tSM8?Xks2xCa0~UR^nJm=%`gwwXK~YFY{p9h^Y@6``>$aSMHRV9p--D z|2qHq@Bi(d*#3lPCb_4NL$tkugdREE{c$fpzqi!rsr}?KK}e787QI!tIra3dxx0Ux zJ+XdwfS?IRh`OstD9v*i!|Aaq633z{xro=j)D%#x`VJTVh!-fU_SwE!O%mDdD1OmU zm>k9%e%Kan`LbL<-)97%g>7k2M$NAAMcq|H ztn8*$TYSVzos_7{9vnjJog`quNo;KFs`lCKKb@WQc*acc^_484MCVwgBAWMHO?k=9 zy57-UbgRmYO>lo*k&Dpa4TNIlGT9q(`o3Im*6vz3cadAaoO*%#04P05uY(eT9}1iq zRRw%1wPbNxH$o#F9I_|@3;q&V*{v#PXZ!dsrnj=|WrnP#hh`t3x+n8^Pa#6zQAdeR zWHo;^94}OPuFh;v{`W?-#QWty3(xF0VR1DVA1T&6>+4(Qn(B*^JOw(V0qmGzy1E)Ib-#kI& ziUy%!gSU5KlE04CM`BGt$n0ZH>KCO=BZP)HnlXqi?AQWq;k>03Z-!C~F*s3Bo#$T8 z;d==JE3)F31Xb2ce;Um<@Cfw?NEs|SD`s{ZUd*d3CoZ?2w}WMT075a+RfG*R!mM6O zz>o5(uFr&j-hZD%sDutB;b@laz&74n-FuBt-N7V$i&ItiT;6B8^Bg;|1dHmN1$O6| z90*kkdF6SMtJ5;;q+NUZ|m8wQt9{e>QI{Play?ji66YjDFMr4h{Xp(1!{WyRl)BzR`WmdaPENu{vnY_(B(yR=m zGH2|W5qyGIwRI+9YS{$G;uxN*CW?R4KYd8Uihhs3*Qh+k?oL{*9CIDVb?WqNj(z(D zp8a?NhpGqbMj1YLWVrg~`)X|eibZmSTF{>~7%EivUKZE5Ayb3Y1EN$83p1K>uk7@4wfn(3+ z5WPA?{%y?{JcKv^pGY|bgJg+Z(CjN0#vk5&pZznREcXp)S10r#_h+O-9j&(bi`0II zlmP`TkfNDb$j`|s&f$v|-}H82(A5vW-4Te;b4w61ATw&LDz7U~sA+I*%Fhc#f_-Ot zg?%TDSVu}ZEpx9Usev@I0v6O{IoYE~+AG{rs;eZ0TqiB7BpbMk(!VfI{e;G2p0Ln! z)kF)1hAJZRwh6kvlFSD#k^b6A^nioRiTs-tG^iSG4V2CS80eeGndsbVvId?&+(cn* z6z}0COpTD0eDCP|Ngd4cK*sKJBQT*HfGh!7>JS0S2&1~$alBV4{FJ3@C7Lng@4Tp zlfUUluV4yozpYNnZd{b(WjfM4wg+8KS6A;TjRS3`W#KPN+vco2a zG2Z8p>H>!y{R+|Ql8*sOBMONrb7W%>8>yurF`|&xtYD?}B%dD%r{^08s`ptDTyg{ycWdp=1jh{t;s`z_&T4FEQB=%wYZo&iN@kSuZ6BPo~!{1y` z50M{tNCb!ijot!Nc8#8s{L8natl*#y ze*6c?Vb>0Y08#K4;cNn8!!H#AG{Bz%G(hBE+@%m81JXd|ZG#fGQEnTQ*snlm0pc6` z6f@ZK{(hu1197%rA=;YdyMq-k^81b`7%ccW-`GUA0MS1}m$#4%$g@LwWFOMQEwEG& zot7St9;KnJBoiKsTH*1DQQ8iVF}|x*7B@l;!l7W*Rc<(m>62r4i9c5xjNUwv(Dil_ zEG=>AJ%!aR3L6*6V5ZL!*0(5>q!W5Bi$q8(T(WF%!{jfMv1fdSU%A2XyQgK)Os(z2 z28Q|V0B&AEXizq+l8;vrf6vcK%ttF_=4oGy)D8e|t(L)Hn%zk>v?k|)&tZxF$y%8< z(Qi8B6!SX)tjl@eBdr(Mz9N4*m+tQ*Pr>62$)>E}fsKa7bP;<>qyXRQ;p_PSFWvd_ mTL6n1?e8imB&ro0Agy4!wu#K6?{pD^;i?IC`Sb9z0sRkDyOeYQ delta 4370 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 diff --git a/assets/default_themes/light.zip b/assets/default_themes/light.zip index 3f0e3e96105f4ecfcc2e8ae8c4e855ac738ec405..d94ce2ac8660b38e4e2889e3ed0e2add9a124c89 100644 GIT binary patch delta 5341 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 J-G%}8e*n5d8{Pl_ delta 6591 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 Date: Fri, 10 May 2024 15:54:41 -0600 Subject: [PATCH 263/272] dirty peercoin fee calc hack --- lib/wallets/wallet/impl/peercoin_wallet.dart | 23 +++++++++---------- .../electrumx_interface.dart | 4 +++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart index 535f537b9..e08566f45 100644 --- a/lib/wallets/wallet/impl/peercoin_wallet.dart +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -49,22 +49,21 @@ class PeercoinWallet extends Bip39HDWallet @override Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - // TODO: check ppc fee stuff - throw Exception("TODO"); - // return Amount( - // rawValue: BigInt.from( - // ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - // (feeRatePerKB / 1000).ceil(), - // ), - // fractionDigits: cryptoCurrency.fractionDigits, - // ); + // TODO: actually do this properly for peercoin + // this is probably wrong for peercoin + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(), + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); } + /// we can just pretend vSize is size for peercoin @override int estimateTxFee({required int vSize, required int feeRatePerKB}) { - // TODO: check ppc fee stuff - throw Exception("TODO"); - // return vSize * (feeRatePerKB / 1000).ceil(); + return vSize * (feeRatePerKB / 1000).ceil(); } // =========================================================================== diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index b4fdb7a9d..026e9f5fc 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -24,6 +24,7 @@ import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/peercoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; @@ -768,7 +769,8 @@ mixin ElectrumXInterface on Bip39HDWallet { return txData.copyWith( raw: clTx.toHex(), - vSize: clTx.vSize(), + // dirty shortcut for peercoin's weirdness + vSize: this is PeercoinWallet ? clTx.size : clTx.vSize(), tempTx: TransactionV2( walletId: walletId, blockHash: null, From ba0e3e1fbd9000976e4f31458e837437c95cc00e Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 10 May 2024 15:59:23 -0600 Subject: [PATCH 264/272] update mocks --- test/cached_electrumx_test.mocks.dart | 26 ------------------- .../pages/send_view/send_view_test.mocks.dart | 26 ------------------- .../exchange/exchange_view_test.mocks.dart | 26 ------------------- ...allet_settings_view_screen_test.mocks.dart | 2 ++ .../bitcoin/bitcoin_wallet_test.mocks.dart | 2 ++ .../bitcoincash_wallet_test.mocks.dart | 2 ++ .../dogecoin/dogecoin_wallet_test.mocks.dart | 2 ++ .../namecoin/namecoin_wallet_test.mocks.dart | 2 ++ .../particl/particl_wallet_test.mocks.dart | 2 ++ .../managed_favorite_test.mocks.dart | 26 ------------------- .../node_options_sheet_test.mocks.dart | 26 ------------------- .../transaction_card_test.mocks.dart | 26 ------------------- 12 files changed, 12 insertions(+), 156 deletions(-) diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 1b032ceff..cb1317096 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -884,32 +884,6 @@ class MockPrefs extends _i1.Mock implements _i7.Prefs { returnValueForMissingStub: null, ); @override - bool get solanaEnabled => (super.noSuchMethod( - Invocation.getter(#solanaEnabled), - returnValue: false, - ) as bool); - @override - set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( - Invocation.setter( - #solanaEnabled, - solanaEnabled, - ), - returnValueForMissingStub: null, - ); - @override - bool get frostEnabled => (super.noSuchMethod( - Invocation.getter(#frostEnabled), - returnValue: false, - ) as bool); - @override - set frostEnabled(bool? frostEnabled) => super.noSuchMethod( - Invocation.setter( - #frostEnabled, - frostEnabled, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 710bc087e..fba574b15 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -978,32 +978,6 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override - bool get solanaEnabled => (super.noSuchMethod( - Invocation.getter(#solanaEnabled), - returnValue: false, - ) as bool); - @override - set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( - Invocation.setter( - #solanaEnabled, - solanaEnabled, - ), - returnValueForMissingStub: null, - ); - @override - bool get frostEnabled => (super.noSuchMethod( - Invocation.getter(#frostEnabled), - returnValue: false, - ) as bool); - @override - set frostEnabled(bool? frostEnabled) => super.noSuchMethod( - Invocation.setter( - #frostEnabled, - frostEnabled, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 63ddafc1b..1d97e1d48 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -454,32 +454,6 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); @override - bool get solanaEnabled => (super.noSuchMethod( - Invocation.getter(#solanaEnabled), - returnValue: false, - ) as bool); - @override - set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( - Invocation.setter( - #solanaEnabled, - solanaEnabled, - ), - returnValueForMissingStub: null, - ); - @override - bool get frostEnabled => (super.noSuchMethod( - Invocation.getter(#frostEnabled), - returnValue: false, - ) as bool); - @override - set frostEnabled(bool? frostEnabled) => super.noSuchMethod( - Invocation.setter( - #frostEnabled, - frostEnabled, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index bb7ed9b5b..5bc43e816 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -78,6 +78,7 @@ class MockCachedElectrumXClient extends _i1.Mock required String? groupId, String? blockhash = r'', required _i5.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -87,6 +88,7 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index 85e32012e..999e3f135 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -548,6 +548,7 @@ class MockCachedElectrumXClient extends _i1.Mock required String? groupId, String? blockhash = r'', required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -557,6 +558,7 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index 0c2ab7d27..9886b150a 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -548,6 +548,7 @@ class MockCachedElectrumXClient extends _i1.Mock required String? groupId, String? blockhash = r'', required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -557,6 +558,7 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 20a3938e6..f9cc4af74 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -548,6 +548,7 @@ class MockCachedElectrumXClient extends _i1.Mock required String? groupId, String? blockhash = r'', required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -557,6 +558,7 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index 34a2b7c9e..c5ed61ed5 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -548,6 +548,7 @@ class MockCachedElectrumXClient extends _i1.Mock required String? groupId, String? blockhash = r'', required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -557,6 +558,7 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 447d01329..587dbe42e 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -548,6 +548,7 @@ class MockCachedElectrumXClient extends _i1.Mock required String? groupId, String? blockhash = r'', required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -557,6 +558,7 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index ebb7ca994..e78909a96 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -712,32 +712,6 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override - bool get solanaEnabled => (super.noSuchMethod( - Invocation.getter(#solanaEnabled), - returnValue: false, - ) as bool); - @override - set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( - Invocation.setter( - #solanaEnabled, - solanaEnabled, - ), - returnValueForMissingStub: null, - ); - @override - bool get frostEnabled => (super.noSuchMethod( - Invocation.getter(#frostEnabled), - returnValue: false, - ) as bool); - @override - set frostEnabled(bool? frostEnabled) => super.noSuchMethod( - Invocation.setter( - #frostEnabled, - frostEnabled, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index ad81db4cd..3d0a10b68 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -598,32 +598,6 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override - bool get solanaEnabled => (super.noSuchMethod( - Invocation.getter(#solanaEnabled), - returnValue: false, - ) as bool); - @override - set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( - Invocation.setter( - #solanaEnabled, - solanaEnabled, - ), - returnValueForMissingStub: null, - ); - @override - bool get frostEnabled => (super.noSuchMethod( - Invocation.getter(#frostEnabled), - returnValue: false, - ) as bool); - @override - set frostEnabled(bool? frostEnabled) => super.noSuchMethod( - Invocation.setter( - #frostEnabled, - frostEnabled, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index fc08360ac..e3880f1b6 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -698,32 +698,6 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { returnValueForMissingStub: null, ); @override - bool get solanaEnabled => (super.noSuchMethod( - Invocation.getter(#solanaEnabled), - returnValue: false, - ) as bool); - @override - set solanaEnabled(bool? solanaEnabled) => super.noSuchMethod( - Invocation.setter( - #solanaEnabled, - solanaEnabled, - ), - returnValueForMissingStub: null, - ); - @override - bool get frostEnabled => (super.noSuchMethod( - Invocation.getter(#frostEnabled), - returnValue: false, - ) as bool); - @override - set frostEnabled(bool? frostEnabled) => super.noSuchMethod( - Invocation.setter( - #frostEnabled, - frostEnabled, - ), - returnValueForMissingStub: null, - ); - @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, From c9ea423495dfd034734639f00cc6af5fbe430c72 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Fri, 10 May 2024 16:08:37 -0600 Subject: [PATCH 265/272] Update version (v2.0.0, build 221) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 607d7b920..fd2db4810 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.0.0+220 +version: 2.0.0+221 environment: sdk: ">=3.3.4 <4.0.0" From 032a507e72e03cd8602848740b03bf88c6eda986 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 13 May 2024 08:10:53 -0600 Subject: [PATCH 266/272] frost db tx fix and some lint clean up --- .../wallet/impl/bitcoin_frost_wallet.dart | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 1798fd79f..5d0bb3ffc 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -180,10 +180,12 @@ class BitcoinFrostWallet extends Wallet { final config = Frost.createSignConfig( network: network, inputs: utxosToUse - .map((e) => ( - utxo: e, - scriptPubKey: publicKey, - )) + .map( + (e) => ( + utxo: e, + scriptPubKey: publicKey, + ), + ) .toList(), outputs: txData.recipients!, changeAddress: (await getCurrentReceivingAddress())!.value, @@ -278,8 +280,9 @@ class BitcoinFrostWallet extends Wallet { Amount _roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { return Amount( rawValue: BigInt.from( - ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil()), + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(), + ), fractionDigits: cryptoCurrency.fractionDigits, ); } @@ -333,7 +336,7 @@ class BitcoinFrostWallet extends Wallet { final currentHeight = await chainHeight; final coin = info.coin; - List> allTransactions = []; + final List> allTransactions = []; for (final txHash in allTxHashes) { final storedTx = await mainDB.isar.transactionV2s @@ -390,8 +393,8 @@ class BitcoinFrostWallet extends Wallet { ); final prevOutJson = Map.from( - (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) - as Map); + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map, + ); final prevOut = OutputV2.fromElectrumXJson( prevOutJson, @@ -456,7 +459,8 @@ class BitcoinFrostWallet extends Wallet { TransactionSubType subType = TransactionSubType.none; if (outputs.length > 1 && inputs.isNotEmpty) { for (int i = 0; i < outputs.length; i++) { - List? scriptChunks = outputs[i].scriptPubKeyAsm?.split(" "); + final List? scriptChunks = + outputs[i].scriptPubKeyAsm?.split(" "); if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { final blindedPaymentCode = scriptChunks![1]; final bytes = blindedPaymentCode.toUint8ListFromHex; @@ -575,8 +579,10 @@ class BitcoinFrostWallet extends Wallet { return txData; } catch (e, s) { - Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", - level: LogLevel.Error); + Logging.instance.log( + "Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error, + ); rethrow; } } @@ -685,7 +691,7 @@ class BitcoinFrostWallet extends Wallet { multisigConfig = await getMultisigConfig(); } if (serializedKeys == null || multisigConfig == null) { - String err = "${info.coinName} wallet ${info.walletId} had null keys/cfg"; + final err = "${info.coinName} wallet ${info.walletId} had null keys/cfg"; Logging.instance.log(err, level: LogLevel.Fatal); throw Exception(err); // TODO [prio=low]: handle null keys or config. This should not happen. @@ -713,7 +719,7 @@ class BitcoinFrostWallet extends Wallet { if (knownSalts.contains(salt)) { throw Exception("Known frost multisig salt found!"); } - List updatedKnownSalts = List.from(knownSalts); + final List updatedKnownSalts = List.from(knownSalts); updatedKnownSalts.add(salt); await _updateKnownSalts(updatedKnownSalts); } else { @@ -1015,8 +1021,9 @@ class BitcoinFrostWallet extends Wallet { .findFirstSync()!; Future _updateParticipants(List participants) async { + final info = frostInfo; + await mainDB.isar.writeTxn(() async { - final info = frostInfo; await mainDB.isar.frostWalletInfo.delete(info.id); await mainDB.isar.frostWalletInfo.put( info.copyWith(participants: participants), @@ -1031,8 +1038,9 @@ class BitcoinFrostWallet extends Wallet { .findFirstSync()!; Future _updateThreshold(int threshold) async { + final info = frostInfo; + await mainDB.isar.writeTxn(() async { - final info = frostInfo; await mainDB.isar.frostWalletInfo.delete(info.id); await mainDB.isar.frostWalletInfo.put( info.copyWith(threshold: threshold), @@ -1047,8 +1055,9 @@ class BitcoinFrostWallet extends Wallet { .findFirstSync()!; Future _updateMyName(String myName) async { + final info = frostInfo; + await mainDB.isar.writeTxn(() async { - final info = frostInfo; await mainDB.isar.frostWalletInfo.delete(info.id); await mainDB.isar.frostWalletInfo.put( info.copyWith(myName: myName), @@ -1074,13 +1083,15 @@ class BitcoinFrostWallet extends Wallet { Future _updateElectrumX() async { final failovers = nodeService .failoverNodesFor(coin: cryptoCurrency.coin) - .map((e) => ElectrumXNode( - address: e.host, - port: e.port, - name: e.name, - id: e.id, - useSSL: e.useSSL, - )) + .map( + (e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + ), + ) .toList(); final newNode = await _getCurrentElectrumXNode(); @@ -1109,7 +1120,9 @@ class BitcoinFrostWallet extends Wallet { } bool _duplicateTxCheck( - List> allTransactions, String txid) { + List> allTransactions, + String txid, + ) { for (int i = 0; i < allTransactions.length; i++) { if (allTransactions[i]["txid"] == txid) { return true; From 441faf4d3ab53f04db6a022f34e80a4e0e9a289b Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 13 May 2024 08:11:12 -0600 Subject: [PATCH 267/272] fix frost coin assets --- lib/themes/coin_card_provider.dart | 14 -------------- lib/themes/coin_icon_provider.dart | 7 ------- lib/themes/coin_image_provider.dart | 14 -------------- 3 files changed, 35 deletions(-) diff --git a/lib/themes/coin_card_provider.dart b/lib/themes/coin_card_provider.dart index ce5e71038..b34e9e6f1 100644 --- a/lib/themes/coin_card_provider.dart +++ b/lib/themes/coin_card_provider.dart @@ -16,13 +16,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; final coinCardProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); - // TODO: handle this differently by adding proper frost assets to themes - if (coin == Coin.bitcoinFrost) { - coin = Coin.bitcoin; - } else if (coin == Coin.bitcoinFrostTestNet) { - coin = Coin.bitcoinTestNet; - } - if (assets is ThemeAssetsV3) { return assets.coinCardImages?[coin.mainNetVersion]; } else { @@ -33,13 +26,6 @@ final coinCardProvider = Provider.family((ref, coin) { final coinCardFavoritesProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); - // TODO: handle this differently by adding proper frost assets to themes - if (coin == Coin.bitcoinFrost) { - coin = Coin.bitcoin; - } else if (coin == Coin.bitcoinFrostTestNet) { - coin = Coin.bitcoinTestNet; - } - if (assets is ThemeAssetsV3) { return assets.coinCardFavoritesImages?[coin.mainNetVersion] ?? assets.coinCardImages?[coin.mainNetVersion]; diff --git a/lib/themes/coin_icon_provider.dart b/lib/themes/coin_icon_provider.dart index f0c0df842..9bd3990bb 100644 --- a/lib/themes/coin_icon_provider.dart +++ b/lib/themes/coin_icon_provider.dart @@ -16,13 +16,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; final coinIconProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); - // TODO: handle this differently by adding proper frost assets to themes - if (coin == Coin.bitcoinFrost) { - coin = Coin.bitcoin; - } else if (coin == Coin.bitcoinFrostTestNet) { - coin = Coin.bitcoinTestNet; - } - if (assets is ThemeAssets) { switch (coin) { case Coin.bitcoin: diff --git a/lib/themes/coin_image_provider.dart b/lib/themes/coin_image_provider.dart index fa1fdde4c..6ca839fb9 100644 --- a/lib/themes/coin_image_provider.dart +++ b/lib/themes/coin_image_provider.dart @@ -16,13 +16,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; final coinImageProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); - // TODO: handle this differently by adding proper frost assets to themes - if (coin == Coin.bitcoinFrost) { - coin = Coin.bitcoin; - } else if (coin == Coin.bitcoinFrostTestNet) { - coin = Coin.bitcoinTestNet; - } - if (assets is ThemeAssets) { switch (coin) { case Coin.bitcoin: @@ -71,13 +64,6 @@ final coinImageProvider = Provider.family((ref, coin) { final coinImageSecondaryProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); - // TODO: handle this differently by adding proper frost assets to themes - if (coin == Coin.bitcoinFrost) { - coin = Coin.bitcoin; - } else if (coin == Coin.bitcoinFrostTestNet) { - coin = Coin.bitcoinTestNet; - } - if (assets is ThemeAssets) { switch (coin) { case Coin.bitcoin: From 3f4ebe022945bea4224648e1a8acd64893a652ab Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 13 May 2024 08:55:27 -0600 Subject: [PATCH 268/272] specific error dialog when an unrecoverable frost error has occurred --- lib/frost_route_generator.dart | 1 + .../new/create_new_frost_ms_wallet_view.dart | 1 + .../select_new_frost_import_type_view.dart | 3 +- .../new/steps/frost_create_step_2.dart | 4 +- .../new/steps/frost_create_step_3.dart | 4 +- .../reshare/frost_reshare_step_1a.dart | 5 +- .../reshare/frost_reshare_step_1b.dart | 5 +- .../reshare/frost_reshare_step_1c.dart | 1 + .../reshare/frost_reshare_step_2abd.dart | 5 +- .../reshare/frost_reshare_step_2c.dart | 19 +-- .../reshare/frost_reshare_step_3abd.dart | 20 +-- .../reshare/frost_reshare_step_4.dart | 5 +- .../reshare/frost_reshare_step_5.dart | 5 +- .../send_view/frost_ms/frost_send_view.dart | 1 + .../send_steps/frost_send_step_1b.dart | 18 ++- .../send_steps/frost_send_step_2.dart | 17 +- .../send_steps/frost_send_step_3.dart | 5 +- .../frost_ms/frost_ms_options_view.dart | 1 + .../complete_reshare_config_view.dart | 26 +-- lib/pages/wallet_view/wallet_view.dart | 151 ++++++++++-------- .../wallet_view/sub_widgets/my_wallet.dart | 4 +- .../dialogs/frost/frost_error_dialog.dart | 92 +++++++++++ 22 files changed, 259 insertions(+), 134 deletions(-) create mode 100644 lib/widgets/dialogs/frost/frost_error_dialog.dart diff --git a/lib/frost_route_generator.dart b/lib/frost_route_generator.dart index a6905a243..d401c7030 100644 --- a/lib/frost_route_generator.dart +++ b/lib/frost_route_generator.dart @@ -41,6 +41,7 @@ final pFrostScaffoldArgs = StateProvider< List stepRoutes, FrostInterruptionDialogType frostInterruptionDialogType, NavigatorState parentNav, + String callerRouteName, })?>((ref) => null); abstract class FrostRouteGenerator { diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart index c8bf16e26..5ce23eaad 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -431,6 +431,7 @@ class _NewFrostMsWalletViewState frostInterruptionDialogType: FrostInterruptionDialogType.walletCreation, parentNav: Navigator.of(context), + callerRouteName: CreateNewFrostMsWalletView.routeName, ); await Navigator.of(context).pushNamed( diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart index b438baac5..1a7378bc6 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -131,7 +131,6 @@ class _SelectNewFrostImportTypeViewState PrimaryButton( label: "Continue", onPressed: () async { - final String route; switch (_selectedOption) { case _ImportOption.multisigNew: ref.read(pFrostScaffoldArgs.state).state = ( @@ -144,6 +143,7 @@ class _SelectNewFrostImportTypeViewState parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.walletCreation, + callerRouteName: SelectNewFrostImportTypeView.routeName, ); break; @@ -158,6 +158,7 @@ class _SelectNewFrostImportTypeViewState parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.resharing, + callerRouteName: SelectNewFrostImportTypeView.routeName, ); break; } diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart index 181a7c3d6..f993d6783 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -11,6 +11,7 @@ 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'; @@ -183,10 +184,9 @@ class _FrostCreateStep2State extends ConsumerState { if (context.mounted) { return await showDialog( context: context, - builder: (_) => StackOkDialog( + builder: (_) => FrostErrorDialog( title: "Failed to generate shares", message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, ), ); } diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart index 782d86135..54cabcec0 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -11,6 +11,7 @@ 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'; @@ -184,9 +185,8 @@ class _FrostCreateStep3State extends ConsumerState { if (context.mounted) { return await showDialog( context: context, - builder: (_) => StackOkDialog( + builder: (_) => const FrostErrorDialog( title: "Failed to complete key generation", - desktopPopRootNavigator: Util.isDesktop, ), ); } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart index 46f272daf..3b20a8820 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart @@ -20,9 +20,9 @@ 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'; -import 'package:stackwallet/widgets/stack_dialog.dart'; class FrostReshareStep1a extends ConsumerStatefulWidget { const FrostReshareStep1a({super.key}); @@ -88,9 +88,8 @@ class _FrostReshareStep1aState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( + builder: (_) => FrostErrorDialog( title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, ), ); } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart index 71fe0e737..0df4c4b09 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart @@ -12,8 +12,8 @@ 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/stack_dialog.dart'; import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep1b extends ConsumerStatefulWidget { @@ -124,9 +124,8 @@ class _FrostReshareStep1bState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( + builder: (_) => FrostErrorDialog( title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, ), ); } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart index adc1c7f5e..2bbfef1de 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -190,6 +190,7 @@ class _FrostReshareStep1cState extends ConsumerState { parentNav: data.parentNav, frostInterruptionDialogType: FrostInterruptionDialogType.resharing, + callerRouteName: data.callerRouteName, ); ref.read(pFrostMyName.state).state = ref.read(pFrostResharingData).myName!; diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart index 63edc03cf..1a688bb03 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart @@ -15,7 +15,7 @@ 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/stack_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep2abd extends ConsumerStatefulWidget { @@ -86,10 +86,9 @@ class _FrostReshareStep2abdState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( + builder: (_) => FrostErrorDialog( title: "Error", message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, ), ); } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart index 0c811c635..798c503e5 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart @@ -8,7 +8,7 @@ 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/stack_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep2c extends ConsumerStatefulWidget { @@ -63,14 +63,15 @@ class _FrostReshareStep2cState extends ConsumerState { level: LogLevel.Fatal, ); - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: "Error", + message: e.toString(), + ), + ); + } } finally { _buttonLock = false; } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart index 5bccf7756..d49df3cc7 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -13,7 +13,7 @@ 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/stack_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostReshareStep3abd extends ConsumerStatefulWidget { @@ -73,15 +73,15 @@ class _FrostReshareStep3abdState extends ConsumerState { "$e\n$s", level: LogLevel.Fatal, ); - - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: "Error", + message: e.toString(), + ), + ); + } } finally { _buttonLock = false; } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart index 2eb7c8f65..12de74573 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -17,7 +17,7 @@ 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/stack_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; // was FinishResharingView @@ -94,10 +94,9 @@ class _FrostReshareStep4State extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( + builder: (_) => FrostErrorDialog( title: "Error", message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, ), ); } diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart index 2171f5ad8..fe0875747 100644 --- a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart @@ -20,7 +20,7 @@ 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/stack_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; // was VerifyUpdatedWalletView class FrostReshareStep5 extends ConsumerStatefulWidget { @@ -110,10 +110,9 @@ class _FrostReshareStep5State extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( + builder: (_) => FrostErrorDialog( title: "Error", message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, ), ); } diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 133d0afd4..ccaf5452e 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -139,6 +139,7 @@ class _FrostSendViewState extends ConsumerState { parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.transactionCreation, + callerRouteName: FrostSendView.routeName, ); await Navigator.of(context).pushNamed( diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart index 197d72e63..5015110aa 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart @@ -109,14 +109,16 @@ class _FrostSendStep1bState extends ConsumerState { "$e\n$s", level: LogLevel.Error, ); - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Import and attempt sign config failed", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), - ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Import and attempt sign config failed", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } } finally { _attemptSignLock = false; } diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart index 66f088b39..7fbea39da 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart @@ -14,8 +14,8 @@ 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/stack_dialog.dart'; import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; class FrostSendStep2 extends ConsumerStatefulWidget { @@ -293,13 +293,14 @@ class _FrostSendStep2State extends ConsumerState { level: LogLevel.Fatal, ); - return await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to continue signing", - desktopPopRootNavigator: Util.isDesktop, - ), - ); + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => const FrostErrorDialog( + title: "Failed to continue signing", + ), + ); + } } }, ), diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart index bb7e462b9..46bb5cdb0 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -15,8 +15,8 @@ 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 FrostSendStep3 extends ConsumerStatefulWidget { @@ -238,9 +238,8 @@ class _FrostSendStep3State extends ConsumerState { if (context.mounted) { return await showDialog( context: context, - builder: (_) => StackOkDialog( + builder: (_) => const FrostErrorDialog( title: "Failed to complete signing process", - desktopPopRootNavigator: Util.isDesktop, ), ); } diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart index c18d78477..491f96c93 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -162,6 +162,7 @@ class FrostMSWalletOptionsView extends ConsumerWidget { parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.resharing, + callerRouteName: FrostMSWalletOptionsView.routeName, ); Navigator.of(context).pushNamed( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart index 7d8a61bcf..5ac986a6c 100644 --- a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart @@ -129,18 +129,19 @@ class _CompleteReshareConfigViewState final wallet = ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; - 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, - ); - 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, ); @@ -345,7 +346,8 @@ class _CompleteReshareConfigViewState () => _includeMeInReshare = value == true, ); _participantsCountChanged( - _newParticipantsCountController.text); + _newParticipantsCountController.text, + ); }, ), ), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 20bd28575..57d404c0a 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -277,25 +277,27 @@ class _WalletViewState extends ConsumerState { const timeout = Duration(milliseconds: 1500); if (_cachedTime == null || now.difference(_cachedTime!) > timeout) { _cachedTime = now; - unawaited(showDialog( - context: context, - barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async { - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName), - ); - _logout(); - return false; - }, - child: const StackDialog(title: "Tap back again to exit wallet"), + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async { + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName), + ); + _logout(); + return false; + }, + child: const StackDialog(title: "Tap back again to exit wallet"), + ), + ).timeout( + timeout, + onTimeout: () => Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName), + ), ), - ).timeout( - timeout, - onTimeout: () => Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ), - )); + ); } return false; } @@ -373,6 +375,7 @@ class _WalletViewState extends ConsumerState { parentNav: Navigator.of(context), frostInterruptionDialogType: FrostInterruptionDialogType.transactionCreation, + callerRouteName: WalletView.routeName, ); await Navigator.of(context).pushNamed( @@ -398,11 +401,12 @@ class _WalletViewState extends ConsumerState { .tickerEqualToAnyExchangeNameName(coin.ticker) .findFirst(); } catch (_) { - _future = ExchangeDataLoadingService.instance.loadAll().then((_) => - ExchangeDataLoadingService.instance.isar.currencies - .where() - .tickerEqualToAnyExchangeNameName(coin.ticker) - .findFirst()); + _future = ExchangeDataLoadingService.instance.loadAll().then( + (_) => ExchangeDataLoadingService.instance.isar.currencies + .where() + .tickerEqualToAnyExchangeNameName(coin.ticker) + .findFirst(), + ); } final currency = await showLoading( @@ -411,7 +415,7 @@ class _WalletViewState extends ConsumerState { message: "Loading...", ); - if (mounted) { + if (context.mounted) { unawaited( Navigator.of(context).pushNamed( WalletInitiatedExchangeView.routeName, @@ -568,7 +572,7 @@ class _WalletViewState extends ConsumerState { }, ), ), - ) + ), ], ), ); @@ -607,7 +611,7 @@ class _WalletViewState extends ConsumerState { style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), - ) + ), ], ), actions: [ @@ -670,9 +674,12 @@ class _WalletViewState extends ConsumerState { color: Theme.of(context) .extension()! .background, - icon: ref.watch(notificationsProvider.select( - (value) => value - .hasUnreadNotificationsFor(walletId))) + icon: ref.watch( + notificationsProvider.select( + (value) => + value.hasUnreadNotificationsFor(walletId), + ), + ) ? SvgPicture.file( File( ref.watch( @@ -683,10 +690,14 @@ class _WalletViewState extends ConsumerState { ), width: 20, height: 20, - color: ref.watch(notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor( - walletId))) + color: ref.watch( + notificationsProvider.select( + (value) => + value.hasUnreadNotificationsFor( + walletId, + ), + ), + ) ? null : Theme.of(context) .extension()! @@ -696,10 +707,14 @@ class _WalletViewState extends ConsumerState { Assets.svg.bell, width: 20, height: 20, - color: ref.watch(notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor( - walletId))) + color: ref.watch( + notificationsProvider.select( + (value) => + value.hasUnreadNotificationsFor( + walletId, + ), + ), + ) ? null : Theme.of(context) .extension()! @@ -720,22 +735,25 @@ class _WalletViewState extends ConsumerState { .state; if (unreadNotificationIds.isEmpty) return; - List> futures = []; + final List> futures = []; for (int i = 0; i < unreadNotificationIds.length - 1; i++) { - futures.add(ref - .read(notificationsProvider) - .markAsRead( + futures.add( + ref.read(notificationsProvider).markAsRead( unreadNotificationIds.elementAt(i), - false)); + false, + ), + ); } // wait for multiple to update if any Future.wait(futures).then((_) { // only notify listeners once ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.last, true); + unreadNotificationIds.last, + true, + ); }); }); }, @@ -824,7 +842,8 @@ class _WalletViewState extends ConsumerState { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle( - context), + context, + ), onPressed: () async { await showDialog( context: context, @@ -855,7 +874,8 @@ class _WalletViewState extends ConsumerState { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle( - context), + context, + ), child: Text( "Continue", style: @@ -1073,21 +1093,22 @@ class _WalletViewState extends ConsumerState { ), if (coin == Coin.banano) WalletNavigationBarItemData( - icon: SvgPicture.asset( - Assets.svg.monkey, - height: 20, - width: 20, - color: Theme.of(context) - .extension()! - .bottomNavIconIcon, - ), - label: "MonKey", - onTap: () { - Navigator.of(context).pushNamed( - MonkeyView.routeName, - arguments: widget.walletId, - ); - }), + icon: SvgPicture.asset( + Assets.svg.monkey, + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .bottomNavIconIcon, + ), + label: "MonKey", + onTap: () { + Navigator.of(context).pushNamed( + MonkeyView.routeName, + arguments: widget.walletId, + ); + }, + ), if (ref.watch( pWallets.select( (value) => value.getWallet(widget.walletId) @@ -1112,8 +1133,12 @@ class _WalletViewState extends ConsumerState { ); }, ), - if (ref.watch(pWallets.select((value) => - value.getWallet(widget.walletId) is PaynymInterface))) + if (ref.watch( + pWallets.select( + (value) => + value.getWallet(widget.walletId) is PaynymInterface, + ), + )) WalletNavigationBarItemData( label: "PayNym", icon: const PaynymNavIcon(), @@ -1145,7 +1170,7 @@ class _WalletViewState extends ConsumerState { level: LogLevel.Info, ); - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); // check if account exists and for matching code to see if claimed diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index da7cd15e9..63974518b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart'; @@ -104,6 +105,7 @@ class _MyWalletState extends ConsumerState { frostInterruptionDialogType: FrostInterruptionDialogType .transactionCreation, + callerRouteName: MyStackView.routeName, ); await Navigator.of(context).pushNamed( @@ -111,7 +113,7 @@ class _MyWalletState extends ConsumerState { ); }, ), - ) + ), ], ), FrostSendView( diff --git a/lib/widgets/dialogs/frost/frost_error_dialog.dart b/lib/widgets/dialogs/frost/frost_error_dialog.dart new file mode 100644 index 000000000..8b1e56e15 --- /dev/null +++ b/lib/widgets/dialogs/frost/frost_error_dialog.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostErrorDialog extends ConsumerWidget { + const FrostErrorDialog({ + super.key, + required this.title, + this.icon, + this.message, + }); + + final String title; + final Widget? icon; + final String? message; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SafeArea( + child: StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + title, + style: STextStyles.pageTitleH2(context), + ), + ), + icon != null ? icon! : Container(), + ], + ), + if (message != null) + const SizedBox( + height: 8, + ), + if (message != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message!, + style: STextStyles.smallMed14(context), + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Process must be restarted", + style: STextStyles.smallMed14(context), + ), + ], + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 8, + ), + PrimaryButton( + label: "Ok", + onPressed: () { + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostScaffoldArgs)!.parentNav.popUntil( + ModalRoute.withName( + ref.read(pFrostScaffoldArgs)!.callerRouteName, + ), + ); + }, + ), + ], + ), + ], + ), + ), + ); + } +} From 20438da655e694fdb92de6d391c25ee0a8d893fc Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 13 May 2024 10:10:34 -0600 Subject: [PATCH 269/272] all wallets sync changes for better ui performance --- .../syncing_options_view.dart | 37 ++-- .../wallet_syncing_options_view.dart | 46 +++-- lib/pages/wallet_view/wallet_view.dart | 16 +- .../wallet_view/desktop_wallet_view.dart | 18 +- lib/services/wallets.dart | 166 +++++++++++++++++- 5 files changed, 218 insertions(+), 65 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart index 4ff5da679..58144b681 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart @@ -11,7 +11,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; -import 'package:stackwallet/providers/global/active_wallet_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -95,12 +94,12 @@ class SyncingOptionsView extends ConsumerWidget { ref.read(prefsChangeNotifierProvider).syncType = SyncingType.currentWalletOnly; - // disable auto sync on all wallets that aren't active/current - ref.read(pWallets).wallets.forEach((e) { - if (e.walletId != ref.read(currentWalletIdProvider)) { - e.shouldAutoSync = false; - } - }); + // // disable auto sync on all wallets that aren't active/current + // ref.read(pWallets).wallets.forEach((e) { + // if (e.walletId != ref.read(currentWalletIdProvider)) { + // e.shouldAutoSync = false; + // } + // }); } }, child: Container( @@ -174,11 +173,11 @@ class SyncingOptionsView extends ConsumerWidget { ref.read(prefsChangeNotifierProvider).syncType = SyncingType.allWalletsOnStartup; - // enable auto sync on all wallets - ref - .read(pWallets) - .wallets - .forEach((e) => e.shouldAutoSync = true); + // // enable auto sync on all wallets + // ref + // .read(pWallets) + // .wallets + // .forEach((e) => e.shouldAutoSync = true); } }, child: Container( @@ -252,13 +251,13 @@ class SyncingOptionsView extends ConsumerWidget { ref.read(prefsChangeNotifierProvider).syncType = SyncingType.selectedWalletsAtStartup; - final ids = ref - .read(prefsChangeNotifierProvider) - .walletIdsSyncOnStartup; - - // enable auto sync on selected wallets only - ref.read(pWallets).wallets.forEach( - (e) => e.shouldAutoSync = ids.contains(e.walletId)); + // final ids = ref + // .read(prefsChangeNotifierProvider) + // .walletIdsSyncOnStartup; + // + // // enable auto sync on selected wallets only + // ref.read(pWallets).wallets.forEach( + // (e) => e.shouldAutoSync = ids.contains(e.walletId)); } }, child: Container( diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart index 824656496..8ded05dd6 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart @@ -13,13 +13,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/providers/global/active_wallet_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; @@ -181,9 +179,9 @@ class WalletSyncingOptionsView extends ConsumerWidget { .walletIdsSyncOnStartup)) .contains(info.walletId), onValueChanged: (value) { - final syncType = ref - .read(prefsChangeNotifierProvider) - .syncType; + // final syncType = ref + // .read(prefsChangeNotifierProvider) + // .syncType; final ids = ref .read(prefsChangeNotifierProvider) .walletIdsSyncOnStartup @@ -194,25 +192,25 @@ class WalletSyncingOptionsView extends ConsumerWidget { ids.remove(info.walletId); } - final wallet = ref - .read(pWallets) - .getWallet(info.walletId); - - switch (syncType) { - case SyncingType.currentWalletOnly: - if (info.walletId == - ref.read( - currentWalletIdProvider)) { - wallet.shouldAutoSync = value; - } - break; - case SyncingType - .selectedWalletsAtStartup: - case SyncingType - .allWalletsOnStartup: - wallet.shouldAutoSync = value; - break; - } + // final wallet = ref + // .read(pWallets) + // .getWallet(info.walletId); + // + // switch (syncType) { + // case SyncingType.currentWalletOnly: + // if (info.walletId == + // ref.read( + // currentWalletIdProvider)) { + // wallet.shouldAutoSync = value; + // } + // break; + // case SyncingType + // .selectedWalletsAtStartup: + // case SyncingType + // .allWalletsOnStartup: + // wallet.shouldAutoSync = value; + // break; + // } ref .read(prefsChangeNotifierProvider) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 57d404c0a..f00751425 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -124,7 +124,7 @@ class _WalletViewState extends ConsumerState { late final bool isSparkWallet; - late final bool _shouldDisableAutoSyncOnLogOut; + // late final bool _shouldDisableAutoSyncOnLogOut; late WalletSyncStatus _currentSyncStatus; late NodeConnectionStatus _currentNodeStatus; @@ -177,9 +177,9 @@ class _WalletViewState extends ConsumerState { 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; @@ -303,10 +303,10 @@ class _WalletViewState extends ConsumerState { } 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; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 70766661b..146f596d7 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -72,7 +72,7 @@ class _DesktopWalletViewState extends ConsumerState { late final TextEditingController controller; late final EventBus eventBus; - late final bool _shouldDisableAutoSyncOnLogOut; + // late final bool _shouldDisableAutoSyncOnLogOut; Future onBackPressed() async { await _logout(); @@ -83,10 +83,10 @@ class _DesktopWalletViewState extends ConsumerState { Future _logout() async { final wallet = ref.read(pWallets).getWallet(widget.walletId); - if (_shouldDisableAutoSyncOnLogOut) { - // disable auto sync if it was enabled only when loading wallet - wallet.shouldAutoSync = false; - } + // if (_shouldDisableAutoSyncOnLogOut) { + // // disable auto sync if it was enabled only when loading wallet + wallet.shouldAutoSync = false; + // } ref.read(transactionFilterProvider.state).state = null; if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && ref.read(prefsChangeNotifierProvider).backupFrequencyType == @@ -131,11 +131,11 @@ class _DesktopWalletViewState extends ConsumerState { ref.read(currentWalletIdProvider.notifier).state = wallet.walletId); if (!wallet.shouldAutoSync) { - // enable auto sync if it wasn't enabled when loading wallet + // // enable auto sync if it wasn't enabled when loading wallet wallet.shouldAutoSync = true; - _shouldDisableAutoSyncOnLogOut = true; - } else { - _shouldDisableAutoSyncOnLogOut = false; + // _shouldDisableAutoSyncOnLogOut = true; + // } else { + // _shouldDisableAutoSyncOnLogOut = false; } wallet.refresh(); diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 7dab60dbd..bba7bfe90 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -136,7 +136,8 @@ class Wallets { Future load(Prefs prefs, MainDB mainDB) async { // return await _loadV1(prefs, mainDB); - return await _loadV2(prefs, mainDB); + // return await _loadV2(prefs, mainDB); + return await _loadV3(prefs, mainDB); } Future _loadV1(Prefs prefs, MainDB mainDB) async { @@ -240,6 +241,7 @@ class Wallets { } } + /// should be fastest but big ui performance hit Future _loadV2(Prefs prefs, MainDB mainDB) async { if (hasLoaded) { return; @@ -358,6 +360,160 @@ class Wallets { await Future.wait(deleteFutures); } + /// should be best performance + Future _loadV3(Prefs prefs, MainDB mainDB) async { + if (hasLoaded) { + return; + } + hasLoaded = true; + + // clear out any wallet hive boxes where the wallet was deleted in previous app run + for (final walletId in DB.instance + .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { + await mainDB.isar.writeTxn(() async => await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll()); + } + // clear list + await DB.instance + .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + + final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); + if (walletInfoList.isEmpty) { + return; + } + + final List> walletIDInitFutures = []; + final List> deleteFutures = []; + final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = + []; + + final List walletIdsToSyncOnceOnStartup = []; + bool shouldSyncAllOnceOnStartup = false; + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + // do nothing as this will be set when going into a wallet from the main screen + break; + case SyncingType.selectedWalletsAtStartup: + walletIdsToSyncOnceOnStartup.addAll(prefs.walletIdsSyncOnStartup); + break; + case SyncingType.allWalletsOnStartup: + shouldSyncAllOnceOnStartup = true; + break; + } + + for (final walletInfo in walletInfoList) { + try { + final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); + Logging.instance.log( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + level: LogLevel.Info, + ); + + if (isVerified) { + // TODO: integrate this into the new wallets somehow? + // requires some thinking + // final txTracker = + // TransactionNotificationTracker(walletId: walletInfo.walletId); + + final walletIdCompleter = Completer(); + + walletIDInitFutures.add(walletIdCompleter.future); + + await Wallet.load( + walletId: walletInfo.walletId, + mainDB: mainDB, + secureStorageInterface: nodeService.secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + ).then((wallet) { + if (wallet is CwBasedInterface) { + // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); + + walletIdCompleter.complete("dummy_ignore"); + } else { + walletIdCompleter.complete(wallet.walletId); + } + + _wallets[wallet.walletId] = wallet; + }); + } else { + // wallet creation was not completed by user so we remove it completely + deleteFutures.add(_deleteWallet(walletInfo.walletId)); + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Fatal); + continue; + } + } + + final asyncWalletIds = await Future.wait(walletIDInitFutures); + asyncWalletIds.removeWhere((e) => e == "dummy_ignore"); + + final List idsToRefresh = []; + final List> walletInitFutures = asyncWalletIds + .map( + (id) => _wallets[id]!.init().then( + (_) { + if (shouldSyncAllOnceOnStartup || + walletIdsToSyncOnceOnStartup.contains(id)) { + idsToRefresh.add(id); + } + }, + ), + ) + .toList(); + + Future _refreshFutures(List idsToRefresh) async { + final start = DateTime.now(); + Logging.instance.log( + "Initial refresh start: ${start.toUtc()}", + level: LogLevel.Warning, + ); + const groupCount = 3; + for (int i = 0; i < idsToRefresh.length; i += groupCount) { + final List> futures = []; + for (int j = 0; j < groupCount; j++) { + if (i + j >= idsToRefresh.length) { + break; + } + futures.add( + _wallets[idsToRefresh[i + j]]!.refresh(), + ); + } + await Future.wait(futures); + } + Logging.instance.log( + "Initial refresh duration: ${DateTime.now().difference(start)}", + level: LogLevel.Warning, + ); + } + + if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { + unawaited( + Future.wait([ + _initLinearly(walletsToInitLinearly), + ...walletInitFutures, + ]).then( + (value) => _refreshFutures(idsToRefresh), + ), + ); + } else if (walletInitFutures.isNotEmpty) { + unawaited( + Future.wait(walletInitFutures).then( + (value) => _refreshFutures(idsToRefresh), + ), + ); + } else if (walletsToInitLinearly.isNotEmpty) { + unawaited(_initLinearly(walletsToInitLinearly)); + } + + // finally await any deletions that haven't completed yet + await Future.wait(deleteFutures); + } + Future loadAfterStackRestore( Prefs prefs, List wallets, @@ -396,10 +552,10 @@ class Wallets { if (wallet is CwBasedInterface) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { - walletInitFutures.add(wallet.init().then((value) { - if (shouldSetAutoSync) { - wallet.shouldAutoSync = true; - } + walletInitFutures.add(wallet.init().then((_) { + // if (shouldSetAutoSync) { + // wallet.shouldAutoSync = true; + // } })); } } From bc4d3d738181c4301a71eb26b3ddda999fed3d92 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 13 May 2024 11:01:18 -0600 Subject: [PATCH 270/272] ios project files --- ios/Podfile.lock | 7 +++---- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3bcf54939..fd6ae37e5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -117,8 +117,7 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter - - frostdart (0.0.1): - - Flutter + - frostdart (0.0.1) - integration_test (0.0.1): - Flutter - isar_flutter_libs (1.0.0): @@ -272,7 +271,7 @@ SPEC CHECKSUMS: flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - frostdart: ed3dc4e5dce431a1a8791dd7ddba472a05ea626d + frostdart: 4c72b69ccac2f13ede744107db046a125acce597 integration_test: 13825b8a9334a850581300559b8839134b124670 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 lelantus: 417f0221260013dfc052cae9cf4b741b6479edba @@ -293,4 +292,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3043c5ba8..7a1d5f01e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -193,7 +193,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a33..5e31d3d34 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Mon, 13 May 2024 13:03:13 -0600 Subject: [PATCH 271/272] ios disable select location to save swb and default to Docs dir --- .../create_auto_backup_view.dart | 8 ++++---- .../stack_backup_views/create_backup_view.dart | 8 ++++---- .../stack_backup_views/edit_auto_backup_view.dart | 8 ++++---- .../helpers/swb_file_system.dart | 15 ++++++++++----- .../backup_and_restore/create_auto_backup.dart | 8 ++++---- pubspec.yaml | 2 +- 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index 1e0c01609..546c780e9 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -88,7 +88,7 @@ class _EnableAutoBackupViewState extends ConsumerState { passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -151,11 +151,11 @@ class _EnableAutoBackupViewState extends ConsumerState { const SizedBox( height: 10, ), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid + onTap: Platform.isAndroid || Platform.isIOS ? null : () async { try { @@ -213,7 +213,7 @@ class _EnableAutoBackupViewState extends ConsumerState { ), onChanged: (newValue) {}, ), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) const SizedBox( height: 10, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 2c2af8ca7..4dfa049d1 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -80,7 +80,7 @@ class _RestoreFromFileViewState extends State { passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -179,14 +179,14 @@ class _RestoreFromFileViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) Consumer(builder: (context, ref, __) { return Container( color: Colors.transparent, child: TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid + onTap: Platform.isAndroid || Platform.isIOS ? null : () async { try { @@ -248,7 +248,7 @@ class _RestoreFromFileViewState extends State { ), ); }), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) SizedBox( height: !isDesktop ? 8 : 24, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 0838a25b3..f399c8266 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -260,7 +260,7 @@ class _EditAutoBackupViewState extends ConsumerState { passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -346,11 +346,11 @@ class _EditAutoBackupViewState extends ConsumerState { const SizedBox( height: 10, ), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid + onTap: Platform.isAndroid || Platform.isIOS ? null : () async { try { @@ -418,7 +418,7 @@ class _EditAutoBackupViewState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) const SizedBox( height: 10, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart index 50611e634..046e8642f 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart @@ -79,11 +79,16 @@ class SWBFileSystem { } Future pickDir(BuildContext context) async { - final String? path = await FilePicker.platform.getDirectoryPath( - dialogTitle: "Choose Backup location", - initialDirectory: startPath!.path, - lockParentWindow: true, - ); + final String? path; + if (Platform.isIOS) { + path = startPath?.path; + } else { + path = await FilePicker.platform.getDirectoryPath( + dialogTitle: "Choose Backup location", + initialDirectory: startPath!.path, + lockParentWindow: true, + ); + } dirPath = path; } diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart index 677366c09..2e27798bb 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart @@ -99,7 +99,7 @@ class _CreateAutoBackup extends ConsumerState { passphraseFocusNode = FocusNode(); passphraseRepeatFocusNode = FocusNode(); - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -174,14 +174,14 @@ class _CreateAutoBackup extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) Consumer(builder: (context, ref, __) { return Container( color: Colors.transparent, child: TextField( autocorrect: false, enableSuggestions: false, - onTap: Platform.isAndroid + onTap: Platform.isAndroid || Platform.isIOS ? null : () async { try { @@ -241,7 +241,7 @@ class _CreateAutoBackup extends ConsumerState { ), ); }), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) const SizedBox( height: 24, ), diff --git a/pubspec.yaml b/pubspec.yaml index fd2db4810..049590219 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -140,7 +140,7 @@ dependencies: pointycastle: ^3.6.0 package_info_plus: ^4.0.2 lottie: ^2.3.2 - file_picker: ^5.5.0 + file_picker: ^8.0.3 connectivity_plus: ^4.0.1 isar: 3.0.5 isar_flutter_libs: 3.0.5 # contains the binaries From 33c78a4b4296a0482cfcf6ab0ff672d57701321c Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Mon, 13 May 2024 13:15:10 -0600 Subject: [PATCH 272/272] Update version (v2.0.0, build 222) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 049590219..a3fdcdf9d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.0.0+221 +version: 2.0.0+222 environment: sdk: ">=3.3.4 <4.0.0"