diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 702940c60..e46a16c6c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -25,14 +25,6 @@ jobs: 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 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/ - ./build_all.sh - - name: Build Monero - run: | - cd crypto_plugins/flutter_libmonero/scripts/linux/ - ./build_monero_all.sh - name: Build Epic Cash run: | cd crypto_plugins/flutter_libepiccash/scripts/linux/ diff --git a/.gitignore b/.gitignore index b61c79144..30fb5a6e1 100644 --- a/.gitignore +++ b/.gitignore @@ -63,7 +63,6 @@ scripts/**/build /lib/external_api_keys.dart libepic_cash_wallet.dll -libmobileliblelantus.dll libtor_ffi.dll flutter_libsparkmobile.dll secp256k1.dll @@ -112,3 +111,4 @@ scripts/linux/build/libsecret/subprojects/gi-docgen/.meson-subproject-wrap-hash. crypto_plugins/cs_monero/built_outputs crypto_plugins/cs_monero/build crypto_plugins/*.diff +/devtools_options.yaml diff --git a/.gitmodules b/.gitmodules index 2186826df..d9a561cc1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "crypto_plugins/flutter_libepiccash"] path = crypto_plugins/flutter_libepiccash url = https://github.com/cypherstack/flutter_libepiccash.git -[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://github.com/cypherstack/frostdart diff --git a/analysis_options.yaml b/analysis_options.yaml index ea46ed3ca..db030aa14 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -92,7 +92,7 @@ linter: constant_identifier_names: false prefer_final_locals: true prefer_final_in_for_each: true - require_trailing_commas: true +# require_trailing_commas: true // causes issues with dart 3.7 # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule diff --git a/asset_sources/default_themes/stack_duo/dark.zip b/asset_sources/default_themes/stack_duo/dark.zip index 8b31f4278..a44c89cf8 100644 Binary files a/asset_sources/default_themes/stack_duo/dark.zip and b/asset_sources/default_themes/stack_duo/dark.zip differ diff --git a/asset_sources/default_themes/stack_duo/light.zip b/asset_sources/default_themes/stack_duo/light.zip index 120573ccf..82dc05870 100644 Binary files a/asset_sources/default_themes/stack_duo/light.zip and b/asset_sources/default_themes/stack_duo/light.zip differ diff --git a/asset_sources/default_themes/stack_wallet/.gitignore b/asset_sources/default_themes/stack_wallet/.gitignore new file mode 100644 index 000000000..49f4a6294 --- /dev/null +++ b/asset_sources/default_themes/stack_wallet/.gitignore @@ -0,0 +1,2 @@ +light/ +dark/ diff --git a/asset_sources/default_themes/stack_wallet/dark.zip b/asset_sources/default_themes/stack_wallet/dark.zip index 8b31f4278..a44c89cf8 100644 Binary files a/asset_sources/default_themes/stack_wallet/dark.zip and b/asset_sources/default_themes/stack_wallet/dark.zip differ diff --git a/asset_sources/default_themes/stack_wallet/light.zip b/asset_sources/default_themes/stack_wallet/light.zip index 120573ccf..82dc05870 100644 Binary files a/asset_sources/default_themes/stack_wallet/light.zip and b/asset_sources/default_themes/stack_wallet/light.zip differ diff --git a/asset_sources/svg/campfire/socials/twitter-brands.svg b/asset_sources/svg/campfire/socials/twitter-brands.svg deleted file mode 100644 index 96464c99f..000000000 --- a/asset_sources/svg/campfire/socials/twitter-brands.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/asset_sources/svg/campfire/socials/x.svg b/asset_sources/svg/campfire/socials/x.svg new file mode 100644 index 000000000..114491669 --- /dev/null +++ b/asset_sources/svg/campfire/socials/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset_sources/svg/stack_duo/socials/twitter-brands.svg b/asset_sources/svg/stack_duo/socials/twitter-brands.svg deleted file mode 100644 index 96464c99f..000000000 --- a/asset_sources/svg/stack_duo/socials/twitter-brands.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/asset_sources/svg/stack_duo/socials/x.svg b/asset_sources/svg/stack_duo/socials/x.svg new file mode 100644 index 000000000..114491669 --- /dev/null +++ b/asset_sources/svg/stack_duo/socials/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset_sources/svg/stack_wallet/socials/twitter-brands.svg b/asset_sources/svg/stack_wallet/socials/twitter-brands.svg deleted file mode 100644 index 96464c99f..000000000 --- a/asset_sources/svg/stack_wallet/socials/twitter-brands.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/asset_sources/svg/stack_wallet/socials/x.svg b/asset_sources/svg/stack_wallet/socials/x.svg new file mode 100644 index 000000000..114491669 --- /dev/null +++ b/asset_sources/svg/stack_wallet/socials/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus deleted file mode 160000 index 7b325030b..000000000 --- a/crypto_plugins/flutter_liblelantus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7b325030bce46a423aa46497d1a608b7a8a58976 diff --git a/crypto_plugins/frostdart b/crypto_plugins/frostdart index 6f1310ecc..5e8a51592 160000 --- a/crypto_plugins/frostdart +++ b/crypto_plugins/frostdart @@ -1 +1 @@ -Subproject commit 6f1310eccd336fb3c8dc00b61e39a3f0f3a2b59a +Subproject commit 5e8a51592690650e7ac63fafa81017dfc51ae6d5 diff --git a/docs/building.md b/docs/building.md index 4ba1b7150..6aa647e53 100644 --- a/docs/building.md +++ b/docs/building.md @@ -7,25 +7,12 @@ Here you will find instructions on how to install the necessary tools for buildi - 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 +- Install go: [https://go.dev/doc/install](https://go.dev/doc/install) ## 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.29.2 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.29.2` 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.29.2 -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: ``` @@ -58,7 +45,7 @@ sudo apt-get install libssl-dev curl unzip automake build-essential file pkg-con For Ubuntu 20.04, ``` -sudo apt-get install valac +sudo apt-get install valac python3-pip pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1 ``` @@ -89,6 +76,20 @@ sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1 ``` +### Flutter +Install Flutter 3.29.2 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.29.2` 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.29.2 +echo 'export PATH="$PATH:'"$FLUTTER_DIR"'/bin"' >> "$HOME/.profile" +source "$HOME/.profile" +flutter precache +``` + +Run `flutter doctor` in a terminal to confirm its installation. + ### Clone the repository and initialize submodules After installing the prerequisites listed above, download the code and init the submodules ``` @@ -163,6 +164,8 @@ cd scripts/windows ./deps.sh ``` +install go in WSL [https://go.dev/doc/install](https://go.dev/doc/install) (follow linux instructions) and ensure you have `x86_64-w64-mingw32-gcc` + and use `scripts/build_app.sh` to build plugins: ``` cd .. @@ -283,7 +286,6 @@ The WSL2 host may optionally be navigated to the `stack_wallet` repository on th If the DLLs were built on the WSL filesystem instead of on Windows, copy the resulting `dll`s to their respective positions on the Windows host: - `stack_wallet/crypto_plugins/flutter_libepiccash/scripts/windows/build/libepic_cash_wallet.dll` -- `stack_wallet/crypto_plugins/flutter_liblelantus/scripts/windows/build/libmobileliblelantus.dll` diff --git a/ios/Podfile.lock b/ios/Podfile.lock index add8f4224..3f6bee8e7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,6 +11,8 @@ PODS: - ReachabilitySwift - cs_monero_flutter_libs_ios (0.0.1): - Flutter + - cs_salvium_flutter_libs_ios (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter - devicelocale (0.0.1): @@ -65,8 +67,6 @@ PODS: - Flutter - isar_flutter_libs (1.0.0): - Flutter - - lelantus (0.0.1): - - Flutter - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -87,6 +87,8 @@ PODS: - "sqlite3 (3.46.0+1)": - "sqlite3/common (= 3.46.0+1)" - "sqlite3/common (3.46.0+1)" + - "sqlite3/dbstatvtab (3.46.0+1)": + - sqlite3/common - "sqlite3/fts5 (3.46.0+1)": - sqlite3/common - "sqlite3/perf-threadsafe (3.46.0+1)": @@ -95,7 +97,8 @@ PODS: - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - sqlite3 (~> 3.46.0) + - "sqlite3 (~> 3.46.0+1)" + - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree @@ -117,6 +120,7 @@ DEPENDENCIES: - coinlib_flutter (from `.symlinks/plugins/coinlib_flutter/darwin`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - cs_monero_flutter_libs_ios (from `.symlinks/plugins/cs_monero_flutter_libs_ios/ios`) + - cs_salvium_flutter_libs_ios (from `.symlinks/plugins/cs_salvium_flutter_libs_ios/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -129,7 +133,6 @@ DEPENDENCIES: - 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`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -162,6 +165,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/connectivity_plus/ios" cs_monero_flutter_libs_ios: :path: ".symlinks/plugins/cs_monero_flutter_libs_ios/ios" + cs_salvium_flutter_libs_ios: + :path: ".symlinks/plugins/cs_salvium_flutter_libs_ios/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" devicelocale: @@ -186,8 +191,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" isar_flutter_libs: :path: ".symlinks/plugins/isar_flutter_libs/ios" - lelantus: - :path: ".symlinks/plugins/lelantus/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" package_info_plus: @@ -216,6 +219,7 @@ SPEC CHECKSUMS: coinlib_flutter: 9275e8255ef67d3da33beb6e117d09ced4f46eb5 connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a cs_monero_flutter_libs_ios: fd353631682247f72a36493ff060d4328d6f720d + cs_salvium_flutter_libs_ios: f9d6ce540cb34d8cb8641822cf02fa0695a8d405 device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac @@ -230,7 +234,6 @@ SPEC CHECKSUMS: frostdart: 4c72b69ccac2f13ede744107db046a125acce597 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097 - lelantus: 417f0221260013dfc052cae9cf4b741b6479edba local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 @@ -240,13 +243,14 @@ SPEC CHECKSUMS: SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 - sqlite3_flutter_libs: 0d611efdf6d1c9297d5ab03dab21b75aeebdae31 + sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b stack_wallet_backup: 5b8563aba5d8ffbf2ce1944331ff7294a0ec7c03 SwiftProtobuf: 6ef3f0e422ef90d6605ca20b21a94f6c1324d6b3 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 tor_ffi_plugin: d80e291b649379c8176e1be739e49be007d4ef93 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 + wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 + xelis_flutter: a6a1ee1f1e47f5aeb42dc4a5889358b79d8d90fc PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb diff --git a/lib/app_config.dart b/lib/app_config.dart index 62ce560c3..09eeae26f 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -3,11 +3,7 @@ import 'wallets/crypto_currency/intermediate/frost_currency.dart'; part 'app_config.g.dart'; -enum AppFeature { - themeSelection, - buy, - swap; -} +enum AppFeature { themeSelection, buy, swap } abstract class AppConfig { static const appName = _prefix + _separator + suffix; @@ -27,7 +23,8 @@ abstract class AppConfig { static List get coins => _supportedCoins; - static ({String from, String to}) get swapDefaults => _swapDefaults; + static ({String from, String fromFuzzyNet, String to, String toFuzzyNet}) + get swapDefaults => _swapDefaults; static bool get isSingleCoinApp => coins.length == 1; @@ -75,7 +72,13 @@ abstract class AppConfig { /// Fuzzy logic. Use with caution!! @Deprecated("dangerous") static CryptoCurrency getCryptoCurrencyByPrettyName(final String prettyName) { - final name = prettyName.replaceAll(" ", "").toLowerCase(); + // trocador hack + const hackSplitter = " (Mainnet"; + final name = + prettyName.contains(hackSplitter) + ? prettyName.split(hackSplitter).first.toLowerCase() + : prettyName.replaceAll(" ", "").toLowerCase(); + try { return coins.firstWhere( (e) => e.identifier.toLowerCase() == name || e.prettyName == prettyName, diff --git a/lib/db/db_version_migration.dart b/lib/db/db_version_migration.dart index e18c4b720..9c278ff19 100644 --- a/lib/db/db_version_migration.dart +++ b/lib/db/db_version_migration.dart @@ -12,7 +12,6 @@ import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; import '../app_config.dart'; -import '../electrumx_rpc/electrumx_client.dart'; import '../models/contact.dart'; import '../models/exchange/change_now/exchange_transaction.dart'; import '../models/exchange/response_objects/trade.dart'; @@ -22,7 +21,6 @@ import '../models/isar/models/isar_models.dart' as isar_models; import '../models/models.dart'; import '../models/node_model.dart'; import '../services/mixins/wallet_db.dart'; -import '../services/node_service.dart'; import '../services/wallets_service.dart'; import '../utilities/amount/amount.dart'; import '../utilities/constants.dart'; @@ -50,107 +48,11 @@ class DbVersionMigrator with WalletDB { case 0: await DB.instance.hive.openBox(DB.boxNameAllWalletsData); await DB.instance.hive.openBox(DB.boxNamePrefs); - final walletsService = WalletsService(); - final nodeService = NodeService(secureStorageInterface: secureStore); final prefs = Prefs.instance; - final walletInfoList = await walletsService.walletNames; await prefs.init(); - ElectrumXClient? client; - int? latestSetId; - - final firo = Firo(CryptoCurrencyNetwork.main); - // only instantiate client if there are firo wallets - if (walletInfoList.values - .any((element) => element.coinIdentifier == firo.identifier)) { - await DB.instance.hive.openBox(DB.boxNameNodeModels); - await DB.instance.hive.openBox(DB.boxNamePrimaryNodes); - final node = - nodeService.getPrimaryNodeFor(currency: firo) ?? firo.defaultNode; - final List failovers = nodeService - .failoverNodesFor(currency: firo) - .map( - (e) => ElectrumXNode( - address: e.host, - port: e.port, - name: e.name, - id: e.id, - useSSL: e.useSSL, - torEnabled: e.torEnabled, - clearnetEnabled: e.clearnetEnabled, - ), - ) - .toList(); - - client = ElectrumXClient.from( - node: ElectrumXNode( - address: node.host, - port: node.port, - name: node.name, - id: node.id, - useSSL: node.useSSL, - torEnabled: node.torEnabled, - clearnetEnabled: node.clearnetEnabled, - ), - prefs: prefs, - failovers: failovers, - cryptoCurrency: Firo(CryptoCurrencyNetwork.main), - ); - - try { - latestSetId = await client.getLelantusLatestCoinId(); - } catch (e, s) { - // default to 2 for now - latestSetId = 2; - Logging.instance.w( - "Failed to fetch latest coin id during firo db migrate: $e \nUsing a default value of 2", - error: e, - stackTrace: s, - ); - } - } - - for (final walletInfo in walletInfoList.values) { - // migrate each firo wallet's lelantus coins - if (walletInfo.coinIdentifier == firo.identifier) { - await DB.instance.hive.openBox(walletInfo.walletId); - final _lelantusCoins = DB.instance.get( - boxName: walletInfo.walletId, - key: '_lelantus_coins', - ) as List?; - final List> lelantusCoins = []; - for (final lCoin in _lelantusCoins ?? []) { - lelantusCoins - .add({lCoin.keys.first: lCoin.values.first as LelantusCoin}); - } - - final List> coins = []; - for (final element in lelantusCoins) { - final LelantusCoin coin = element.values.first; - int anonSetId = coin.anonymitySetId; - if (coin.anonymitySetId == 1 && - (coin.publicCoin == '' || - coin.publicCoin == "jmintData.publicCoin")) { - anonSetId = latestSetId!; - } - coins.add({ - element.keys.first: LelantusCoin( - coin.index, - coin.value, - coin.publicCoin, - coin.txId, - anonSetId, - coin.isUsed, - ), - }); - } - await DB.instance.put( - boxName: walletInfo.walletId, - key: '_lelantus_coins', - value: coins, - ); - } - } + // nothing happens here anymore. + // Kept for legacy reasons after removing old lelantus // update version await DB.instance.put( @@ -165,8 +67,9 @@ class DbVersionMigrator with WalletDB { case 1: await DB.instance.hive.openBox(DB.boxNameTrades); await DB.instance.hive.openBox(DB.boxNameTradesV2); - final trades = - DB.instance.values(boxName: DB.boxNameTrades); + final trades = DB.instance.values( + boxName: DB.boxNameTrades, + ); for (final old in trades) { if (old.statusObject != null) { @@ -208,9 +111,7 @@ class DbVersionMigrator with WalletDB { case 3: // clear possible broken firo cache await DB.instance.clearSharedTransactionCache( - currency: Firo( - CryptoCurrencyNetwork.test, - ), + currency: Firo(CryptoCurrencyNetwork.test), ); // update version @@ -244,8 +145,8 @@ class DbVersionMigrator with WalletDB { final themeName = DB.instance.get(boxName: "theme", key: "colorScheme") - as String? ?? - "light"; + as String? ?? + "light"; await DB.instance.put( boxName: DB.boxNamePrefs, @@ -269,11 +170,12 @@ class DbVersionMigrator with WalletDB { final count = await MainDB.instance.isar.addresses.count(); // add change/receiving tags to address labels for (var i = 0; i < count; i += 50) { - final addresses = await MainDB.instance.isar.addresses - .where() - .offset(i) - .limit(50) - .findAll(); + final addresses = + await MainDB.instance.isar.addresses + .where() + .offset(i) + .limit(50) + .findAll(); final List labels = []; for (final address in addresses) { @@ -301,11 +203,14 @@ class DbVersionMigrator with WalletDB { // update/create label if tags is not empty if (tags != null) { - isar_models.AddressLabel? label = await MainDB - .instance.isar.addressLabels - .where() - .addressStringWalletIdEqualTo(address.value, address.walletId) - .findFirst(); + isar_models.AddressLabel? label = + await MainDB.instance.isar.addressLabels + .where() + .addressStringWalletIdEqualTo( + address.value, + address.walletId, + ) + .findFirst(); if (label == null) { label = isar_models.AddressLabel( walletId: address.walletId, @@ -363,12 +268,13 @@ class DbVersionMigrator with WalletDB { Bitcoincash(CryptoCurrencyNetwork.main).identifier || info.coinIdentifier == Bitcoincash(CryptoCurrencyNetwork.test).identifier) { - final ids = await MainDB.instance - .getAddresses(walletId) - .filter() - .typeEqualTo(isar_models.AddressType.p2sh) - .idProperty() - .findAll(); + final ids = + await MainDB.instance + .getAddresses(walletId) + .filter() + .typeEqualTo(isar_models.AddressType.p2sh) + .idProperty() + .findAll(); await MainDB.instance.isar.writeTxn(() async { await MainDB.instance.isar.addresses.deleteAll(ids); @@ -456,6 +362,20 @@ class DbVersionMigrator with WalletDB { // try to continue migrating return await migrate(14, secureStore: secureStore); + case 14: + // migrate + await _v14(); + + // update version + await DB.instance.put( + boxName: DB.boxNameDBInfo, + key: "hive_data_version", + value: 15, + ); + + // try to continue migrating + return await migrate(15, secureStore: secureStore); + default: // finally return return; @@ -490,7 +410,7 @@ class DbVersionMigrator with WalletDB { const rcvIndex = 0; final List> - transactionsData = []; + transactionsData = []; if (txnData != null) { final txns = txnData.getAllTransactions(); @@ -501,15 +421,17 @@ class DbVersionMigrator with WalletDB { walletId: walletId, txid: tx.txid, timestamp: tx.timestamp, - type: isIncoming - ? isar_models.TransactionType.incoming - : isar_models.TransactionType.outgoing, + type: + isIncoming + ? isar_models.TransactionType.incoming + : isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: tx.amount, - amountString: Amount( - rawValue: BigInt.from(tx.amount), - fractionDigits: epic.fractionDigits, - ).toJsonString(), + amountString: + Amount( + rawValue: BigInt.from(tx.amount), + fractionDigits: epic.fractionDigits, + ).toJsonString(), fee: tx.fees, height: tx.height, isCancelled: tx.isCancelled, @@ -531,12 +453,14 @@ class DbVersionMigrator with WalletDB { publicKey: [], derivationIndex: isIncoming ? rcvIndex : -1, derivationPath: null, - type: isIncoming - ? isar_models.AddressType.mimbleWimble - : isar_models.AddressType.unknown, - subType: isIncoming - ? isar_models.AddressSubType.receiving - : isar_models.AddressSubType.unknown, + type: + isIncoming + ? isar_models.AddressType.mimbleWimble + : isar_models.AddressType.unknown, + subType: + isIncoming + ? isar_models.AddressSubType.receiving + : isar_models.AddressSubType.unknown, ); transactionsData.add(Tuple2(iTx, address)); } @@ -594,25 +518,28 @@ class DbVersionMigrator with WalletDB { final crypto = AppConfig.getCryptoCurrencyFor(info.coinIdentifier)!; for (var i = 0; i < count; i += 50) { - final txns = await MainDB.instance - .getTransactions(walletId) - .offset(i) - .limit(50) - .findAll(); + final txns = + await MainDB.instance + .getTransactions(walletId) + .offset(i) + .limit(50) + .findAll(); // migrate amount to serialized amount string - final txnsData = txns - .map( - (tx) => Tuple2( - tx - ..amountString = Amount( - rawValue: BigInt.from(tx.amount), - fractionDigits: crypto.fractionDigits, - ).toJsonString(), - tx.address.value, - ), - ) - .toList(); + final txnsData = + txns + .map( + (tx) => Tuple2( + tx + ..amountString = + Amount( + rawValue: BigInt.from(tx.amount), + fractionDigits: crypto.fractionDigits, + ).toJsonString(), + tx.address.value, + ), + ) + .toList(); // update db records await MainDB.instance.addNewTransactionData(txnsData, walletId); @@ -621,17 +548,16 @@ class DbVersionMigrator with WalletDB { } Future _v9() async { - final addressBookBox = - await DB.instance.hive.openBox(DB.boxNameAddressBook); + final addressBookBox = await DB.instance.hive.openBox( + DB.boxNameAddressBook, + ); await MainDB.instance.initMainDB(); final keys = List.from(addressBookBox.keys); final contacts = keys .map( (id) => Contact.fromJson( - Map.from( - addressBookBox.get(id) as Map, - ), + Map.from(addressBookBox.get(id) as Map), ), ) .toList(growable: false); @@ -672,65 +598,12 @@ class DbVersionMigrator with WalletDB { Future _v10(SecureStorageInterface secureStore) async { await DB.instance.hive.openBox(DB.boxNameAllWalletsData); await DB.instance.hive.openBox(DB.boxNamePrefs); - final walletsService = WalletsService(); final prefs = Prefs.instance; - final walletInfoList = await walletsService.walletNames; await prefs.init(); await MainDB.instance.initMainDB(); - final firo = Firo(CryptoCurrencyNetwork.main); - - for (final walletId in walletInfoList.keys) { - final info = walletInfoList[walletId]!; - assert(info.walletId == walletId); - - if (info.coinIdentifier == firo.identifier && - MainDB.instance.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .countSync() == - 0) { - final walletBox = await DB.instance.hive.openBox(walletId); - - final hiveLCoins = DB.instance.get( - boxName: walletId, - key: "_lelantus_coins", - ) as List? ?? - []; - - final jindexes = (DB.instance - .get(boxName: walletId, key: "jindex") as List? ?? - []) - .cast(); - - final List coins = []; - for (final e in hiveLCoins) { - final map = e as Map; - final lcoin = map.values.first as LelantusCoin; - - final isJMint = jindexes.contains(lcoin.index); - - final coin = isar_models.LelantusCoin( - walletId: walletId, - txid: lcoin.txId, - value: lcoin.value.toString(), - mintIndex: lcoin.index, - anonymitySetId: lcoin.anonymitySetId, - isUsed: lcoin.isUsed, - isJMint: isJMint, - otherData: null, - ); - - coins.add(coin); - } - - if (coins.isNotEmpty) { - await MainDB.instance.isar.writeTxn(() async { - await MainDB.instance.isar.lelantusCoins.putAll(coins); - }); - } - } - } + // nothing happens here anymore. + // Kept for legacy reasons after removing old lelantus } Future _v11(SecureStorageInterface secureStore) async { @@ -774,4 +647,28 @@ class DbVersionMigrator with WalletDB { await isar.close(deleteFromDisk: true); } } + + Future _v14() async { + final nodesBox = await DB.instance.hive.openBox( + DB.boxNameNodeModels, + ); + final primariesBox = await DB.instance.hive.openBox( + DB.boxNamePrimaryNodesDeprecated, + ); + + final primaries = primariesBox.values; + + for (final node in primaries) { + await nodesBox.put( + node.id, + node.copyWith( + loginName: node.loginName, + trusted: node.trusted, + isPrimary: true, + ), + ); + } + + await primariesBox.deleteFromDisk(); + } } diff --git a/lib/db/drift/database.dart b/lib/db/drift/database.dart new file mode 100644 index 000000000..4bfd94341 --- /dev/null +++ b/lib/db/drift/database.dart @@ -0,0 +1,131 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-05-06 + * + */ + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:path/path.dart' as path; + +import '../../utilities/stack_file_system.dart'; + +part 'database.g.dart'; + +abstract final class Drift { + static bool _didInit = false; + + static final Map _map = {}; + + static WalletDatabase get(String walletId) { + if (!_didInit) { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + _didInit = true; + } + + return _map[walletId] ??= WalletDatabase._(walletId); + } +} + +class SparkNames extends Table { + TextColumn get name => + text().customConstraint("UNIQUE NOT NULL COLLATE NOCASE")(); + TextColumn get address => text()(); + IntColumn get validUntil => integer()(); + TextColumn get additionalInfo => text().nullable()(); + + @override + Set get primaryKey => {name}; +} + +class MwebUtxos extends Table { + TextColumn get outputId => text()(); + TextColumn get address => text()(); + IntColumn get value => integer()(); + IntColumn get height => integer()(); + IntColumn get blockTime => integer()(); + BoolColumn get blocked => boolean()(); + BoolColumn get used => boolean()(); + + @override + Set get primaryKey => {outputId}; +} + +extension MwebUtxoExt on MwebUtxo { + int getConfirmations(int currentChainHeight) { + if (blockTime <= 0) return 0; + if (height <= 0) return 0; + return math.max(0, currentChainHeight - (height - 1)); + } + + bool isConfirmed( + int currentChainHeight, + int minimumConfirms, { + int? overrideMinConfirms, + }) { + final confirmations = getConfirmations(currentChainHeight); + + if (overrideMinConfirms != null) { + return confirmations >= overrideMinConfirms; + } + return confirmations >= minimumConfirms; + } +} + +@DriftDatabase(tables: [SparkNames, MwebUtxos]) +final class WalletDatabase extends _$WalletDatabase { + WalletDatabase._(String walletId, [QueryExecutor? executor]) + : super(executor ?? _openConnection(walletId)); + + @override + int get schemaVersion => 2; + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + if (from == 1 && to == 2) { + await m.createTable(mwebUtxos); + } + }, + ); + + static QueryExecutor _openConnection(String walletId) { + return driftDatabase( + name: walletId, + native: DriftNativeOptions( + shareAcrossIsolates: true, + databasePath: () async { + final dir = await StackFileSystem.applicationDriftDirectory(); + return path.join(dir.path, "wallets", walletId, "$walletId.db"); + }, + ), + ); + } + + Future upsertSparkNames( + List< + ({String name, String address, int validUntil, String? additionalInfo}) + > + names, + ) async { + await transaction(() async { + for (final name in names) { + await into(sparkNames).insertOnConflictUpdate( + SparkNamesCompanion( + name: Value(name.name), + address: Value(name.address), + validUntil: Value(name.validUntil), + additionalInfo: Value(name.additionalInfo), + ), + ); + } + }); + } +} diff --git a/lib/db/drift/database.g.dart b/lib/db/drift/database.g.dart new file mode 100644 index 000000000..67660d3a6 --- /dev/null +++ b/lib/db/drift/database.g.dart @@ -0,0 +1,1046 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $SparkNamesTable extends SparkNames + with TableInfo<$SparkNamesTable, SparkName> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SparkNamesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'UNIQUE NOT NULL COLLATE NOCASE'); + static const VerificationMeta _addressMeta = + const VerificationMeta('address'); + @override + late final GeneratedColumn address = GeneratedColumn( + 'address', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _validUntilMeta = + const VerificationMeta('validUntil'); + @override + late final GeneratedColumn validUntil = GeneratedColumn( + 'valid_until', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _additionalInfoMeta = + const VerificationMeta('additionalInfo'); + @override + late final GeneratedColumn additionalInfo = GeneratedColumn( + 'additional_info', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => + [name, address, validUntil, additionalInfo]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'spark_names'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('address')) { + context.handle(_addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta)); + } else if (isInserting) { + context.missing(_addressMeta); + } + if (data.containsKey('valid_until')) { + context.handle( + _validUntilMeta, + validUntil.isAcceptableOrUnknown( + data['valid_until']!, _validUntilMeta)); + } else if (isInserting) { + context.missing(_validUntilMeta); + } + if (data.containsKey('additional_info')) { + context.handle( + _additionalInfoMeta, + additionalInfo.isAcceptableOrUnknown( + data['additional_info']!, _additionalInfoMeta)); + } + return context; + } + + @override + Set get $primaryKey => {name}; + @override + SparkName map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SparkName( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + address: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}address'])!, + validUntil: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}valid_until'])!, + additionalInfo: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}additional_info']), + ); + } + + @override + $SparkNamesTable createAlias(String alias) { + return $SparkNamesTable(attachedDatabase, alias); + } +} + +class SparkName extends DataClass implements Insertable { + final String name; + final String address; + final int validUntil; + final String? additionalInfo; + const SparkName( + {required this.name, + required this.address, + required this.validUntil, + this.additionalInfo}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['address'] = Variable(address); + map['valid_until'] = Variable(validUntil); + if (!nullToAbsent || additionalInfo != null) { + map['additional_info'] = Variable(additionalInfo); + } + return map; + } + + SparkNamesCompanion toCompanion(bool nullToAbsent) { + return SparkNamesCompanion( + name: Value(name), + address: Value(address), + validUntil: Value(validUntil), + additionalInfo: additionalInfo == null && nullToAbsent + ? const Value.absent() + : Value(additionalInfo), + ); + } + + factory SparkName.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SparkName( + name: serializer.fromJson(json['name']), + address: serializer.fromJson(json['address']), + validUntil: serializer.fromJson(json['validUntil']), + additionalInfo: serializer.fromJson(json['additionalInfo']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'address': serializer.toJson(address), + 'validUntil': serializer.toJson(validUntil), + 'additionalInfo': serializer.toJson(additionalInfo), + }; + } + + SparkName copyWith( + {String? name, + String? address, + int? validUntil, + Value additionalInfo = const Value.absent()}) => + SparkName( + name: name ?? this.name, + address: address ?? this.address, + validUntil: validUntil ?? this.validUntil, + additionalInfo: + additionalInfo.present ? additionalInfo.value : this.additionalInfo, + ); + SparkName copyWithCompanion(SparkNamesCompanion data) { + return SparkName( + name: data.name.present ? data.name.value : this.name, + address: data.address.present ? data.address.value : this.address, + validUntil: + data.validUntil.present ? data.validUntil.value : this.validUntil, + additionalInfo: data.additionalInfo.present + ? data.additionalInfo.value + : this.additionalInfo, + ); + } + + @override + String toString() { + return (StringBuffer('SparkName(') + ..write('name: $name, ') + ..write('address: $address, ') + ..write('validUntil: $validUntil, ') + ..write('additionalInfo: $additionalInfo') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, address, validUntil, additionalInfo); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SparkName && + other.name == this.name && + other.address == this.address && + other.validUntil == this.validUntil && + other.additionalInfo == this.additionalInfo); +} + +class SparkNamesCompanion extends UpdateCompanion { + final Value name; + final Value address; + final Value validUntil; + final Value additionalInfo; + final Value rowid; + const SparkNamesCompanion({ + this.name = const Value.absent(), + this.address = const Value.absent(), + this.validUntil = const Value.absent(), + this.additionalInfo = const Value.absent(), + this.rowid = const Value.absent(), + }); + SparkNamesCompanion.insert({ + required String name, + required String address, + required int validUntil, + this.additionalInfo = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name), + address = Value(address), + validUntil = Value(validUntil); + static Insertable custom({ + Expression? name, + Expression? address, + Expression? validUntil, + Expression? additionalInfo, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (address != null) 'address': address, + if (validUntil != null) 'valid_until': validUntil, + if (additionalInfo != null) 'additional_info': additionalInfo, + if (rowid != null) 'rowid': rowid, + }); + } + + SparkNamesCompanion copyWith( + {Value? name, + Value? address, + Value? validUntil, + Value? additionalInfo, + Value? rowid}) { + return SparkNamesCompanion( + name: name ?? this.name, + address: address ?? this.address, + validUntil: validUntil ?? this.validUntil, + additionalInfo: additionalInfo ?? this.additionalInfo, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (address.present) { + map['address'] = Variable(address.value); + } + if (validUntil.present) { + map['valid_until'] = Variable(validUntil.value); + } + if (additionalInfo.present) { + map['additional_info'] = Variable(additionalInfo.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SparkNamesCompanion(') + ..write('name: $name, ') + ..write('address: $address, ') + ..write('validUntil: $validUntil, ') + ..write('additionalInfo: $additionalInfo, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $MwebUtxosTable extends MwebUtxos + with TableInfo<$MwebUtxosTable, MwebUtxo> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MwebUtxosTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _outputIdMeta = + const VerificationMeta('outputId'); + @override + late final GeneratedColumn outputId = GeneratedColumn( + 'output_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _addressMeta = + const VerificationMeta('address'); + @override + late final GeneratedColumn address = GeneratedColumn( + 'address', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _valueMeta = const VerificationMeta('value'); + @override + late final GeneratedColumn value = GeneratedColumn( + 'value', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _heightMeta = const VerificationMeta('height'); + @override + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _blockTimeMeta = + const VerificationMeta('blockTime'); + @override + late final GeneratedColumn blockTime = GeneratedColumn( + 'block_time', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _blockedMeta = + const VerificationMeta('blocked'); + @override + late final GeneratedColumn blocked = GeneratedColumn( + 'blocked', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))')); + static const VerificationMeta _usedMeta = const VerificationMeta('used'); + @override + late final GeneratedColumn used = GeneratedColumn( + 'used', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("used" IN (0, 1))')); + @override + List get $columns => + [outputId, address, value, height, blockTime, blocked, used]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'mweb_utxos'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('output_id')) { + context.handle(_outputIdMeta, + outputId.isAcceptableOrUnknown(data['output_id']!, _outputIdMeta)); + } else if (isInserting) { + context.missing(_outputIdMeta); + } + if (data.containsKey('address')) { + context.handle(_addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta)); + } else if (isInserting) { + context.missing(_addressMeta); + } + if (data.containsKey('value')) { + context.handle( + _valueMeta, value.isAcceptableOrUnknown(data['value']!, _valueMeta)); + } else if (isInserting) { + context.missing(_valueMeta); + } + if (data.containsKey('height')) { + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + } else if (isInserting) { + context.missing(_heightMeta); + } + if (data.containsKey('block_time')) { + context.handle(_blockTimeMeta, + blockTime.isAcceptableOrUnknown(data['block_time']!, _blockTimeMeta)); + } else if (isInserting) { + context.missing(_blockTimeMeta); + } + if (data.containsKey('blocked')) { + context.handle(_blockedMeta, + blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta)); + } else if (isInserting) { + context.missing(_blockedMeta); + } + if (data.containsKey('used')) { + context.handle( + _usedMeta, used.isAcceptableOrUnknown(data['used']!, _usedMeta)); + } else if (isInserting) { + context.missing(_usedMeta); + } + return context; + } + + @override + Set get $primaryKey => {outputId}; + @override + MwebUtxo map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MwebUtxo( + outputId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}output_id'])!, + address: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}address'])!, + value: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}value'])!, + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height'])!, + blockTime: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}block_time'])!, + blocked: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, + used: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}used'])!, + ); + } + + @override + $MwebUtxosTable createAlias(String alias) { + return $MwebUtxosTable(attachedDatabase, alias); + } +} + +class MwebUtxo extends DataClass implements Insertable { + final String outputId; + final String address; + final int value; + final int height; + final int blockTime; + final bool blocked; + final bool used; + const MwebUtxo( + {required this.outputId, + required this.address, + required this.value, + required this.height, + required this.blockTime, + required this.blocked, + required this.used}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['output_id'] = Variable(outputId); + map['address'] = Variable(address); + map['value'] = Variable(value); + map['height'] = Variable(height); + map['block_time'] = Variable(blockTime); + map['blocked'] = Variable(blocked); + map['used'] = Variable(used); + return map; + } + + MwebUtxosCompanion toCompanion(bool nullToAbsent) { + return MwebUtxosCompanion( + outputId: Value(outputId), + address: Value(address), + value: Value(value), + height: Value(height), + blockTime: Value(blockTime), + blocked: Value(blocked), + used: Value(used), + ); + } + + factory MwebUtxo.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MwebUtxo( + outputId: serializer.fromJson(json['outputId']), + address: serializer.fromJson(json['address']), + value: serializer.fromJson(json['value']), + height: serializer.fromJson(json['height']), + blockTime: serializer.fromJson(json['blockTime']), + blocked: serializer.fromJson(json['blocked']), + used: serializer.fromJson(json['used']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'outputId': serializer.toJson(outputId), + 'address': serializer.toJson(address), + 'value': serializer.toJson(value), + 'height': serializer.toJson(height), + 'blockTime': serializer.toJson(blockTime), + 'blocked': serializer.toJson(blocked), + 'used': serializer.toJson(used), + }; + } + + MwebUtxo copyWith( + {String? outputId, + String? address, + int? value, + int? height, + int? blockTime, + bool? blocked, + bool? used}) => + MwebUtxo( + outputId: outputId ?? this.outputId, + address: address ?? this.address, + value: value ?? this.value, + height: height ?? this.height, + blockTime: blockTime ?? this.blockTime, + blocked: blocked ?? this.blocked, + used: used ?? this.used, + ); + MwebUtxo copyWithCompanion(MwebUtxosCompanion data) { + return MwebUtxo( + outputId: data.outputId.present ? data.outputId.value : this.outputId, + address: data.address.present ? data.address.value : this.address, + value: data.value.present ? data.value.value : this.value, + height: data.height.present ? data.height.value : this.height, + blockTime: data.blockTime.present ? data.blockTime.value : this.blockTime, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + used: data.used.present ? data.used.value : this.used, + ); + } + + @override + String toString() { + return (StringBuffer('MwebUtxo(') + ..write('outputId: $outputId, ') + ..write('address: $address, ') + ..write('value: $value, ') + ..write('height: $height, ') + ..write('blockTime: $blockTime, ') + ..write('blocked: $blocked, ') + ..write('used: $used') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(outputId, address, value, height, blockTime, blocked, used); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MwebUtxo && + other.outputId == this.outputId && + other.address == this.address && + other.value == this.value && + other.height == this.height && + other.blockTime == this.blockTime && + other.blocked == this.blocked && + other.used == this.used); +} + +class MwebUtxosCompanion extends UpdateCompanion { + final Value outputId; + final Value address; + final Value value; + final Value height; + final Value blockTime; + final Value blocked; + final Value used; + final Value rowid; + const MwebUtxosCompanion({ + this.outputId = const Value.absent(), + this.address = const Value.absent(), + this.value = const Value.absent(), + this.height = const Value.absent(), + this.blockTime = const Value.absent(), + this.blocked = const Value.absent(), + this.used = const Value.absent(), + this.rowid = const Value.absent(), + }); + MwebUtxosCompanion.insert({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + this.rowid = const Value.absent(), + }) : outputId = Value(outputId), + address = Value(address), + value = Value(value), + height = Value(height), + blockTime = Value(blockTime), + blocked = Value(blocked), + used = Value(used); + static Insertable custom({ + Expression? outputId, + Expression? address, + Expression? value, + Expression? height, + Expression? blockTime, + Expression? blocked, + Expression? used, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (outputId != null) 'output_id': outputId, + if (address != null) 'address': address, + if (value != null) 'value': value, + if (height != null) 'height': height, + if (blockTime != null) 'block_time': blockTime, + if (blocked != null) 'blocked': blocked, + if (used != null) 'used': used, + if (rowid != null) 'rowid': rowid, + }); + } + + MwebUtxosCompanion copyWith( + {Value? outputId, + Value? address, + Value? value, + Value? height, + Value? blockTime, + Value? blocked, + Value? used, + Value? rowid}) { + return MwebUtxosCompanion( + outputId: outputId ?? this.outputId, + address: address ?? this.address, + value: value ?? this.value, + height: height ?? this.height, + blockTime: blockTime ?? this.blockTime, + blocked: blocked ?? this.blocked, + used: used ?? this.used, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (outputId.present) { + map['output_id'] = Variable(outputId.value); + } + if (address.present) { + map['address'] = Variable(address.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (blockTime.present) { + map['block_time'] = Variable(blockTime.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (used.present) { + map['used'] = Variable(used.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MwebUtxosCompanion(') + ..write('outputId: $outputId, ') + ..write('address: $address, ') + ..write('value: $value, ') + ..write('height: $height, ') + ..write('blockTime: $blockTime, ') + ..write('blocked: $blocked, ') + ..write('used: $used, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$WalletDatabase extends GeneratedDatabase { + _$WalletDatabase(QueryExecutor e) : super(e); + $WalletDatabaseManager get managers => $WalletDatabaseManager(this); + late final $SparkNamesTable sparkNames = $SparkNamesTable(this); + late final $MwebUtxosTable mwebUtxos = $MwebUtxosTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [sparkNames, mwebUtxos]; +} + +typedef $$SparkNamesTableCreateCompanionBuilder = SparkNamesCompanion Function({ + required String name, + required String address, + required int validUntil, + Value additionalInfo, + Value rowid, +}); +typedef $$SparkNamesTableUpdateCompanionBuilder = SparkNamesCompanion Function({ + Value name, + Value address, + Value validUntil, + Value additionalInfo, + Value rowid, +}); + +class $$SparkNamesTableFilterComposer + extends Composer<_$WalletDatabase, $SparkNamesTable> { + $$SparkNamesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get address => $composableBuilder( + column: $table.address, builder: (column) => ColumnFilters(column)); + + ColumnFilters get validUntil => $composableBuilder( + column: $table.validUntil, builder: (column) => ColumnFilters(column)); + + ColumnFilters get additionalInfo => $composableBuilder( + column: $table.additionalInfo, + builder: (column) => ColumnFilters(column)); +} + +class $$SparkNamesTableOrderingComposer + extends Composer<_$WalletDatabase, $SparkNamesTable> { + $$SparkNamesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get address => $composableBuilder( + column: $table.address, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get validUntil => $composableBuilder( + column: $table.validUntil, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get additionalInfo => $composableBuilder( + column: $table.additionalInfo, + builder: (column) => ColumnOrderings(column)); +} + +class $$SparkNamesTableAnnotationComposer + extends Composer<_$WalletDatabase, $SparkNamesTable> { + $$SparkNamesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get address => + $composableBuilder(column: $table.address, builder: (column) => column); + + GeneratedColumn get validUntil => $composableBuilder( + column: $table.validUntil, builder: (column) => column); + + GeneratedColumn get additionalInfo => $composableBuilder( + column: $table.additionalInfo, builder: (column) => column); +} + +class $$SparkNamesTableTableManager extends RootTableManager< + _$WalletDatabase, + $SparkNamesTable, + SparkName, + $$SparkNamesTableFilterComposer, + $$SparkNamesTableOrderingComposer, + $$SparkNamesTableAnnotationComposer, + $$SparkNamesTableCreateCompanionBuilder, + $$SparkNamesTableUpdateCompanionBuilder, + (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), + SparkName, + PrefetchHooks Function()> { + $$SparkNamesTableTableManager(_$WalletDatabase db, $SparkNamesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SparkNamesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SparkNamesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SparkNamesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value name = const Value.absent(), + Value address = const Value.absent(), + Value validUntil = const Value.absent(), + Value additionalInfo = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SparkNamesCompanion( + name: name, + address: address, + validUntil: validUntil, + additionalInfo: additionalInfo, + rowid: rowid, + ), + createCompanionCallback: ({ + required String name, + required String address, + required int validUntil, + Value additionalInfo = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SparkNamesCompanion.insert( + name: name, + address: address, + validUntil: validUntil, + additionalInfo: additionalInfo, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SparkNamesTableProcessedTableManager = ProcessedTableManager< + _$WalletDatabase, + $SparkNamesTable, + SparkName, + $$SparkNamesTableFilterComposer, + $$SparkNamesTableOrderingComposer, + $$SparkNamesTableAnnotationComposer, + $$SparkNamesTableCreateCompanionBuilder, + $$SparkNamesTableUpdateCompanionBuilder, + (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), + SparkName, + PrefetchHooks Function()>; +typedef $$MwebUtxosTableCreateCompanionBuilder = MwebUtxosCompanion Function({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + Value rowid, +}); +typedef $$MwebUtxosTableUpdateCompanionBuilder = MwebUtxosCompanion Function({ + Value outputId, + Value address, + Value value, + Value height, + Value blockTime, + Value blocked, + Value used, + Value rowid, +}); + +class $$MwebUtxosTableFilterComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get outputId => $composableBuilder( + column: $table.outputId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get address => $composableBuilder( + column: $table.address, builder: (column) => ColumnFilters(column)); + + ColumnFilters get value => $composableBuilder( + column: $table.value, builder: (column) => ColumnFilters(column)); + + ColumnFilters get height => $composableBuilder( + column: $table.height, builder: (column) => ColumnFilters(column)); + + ColumnFilters get blockTime => $composableBuilder( + column: $table.blockTime, builder: (column) => ColumnFilters(column)); + + ColumnFilters get blocked => $composableBuilder( + column: $table.blocked, builder: (column) => ColumnFilters(column)); + + ColumnFilters get used => $composableBuilder( + column: $table.used, builder: (column) => ColumnFilters(column)); +} + +class $$MwebUtxosTableOrderingComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get outputId => $composableBuilder( + column: $table.outputId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get address => $composableBuilder( + column: $table.address, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get value => $composableBuilder( + column: $table.value, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get height => $composableBuilder( + column: $table.height, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get blockTime => $composableBuilder( + column: $table.blockTime, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get blocked => $composableBuilder( + column: $table.blocked, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get used => $composableBuilder( + column: $table.used, builder: (column) => ColumnOrderings(column)); +} + +class $$MwebUtxosTableAnnotationComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get outputId => + $composableBuilder(column: $table.outputId, builder: (column) => column); + + GeneratedColumn get address => + $composableBuilder(column: $table.address, builder: (column) => column); + + GeneratedColumn get value => + $composableBuilder(column: $table.value, builder: (column) => column); + + GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + + GeneratedColumn get blockTime => + $composableBuilder(column: $table.blockTime, builder: (column) => column); + + GeneratedColumn get blocked => + $composableBuilder(column: $table.blocked, builder: (column) => column); + + GeneratedColumn get used => + $composableBuilder(column: $table.used, builder: (column) => column); +} + +class $$MwebUtxosTableTableManager extends RootTableManager< + _$WalletDatabase, + $MwebUtxosTable, + MwebUtxo, + $$MwebUtxosTableFilterComposer, + $$MwebUtxosTableOrderingComposer, + $$MwebUtxosTableAnnotationComposer, + $$MwebUtxosTableCreateCompanionBuilder, + $$MwebUtxosTableUpdateCompanionBuilder, + (MwebUtxo, BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>), + MwebUtxo, + PrefetchHooks Function()> { + $$MwebUtxosTableTableManager(_$WalletDatabase db, $MwebUtxosTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MwebUtxosTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MwebUtxosTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MwebUtxosTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value outputId = const Value.absent(), + Value address = const Value.absent(), + Value value = const Value.absent(), + Value height = const Value.absent(), + Value blockTime = const Value.absent(), + Value blocked = const Value.absent(), + Value used = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MwebUtxosCompanion( + outputId: outputId, + address: address, + value: value, + height: height, + blockTime: blockTime, + blocked: blocked, + used: used, + rowid: rowid, + ), + createCompanionCallback: ({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + Value rowid = const Value.absent(), + }) => + MwebUtxosCompanion.insert( + outputId: outputId, + address: address, + value: value, + height: height, + blockTime: blockTime, + blocked: blocked, + used: used, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$MwebUtxosTableProcessedTableManager = ProcessedTableManager< + _$WalletDatabase, + $MwebUtxosTable, + MwebUtxo, + $$MwebUtxosTableFilterComposer, + $$MwebUtxosTableOrderingComposer, + $$MwebUtxosTableAnnotationComposer, + $$MwebUtxosTableCreateCompanionBuilder, + $$MwebUtxosTableUpdateCompanionBuilder, + (MwebUtxo, BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>), + MwebUtxo, + PrefetchHooks Function()>; + +class $WalletDatabaseManager { + final _$WalletDatabase _db; + $WalletDatabaseManager(this._db); + $$SparkNamesTableTableManager get sparkNames => + $$SparkNamesTableTableManager(_db, _db.sparkNames); + $$MwebUtxosTableTableManager get mwebUtxos => + $$MwebUtxosTableTableManager(_db, _db.mwebUtxos); +} diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 0e9449094..be431aba7 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -33,11 +33,11 @@ class DB { static const String boxNameTrades = "exchangeTransactionsBox"; static const String boxNameAllWalletsData = "wallets"; static const String boxNameFavoriteWallets = "favoriteWallets"; + static const String boxNamePrimaryNodesDeprecated = "primaryNodes"; // in use // TODO: migrate static const String boxNameNodeModels = "nodeModels"; - static const String boxNamePrimaryNodes = "primaryNodes"; static const String boxNameNotifications = "notificationModels"; static const String boxNameWatchedTransactions = "watchedTxNotificationModels"; @@ -127,29 +127,27 @@ class DB { _boxNodeModels = await hive.openBox(boxNameNodeModels); } - if (hive.isBoxOpen(boxNamePrimaryNodes)) { - _boxPrimaryNodes = hive.box(boxNamePrimaryNodes); - } else { - _boxPrimaryNodes = await hive.openBox(boxNamePrimaryNodes); - } - if (hive.isBoxOpen(boxNameAllWalletsData)) { _boxAllWalletsData = hive.box(boxNameAllWalletsData); } else { _boxAllWalletsData = await hive.openBox(boxNameAllWalletsData); } - _boxNotifications = - await hive.openBox(boxNameNotifications); - _boxWatchedTransactions = - await hive.openBox(boxNameWatchedTransactions); - _boxWatchedTrades = - await hive.openBox(boxNameWatchedTrades); + _boxNotifications = await hive.openBox( + boxNameNotifications, + ); + _boxWatchedTransactions = await hive.openBox( + boxNameWatchedTransactions, + ); + _boxWatchedTrades = await hive.openBox( + boxNameWatchedTrades, + ); _boxTradesV2 = await hive.openBox(boxNameTradesV2); _boxTradeNotes = await hive.openBox(boxNameTradeNotes); _boxTradeLookup = await hive.openBox(boxNameTradeLookup); _walletInfoSource = await hive.openBox( - lib_monero_compat.WalletInfo.boxName); + lib_monero_compat.WalletInfo.boxName, + ); _boxFavoriteWallets = await hive.openBox(boxNameFavoriteWallets); await Future.wait([ @@ -183,11 +181,13 @@ class DB { for (final entry in mapped.entries) { if (hive.isBoxOpen(entry.value.walletId)) { - _walletBoxes[entry.value.walletId] = - hive.box(entry.value.walletId); + _walletBoxes[entry.value.walletId] = hive.box( + entry.value.walletId, + ); } else { - _walletBoxes[entry.value.walletId] = - await hive.openBox(entry.value.walletId); + _walletBoxes[entry.value.walletId] = await hive.openBox( + entry.value.walletId, + ); } } } @@ -196,8 +196,9 @@ class DB { if (_txCacheBoxes[currency.identifier]?.isOpen != true) { _txCacheBoxes.remove(currency.identifier); } - return _txCacheBoxes[currency.identifier] ??= - await hive.openBox(_boxNameTxCache(currency: currency)); + return _txCacheBoxes[currency.identifier] ??= await hive.openBox( + _boxNameTxCache(currency: currency), + ); } Future closeTxCacheBox({required CryptoCurrency currency}) async { @@ -210,8 +211,9 @@ class DB { if (_setCacheBoxes[currency.identifier]?.isOpen != true) { _setCacheBoxes.remove(currency.identifier); } - return _setCacheBoxes[currency.identifier] ??= - await hive.openBox(_boxNameSetCache(currency: currency)); + return _setCacheBoxes[currency.identifier] ??= await hive.openBox( + _boxNameSetCache(currency: currency), + ); } Future closeAnonymitySetCacheBox({ @@ -226,10 +228,8 @@ class DB { if (_usedSerialsCacheBoxes[currency.identifier]?.isOpen != true) { _usedSerialsCacheBoxes.remove(currency.identifier); } - return _usedSerialsCacheBoxes[currency.identifier] ??= - await hive.openBox( - _boxNameUsedSerialsCache(currency: currency), - ); + return _usedSerialsCacheBoxes[currency.identifier] ??= await hive + .openBox(_boxNameUsedSerialsCache(currency: currency)); } Future closeUsedSerialsCacheBox({ @@ -274,10 +274,7 @@ class DB { List values({required String boxName}) => hive.box(boxName).values.toList(growable: false); - T? get({ - required String boxName, - required dynamic key, - }) => + T? get({required String boxName, required dynamic key}) => hive.box(boxName).get(key); bool containsKey({required String boxName, required dynamic key}) => @@ -289,9 +286,9 @@ class DB { required String boxName, required dynamic key, required T value, - }) async => - await mutex - .protect(() async => await hive.box(boxName).put(key, value)); + }) async => await mutex.protect( + () async => await hive.box(boxName).put(key, value), + ); Future add({required String boxName, required T value}) async => await mutex.protect(() async => await hive.box(boxName).add(value)); @@ -299,9 +296,9 @@ class DB { Future addAll({ required String boxName, required Iterable values, - }) async => - await mutex - .protect(() async => await hive.box(boxName).addAll(values)); + }) async => await mutex.protect( + () async => await hive.box(boxName).addAll(values), + ); Future delete({ required dynamic key, @@ -325,11 +322,11 @@ class DB { await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameAddressBook); await DB.instance.deleteBoxFromDisk(boxName: "debugInfoBox"); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameNodeModels); - await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePrimaryNodes); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameAllWalletsData); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameNotifications); - await DB.instance - .deleteBoxFromDisk(boxName: DB.boxNameWatchedTransactions); + await DB.instance.deleteBoxFromDisk( + boxName: DB.boxNameWatchedTransactions, + ); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameWatchedTrades); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTrades); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTradesV2); @@ -337,8 +334,9 @@ class DB { await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameTradeLookup); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameFavoriteWallets); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePrefs); - await DB.instance - .deleteBoxFromDisk(boxName: DB.boxNameWalletsToDeleteOnStart); + await DB.instance.deleteBoxFromDisk( + boxName: DB.boxNameWalletsToDeleteOnStart, + ); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNamePriceCache); await DB.instance.deleteBoxFromDisk(boxName: DB.boxNameDBInfo); await DB.instance.deleteBoxFromDisk(boxName: "theme"); diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index a16fb2b9a..0a0269025 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -63,7 +63,6 @@ class MainDB { StackThemeSchema, ContactEntrySchema, OrdinalSchema, - LelantusCoinSchema, WalletInfoSchema, TransactionV2Schema, SparkCoinSchema, @@ -93,13 +92,16 @@ class MainDB { Future updateWalletInfo(WalletInfo walletInfo) async { try { await isar.writeTxn(() async { - final info = await isar.walletInfo - .where() - .walletIdEqualTo(walletInfo.walletId) - .findFirst(); + final info = + await isar.walletInfo + .where() + .walletIdEqualTo(walletInfo.walletId) + .findFirst(); if (info == null) { - throw Exception("updateWalletInfo() called with new WalletInfo." - " Use putWalletInfo()"); + throw Exception( + "updateWalletInfo() called with new WalletInfo." + " Use putWalletInfo()", + ); } await isar.walletInfo.deleteByWalletId(walletInfo.walletId); @@ -174,8 +176,7 @@ class MainDB { // addresses QueryBuilder getAddresses( String walletId, - ) => - isar.addresses.where().walletIdEqualTo(walletId); + ) => isar.addresses.where().walletIdEqualTo(walletId); Future putAddress(Address address) async { try { @@ -202,8 +203,10 @@ class MainDB { final List ids = []; await isar.writeTxn(() async { for (final address in addresses) { - final storedAddress = await isar.addresses - .getByValueWalletId(address.value, address.walletId); + final storedAddress = await isar.addresses.getByValueWalletId( + address.value, + address.walletId, + ); int id; if (storedAddress == null) { @@ -253,8 +256,7 @@ class MainDB { // transactions QueryBuilder getTransactions( String walletId, - ) => - isar.transactions.where().walletIdEqualTo(walletId); + ) => isar.transactions.where().walletIdEqualTo(walletId); Future putTransaction(Transaction transaction) async { try { @@ -294,31 +296,32 @@ class MainDB { QueryBuilder getUTXOsByAddress( String walletId, String address, - ) => - isar.utxos - .where() - .walletIdEqualTo(walletId) - .filter() - .addressEqualTo(address); + ) => isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .addressEqualTo(address); Future putUTXO(UTXO utxo) => isar.writeTxn(() async { - await isar.utxos.put(utxo); - }); + await isar.utxos.put(utxo); + }); Future putUTXOs(List utxos) => isar.writeTxn(() async { - await isar.utxos.putAll(utxos); - }); + await isar.utxos.putAll(utxos); + }); Future updateUTXOs(String walletId, List utxos) async { bool newUTXO = false; + await isar.writeTxn(() async { final set = utxos.toSet(); for (final utxo in utxos) { // check if utxo exists in db and update accordingly - final storedUtxo = await isar.utxos - .where() - .txidWalletIdVoutEqualTo(utxo.txid, utxo.walletId, utxo.vout) - .findFirst(); + final storedUtxo = + await isar.utxos + .where() + .txidWalletIdVoutEqualTo(utxo.txid, utxo.walletId, utxo.vout) + .findFirst(); if (storedUtxo != null) { // update @@ -338,23 +341,22 @@ class MainDB { } await isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await isar.utxos.putAll(set.toList()); + if (set.isNotEmpty) { + await isar.utxos.putAll(set.toList()); + } }); return newUTXO; } - Stream watchUTXO({ - required Id id, - bool fireImmediately = false, - }) { + Stream watchUTXO({required Id id, bool fireImmediately = false}) { return isar.utxos.watchObject(id, fireImmediately: fireImmediately); } // transaction notes QueryBuilder - getTransactionNotes(String walletId) => - isar.transactionNotes.where().walletIdEqualTo(walletId); + getTransactionNotes(String walletId) => + isar.transactionNotes.where().walletIdEqualTo(walletId); Future putTransactionNote(TransactionNote transactionNote) => isar.writeTxn(() async { @@ -370,25 +372,23 @@ class MainDB { String walletId, String txid, ) async { - return isar.transactionNotes.getByTxidWalletId( - txid, - walletId, - ); + return isar.transactionNotes.getByTxidWalletId(txid, walletId); } Stream watchTransactionNote({ required Id id, bool fireImmediately = false, }) { - return isar.transactionNotes - .watchObject(id, fireImmediately: fireImmediately); + return isar.transactionNotes.watchObject( + id, + fireImmediately: fireImmediately, + ); } // address labels QueryBuilder getAddressLabels( String walletId, - ) => - isar.addressLabels.where().walletIdEqualTo(walletId); + ) => isar.addressLabels.where().walletIdEqualTo(walletId); Future putAddressLabel(AddressLabel addressLabel) => isar.writeTxn(() async { @@ -396,8 +396,8 @@ class MainDB { }); int putAddressLabelSync(AddressLabel addressLabel) => isar.writeTxnSync(() { - return isar.addressLabels.putSync(addressLabel); - }); + return isar.addressLabels.putSync(addressLabel); + }); Future putAddressLabels(List addressLabels) => isar.writeTxn(() async { @@ -453,59 +453,44 @@ class MainDB { // transactions for (int i = 0; i < transactionCount; i += paginateLimit) { - final txnIds = await getTransactions(walletId) - .offset(i) - .limit(paginateLimit) - .idProperty() - .findAll(); + final txnIds = + await getTransactions( + walletId, + ).offset(i).limit(paginateLimit).idProperty().findAll(); await isar.transactions.deleteAll(txnIds); } // transactions V2 for (int i = 0; i < transactionCountV2; i += paginateLimit) { - final txnIds = await isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .offset(i) - .limit(paginateLimit) - .idProperty() - .findAll(); + final txnIds = + await isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .offset(i) + .limit(paginateLimit) + .idProperty() + .findAll(); await isar.transactionV2s.deleteAll(txnIds); } // addresses for (int i = 0; i < addressCount; i += paginateLimit) { - final addressIds = await getAddresses(walletId) - .offset(i) - .limit(paginateLimit) - .idProperty() - .findAll(); + final addressIds = + await getAddresses( + walletId, + ).offset(i).limit(paginateLimit).idProperty().findAll(); await isar.addresses.deleteAll(addressIds); } // utxos for (int i = 0; i < utxoCount; i += paginateLimit) { - final utxoIds = await getUTXOs(walletId) - .offset(i) - .limit(paginateLimit) - .idProperty() - .findAll(); + final utxoIds = + await getUTXOs( + walletId, + ).offset(i).limit(paginateLimit).idProperty().findAll(); await isar.utxos.deleteAll(utxoIds); } - // lelantusCoins - await isar.lelantusCoins.where().walletIdEqualTo(walletId).deleteAll(); - // for (int i = 0; i < lelantusCoinCount; i += paginateLimit) { - // final lelantusCoinIds = await isar.lelantusCoins - // .where() - // .walletIdEqualTo(walletId) - // .offset(i) - // .limit(paginateLimit) - // .idProperty() - // .findAll(); - // await isar.lelantusCoins.deleteAll(lelantusCoinIds); - // } - // spark coins await isar.sparkCoins .where() @@ -519,11 +504,10 @@ class MainDB { await isar.writeTxn(() async { const paginateLimit = 50; for (int i = 0; i < addressLabelCount; i += paginateLimit) { - final labelIds = await getAddressLabels(walletId) - .offset(i) - .limit(paginateLimit) - .idProperty() - .findAll(); + final labelIds = + await getAddressLabels( + walletId, + ).offset(i).limit(paginateLimit).idProperty().findAll(); await isar.addressLabels.deleteAll(labelIds); } }); @@ -534,11 +518,10 @@ class MainDB { await isar.writeTxn(() async { const paginateLimit = 50; for (int i = 0; i < noteCount; i += paginateLimit) { - final labelIds = await getTransactionNotes(walletId) - .offset(i) - .limit(paginateLimit) - .idProperty() - .findAll(); + final labelIds = + await getTransactionNotes( + walletId, + ).offset(i).limit(paginateLimit).idProperty().findAll(); await isar.transactionNotes.deleteAll(labelIds); } }); @@ -591,10 +574,11 @@ class MainDB { final List ids = []; await isar.writeTxn(() async { for (final tx in transactions) { - final storedTx = await isar.transactionV2s - .where() - .txidWalletIdEqualTo(tx.txid, tx.walletId) - .findFirst(); + final storedTx = + await isar.transactionV2s + .where() + .txidWalletIdEqualTo(tx.txid, tx.walletId) + .findFirst(); Id id; if (storedTx == null) { @@ -630,22 +614,11 @@ class MainDB { isar.ethContracts.where().addressEqualTo(contractAddress).findFirstSync(); Future putEthContract(EthContract contract) => isar.writeTxn(() async { - return await isar.ethContracts.put(contract); - }); + return await isar.ethContracts.put(contract); + }); Future putEthContracts(List contracts) => isar.writeTxn(() async { await isar.ethContracts.putAll(contracts); }); - - // ========== Lelantus ======================================================= - - Future getHighestUsedMintIndex({required String walletId}) async { - return await isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .sortByMintIndexDesc() - .mintIndexProperty() - .findFirst(); - } } diff --git a/lib/db/migrate_wallets_to_isar.dart b/lib/db/migrate_wallets_to_isar.dart index cd54a4063..6433799f2 100644 --- a/lib/db/migrate_wallets_to_isar.dart +++ b/lib/db/migrate_wallets_to_isar.dart @@ -20,14 +20,17 @@ Future migrateWalletsToIsar({ await MainDB.instance.initMainDB(); // ensure fresh - await MainDB.instance.isar - .writeTxn(() async => await MainDB.instance.isar.transactionV2s.clear()); + await MainDB.instance.isar.writeTxn( + () async => await MainDB.instance.isar.transactionV2s.clear(), + ); - final allWalletsBox = - await DB.instance.hive.openBox(DB.boxNameAllWalletsData); + final allWalletsBox = await DB.instance.hive.openBox( + DB.boxNameAllWalletsData, + ); - final names = DB.instance - .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + final names = + DB.instance.get(boxName: DB.boxNameAllWalletsData, key: 'names') + as Map?; if (names == null) { // no wallets to migrate @@ -37,27 +40,23 @@ Future migrateWalletsToIsar({ // // Parse the old data from the Hive map into a nice list // - final List< - ({ - String coinIdentifier, - String name, - String walletId, - })> oldInfo = Map.from(names).values.map((e) { - final map = e as Map; - return ( - coinIdentifier: map["coin"] as String, - walletId: map["id"] as String, - name: map["name"] as String, - ); - }).toList(); + final List<({String coinIdentifier, String name, String walletId})> oldInfo = + Map.from(names).values.map((e) { + final map = e as Map; + return ( + coinIdentifier: map["coin"] as String, + walletId: map["id"] as String, + name: map["name"] as String, + ); + }).toList(); // // Get current ordered list of favourite wallet Ids // final List favourites = - (await DB.instance.hive.openBox(DB.boxNameFavoriteWallets)) - .values - .toList(); + (await DB.instance.hive.openBox( + DB.boxNameFavoriteWallets, + )).values.toList(); final List<(WalletInfo, WalletInfoMeta)> newInfo = []; final List tokenInfo = []; @@ -72,10 +71,11 @@ Future migrateWalletsToIsar({ // // First handle transaction notes // - final newNoteCount = await MainDB.instance.isar.transactionNotes - .where() - .walletIdEqualTo(old.walletId) - .count(); + final newNoteCount = + await MainDB.instance.isar.transactionNotes + .where() + .walletIdEqualTo(old.walletId) + .count(); if (newNoteCount == 0) { final map = walletBox.get('notes') as Map?; @@ -108,18 +108,18 @@ Future migrateWalletsToIsar({ // final Map otherData = {}; - final List? tokenContractAddresses = walletBox.get( - "ethTokenContracts", - ) as List?; + final List? tokenContractAddresses = + walletBox.get("ethTokenContracts") as List?; if (tokenContractAddresses?.isNotEmpty == true) { otherData[WalletInfoKeys.tokenContractAddresses] = tokenContractAddresses; for (final address in tokenContractAddresses!) { - final contract = await MainDB.instance.isar.ethContracts - .where() - .addressEqualTo(address) - .findFirst(); + final contract = + await MainDB.instance.isar.ethContracts + .where() + .addressEqualTo(address) + .findFirst(); if (contract != null) { tokenInfo.add( TokenWalletInfo( @@ -133,7 +133,7 @@ Future migrateWalletsToIsar({ } // epiccash specifics - if (old.coinIdentifier == Epiccash(CryptoCurrencyNetwork.main)) { + if (old.coinIdentifier == Epiccash(CryptoCurrencyNetwork.main).identifier) { final epicWalletInfo = ExtraEpiccashWalletInfo.fromMap({ "receivingIndex": walletBox.get("receivingIndex") as int? ?? 0, "changeIndex": walletBox.get("changeIndex") as int? ?? 0, @@ -146,12 +146,6 @@ Future migrateWalletsToIsar({ otherData[WalletInfoKeys.epiccashData] = jsonEncode( epicWalletInfo.toMap(), ); - } else if (old.coinIdentifier == - Firo(CryptoCurrencyNetwork.main).identifier || - old.coinIdentifier == Firo(CryptoCurrencyNetwork.test).identifier) { - otherData[WalletInfoKeys.lelantusCoinIsarRescanRequired] = walletBox - .get(WalletInfoKeys.lelantusCoinIsarRescanRequired) as bool? ?? - true; } // @@ -161,8 +155,9 @@ Future migrateWalletsToIsar({ final infoMeta = WalletInfoMeta( walletId: old.walletId, - isMnemonicVerified: allWalletsBox - .get("${old.walletId}_mnemonicHasBeenVerified") as bool? ?? + isMnemonicVerified: + allWalletsBox.get("${old.walletId}_mnemonicHasBeenVerified") + as bool? ?? false, ); @@ -170,19 +165,15 @@ Future migrateWalletsToIsar({ coinName: old.coinIdentifier, walletId: old.walletId, name: old.name, - mainAddressType: AppConfig.getCryptoCurrencyFor(old.coinIdentifier)! - .defaultAddressType, + mainAddressType: + AppConfig.getCryptoCurrencyFor( + old.coinIdentifier, + )!.defaultAddressType, favouriteOrderIndex: favourites.indexOf(old.walletId), - cachedChainHeight: walletBox.get( - DBKeys.storedChainHeight, - ) as int? ?? - 0, - cachedBalanceString: walletBox.get( - DBKeys.cachedBalance, - ) as String?, - cachedBalanceSecondaryString: walletBox.get( - DBKeys.cachedBalanceSecondary, - ) as String?, + cachedChainHeight: walletBox.get(DBKeys.storedChainHeight) as int? ?? 0, + cachedBalanceString: walletBox.get(DBKeys.cachedBalance) as String?, + cachedBalanceSecondaryString: + walletBox.get(DBKeys.cachedBalanceSecondary) as String?, otherDataJsonString: jsonEncode(otherData), ); @@ -197,10 +188,12 @@ Future migrateWalletsToIsar({ if (newInfo.isNotEmpty) { await MainDB.instance.isar.writeTxn(() async { - await MainDB.instance.isar.walletInfo - .putAll(newInfo.map((e) => e.$1).toList()); - await MainDB.instance.isar.walletInfoMeta - .putAll(newInfo.map((e) => e.$2).toList()); + await MainDB.instance.isar.walletInfo.putAll( + newInfo.map((e) => e.$1).toList(), + ); + await MainDB.instance.isar.walletInfoMeta.putAll( + newInfo.map((e) => e.$2).toList(), + ); if (tokenInfo.isNotEmpty) { await MainDB.instance.isar.tokenWalletInfo.putAll(tokenInfo); diff --git a/lib/dto/ethereum/eth_token_tx_dto.dart b/lib/dto/ethereum/eth_token_tx_dto.dart index 51a9a7449..0a63c6106 100644 --- a/lib/dto/ethereum/eth_token_tx_dto.dart +++ b/lib/dto/ethereum/eth_token_tx_dto.dart @@ -28,23 +28,32 @@ class EthTokenTxDto { required this.articulatedLog, required this.transactionHash, required this.transactionIndex, + required this.blockHash, + required this.timestamp, + required this.nonce, + required this.gasUsed, + required this.gasPrice, }); EthTokenTxDto.fromMap(Map map) - : address = map['address'] as String, - blockNumber = map['blockNumber'] as int, - logIndex = map['logIndex'] as int, - topics = List.from(map['topics'] as List), - data = map['data'] as String, - articulatedLog = map['articulatedLog'] == null - ? null - : ArticulatedLog.fromMap( - Map.from( - map['articulatedLog'] as Map, - ), + : address = map['address'] as String, + blockNumber = map['blockNumber'] as int, + logIndex = map['logIndex'] as int, + topics = List.from(map['topics'] as List), + data = map['data'] as String, + articulatedLog = + map['articulatedLog'] == null + ? null + : ArticulatedLog.fromMap( + Map.from(map['articulatedLog'] as Map), ), - transactionHash = map['transactionHash'] as String, - transactionIndex = map['transactionIndex'] as int; + transactionHash = map['transactionHash'] as String, + transactionIndex = map['transactionIndex'] as int, + blockHash = map['blockHash'] as String?, + timestamp = map['timestamp'] as int, + nonce = map['nonce'] as int?, + gasUsed = map['gasUsed'] as int?, + gasPrice = BigInt.tryParse(map['gasPrice'].toString()); final String address; final int blockNumber; @@ -54,6 +63,11 @@ class EthTokenTxDto { final ArticulatedLog? articulatedLog; final String transactionHash; final int transactionIndex; + final String? blockHash; + final int timestamp; + final int? nonce; + final int? gasUsed; + final BigInt? gasPrice; EthTokenTxDto copyWith({ String? address, @@ -65,17 +79,26 @@ class EthTokenTxDto { String? compressedLog, String? transactionHash, int? transactionIndex, - }) => - EthTokenTxDto( - address: address ?? this.address, - blockNumber: blockNumber ?? this.blockNumber, - logIndex: logIndex ?? this.logIndex, - topics: topics ?? this.topics, - data: data ?? this.data, - articulatedLog: articulatedLog ?? this.articulatedLog, - transactionHash: transactionHash ?? this.transactionHash, - transactionIndex: transactionIndex ?? this.transactionIndex, - ); + String? blockHash, + int? timestamp, + int? nonce, + int? gasUsed, + BigInt? gasPrice, + }) => EthTokenTxDto( + address: address ?? this.address, + blockNumber: blockNumber ?? this.blockNumber, + logIndex: logIndex ?? this.logIndex, + topics: topics ?? this.topics, + data: data ?? this.data, + articulatedLog: articulatedLog ?? this.articulatedLog, + transactionHash: transactionHash ?? this.transactionHash, + transactionIndex: transactionIndex ?? this.transactionIndex, + blockHash: blockHash ?? this.blockHash, + timestamp: timestamp ?? this.timestamp, + nonce: nonce ?? this.nonce, + gasUsed: gasUsed ?? this.gasUsed, + gasPrice: gasPrice ?? this.gasPrice, + ); Map toMap() { final map = {}; @@ -87,6 +110,11 @@ class EthTokenTxDto { map['articulatedLog'] = articulatedLog?.toMap(); map['transactionHash'] = transactionHash; map['transactionIndex'] = transactionIndex; + map['blockHash'] = blockHash; + map['timestamp'] = timestamp; + map['nonce'] = nonce; + map['gasPrice'] = gasPrice; + map['gasUsed'] = gasUsed; return map; } @@ -100,30 +128,17 @@ class EthTokenTxDto { /// inputs : {"_amount":"3291036540000000000","_from":"0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","_to":"0xc5e81fc2401b8104966637d5334cbce92f01dbf7"} class ArticulatedLog { - ArticulatedLog({ - required this.name, - required this.inputs, - }); + ArticulatedLog({required this.name, required this.inputs}); ArticulatedLog.fromMap(Map map) - : name = map['name'] as String, - inputs = Inputs.fromMap( - Map.from( - map['inputs'] as Map, - ), - ); + : name = map['name'] as String, + inputs = Inputs.fromMap(Map.from(map['inputs'] as Map)); final String name; final Inputs inputs; - ArticulatedLog copyWith({ - String? name, - Inputs? inputs, - }) => - ArticulatedLog( - name: name ?? this.name, - inputs: inputs ?? this.inputs, - ); + ArticulatedLog copyWith({String? name, Inputs? inputs}) => + ArticulatedLog(name: name ?? this.name, inputs: inputs ?? this.inputs); Map toMap() { final map = {}; @@ -138,31 +153,22 @@ class ArticulatedLog { /// _to : "0xc5e81fc2401b8104966637d5334cbce92f01dbf7" /// class Inputs { - Inputs({ - required this.amount, - required this.from, - required this.to, - }); + Inputs({required this.amount, required this.from, required this.to}); Inputs.fromMap(Map map) - : amount = map['_amount'] as String, - from = map['_from'] as String, - to = map['_to'] as String; + : amount = map['_amount'] as String, + from = map['_from'] as String, + to = map['_to'] as String; final String amount; final String from; final String to; - Inputs copyWith({ - String? amount, - String? from, - String? to, - }) => - Inputs( - amount: amount ?? this.amount, - from: from ?? this.from, - to: to ?? this.to, - ); + Inputs copyWith({String? amount, String? from, String? to}) => Inputs( + amount: amount ?? this.amount, + from: from ?? this.from, + to: to ?? this.to, + ); Map toMap() { final map = {}; diff --git a/lib/dto/ethereum/eth_token_tx_extra_dto.dart b/lib/dto/ethereum/eth_token_tx_extra_dto.dart deleted file mode 100644 index 401ea7122..000000000 --- a/lib/dto/ethereum/eth_token_tx_extra_dto.dart +++ /dev/null @@ -1,131 +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:convert'; - -import '../../utilities/amount/amount.dart'; -import '../../wallets/crypto_currency/crypto_currency.dart'; - -class EthTokenTxExtraDTO { - EthTokenTxExtraDTO({ - required this.blockHash, - required this.blockNumber, - required this.from, - required this.gas, - required this.gasCost, - required this.gasPrice, - required this.gasUsed, - required this.hash, - required this.input, - required this.nonce, - required this.timestamp, - required this.to, - required this.transactionIndex, - required this.value, - }); - - factory EthTokenTxExtraDTO.fromMap(Map map) => - EthTokenTxExtraDTO( - hash: map['hash'] as String, - blockHash: map['blockHash'] as String, - blockNumber: map['blockNumber'] as int, - transactionIndex: map['transactionIndex'] as int, - timestamp: map['timestamp'] as int, - from: map['from'] as String, - to: map['to'] as String, - value: Amount( - rawValue: BigInt.parse(map['value'] as String), - fractionDigits: Ethereum(CryptoCurrencyNetwork.main).fractionDigits, - ), - gas: _amountFromJsonNum(map['gas']), - gasPrice: _amountFromJsonNum(map['gasPrice']), - nonce: map['nonce'] as int?, - input: map['input'] as String, - gasCost: _amountFromJsonNum(map['gasCost']), - gasUsed: _amountFromJsonNum(map['gasUsed']), - ); - - final String hash; - final String blockHash; - final int blockNumber; - final int transactionIndex; - final int timestamp; - final String from; - final String to; - final Amount value; - final Amount gas; - final Amount gasPrice; - final String input; - final int? nonce; - final Amount gasCost; - final Amount gasUsed; - - static Amount _amountFromJsonNum(dynamic json) { - return Amount( - rawValue: BigInt.from(json as num), - fractionDigits: Ethereum(CryptoCurrencyNetwork.main).fractionDigits, - ); - } - - EthTokenTxExtraDTO copyWith({ - String? hash, - String? blockHash, - int? blockNumber, - int? transactionIndex, - int? timestamp, - String? from, - String? to, - Amount? value, - Amount? gas, - Amount? gasPrice, - int? nonce, - String? input, - Amount? gasCost, - Amount? gasUsed, - }) => - EthTokenTxExtraDTO( - hash: hash ?? this.hash, - blockHash: blockHash ?? this.blockHash, - blockNumber: blockNumber ?? this.blockNumber, - transactionIndex: transactionIndex ?? this.transactionIndex, - timestamp: timestamp ?? this.timestamp, - from: from ?? this.from, - to: to ?? this.to, - value: value ?? this.value, - gas: gas ?? this.gas, - gasPrice: gasPrice ?? this.gasPrice, - nonce: nonce ?? this.nonce, - input: input ?? this.input, - gasCost: gasCost ?? this.gasCost, - gasUsed: gasUsed ?? this.gasUsed, - ); - - Map toMap() { - final map = {}; - map['hash'] = hash; - map['blockHash'] = blockHash; - map['blockNumber'] = blockNumber; - map['transactionIndex'] = transactionIndex; - map['timestamp'] = timestamp; - map['from'] = from; - map['to'] = to; - map['value'] = value.toJsonString(); - map['gas'] = gas.toJsonString(); - map['gasPrice'] = gasPrice.toJsonString(); - map['input'] = input; - map['nonce'] = nonce; - map['gasCost'] = gasCost.toJsonString(); - map['gasUsed'] = gasUsed.toJsonString(); - return map; - } - - @override - String toString() => jsonEncode(toMap()); -} diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart index 10a46d740..0801d7050 100644 --- a/lib/dto/ethereum/eth_tx_dto.dart +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -31,26 +31,28 @@ class EthTxDTO { required this.hasToken, required this.gasCost, required this.gasUsed, + required this.nonce, }); factory EthTxDTO.fromMap(Map map) => EthTxDTO( - hash: map['hash'] as String, - blockHash: map['blockHash'] as String, - blockNumber: map['blockNumber'] as int, - transactionIndex: map['transactionIndex'] as int, - timestamp: map['timestamp'] as int, - from: map['from'] as String, - to: map['to'] as String, - value: _amountFromJsonNum(map['value'])!, - gas: _amountFromJsonNum(map['gas'])!, - gasPrice: _amountFromJsonNum(map['gasPrice'])!, - maxFeePerGas: _amountFromJsonNum(map['maxFeePerGas']), - maxPriorityFeePerGas: _amountFromJsonNum(map['maxPriorityFeePerGas']), - isError: map['isError'] as bool? ?? false, - hasToken: map['hasToken'] as bool? ?? false, - gasCost: _amountFromJsonNum(map['gasCost'])!, - gasUsed: _amountFromJsonNum(map['gasUsed'])!, - ); + hash: map['hash'] as String, + blockHash: map['blockHash'] as String, + blockNumber: map['blockNumber'] as int, + transactionIndex: map['transactionIndex'] as int, + timestamp: map['timestamp'] as int, + from: map['from'] as String, + to: map['to'] as String, + value: _amountFromJsonNum(map['value'])!, + gas: _amountFromJsonNum(map['gas'])!, + gasPrice: _amountFromJsonNum(map['gasPrice'])!, + maxFeePerGas: _amountFromJsonNum(map['maxFeePerGas']), + maxPriorityFeePerGas: _amountFromJsonNum(map['maxPriorityFeePerGas']), + isError: map['isError'] as bool? ?? false, + hasToken: map['hasToken'] as bool? ?? false, + gasCost: _amountFromJsonNum(map['gasCost'])!, + gasUsed: _amountFromJsonNum(map['gasUsed'])!, + nonce: map['nonce'] as int?, + ); final String hash; final String blockHash; @@ -68,6 +70,7 @@ class EthTxDTO { final bool hasToken; final Amount gasCost; final Amount gasUsed; + final int? nonce; static Amount? _amountFromJsonNum(dynamic json) { if (json == null) { @@ -97,25 +100,26 @@ class EthTxDTO { String? compressedTx, Amount? gasCost, Amount? gasUsed, - }) => - EthTxDTO( - hash: hash ?? this.hash, - blockHash: blockHash ?? this.blockHash, - blockNumber: blockNumber ?? this.blockNumber, - transactionIndex: transactionIndex ?? this.transactionIndex, - timestamp: timestamp ?? this.timestamp, - from: from ?? this.from, - to: to ?? this.to, - value: value ?? this.value, - gas: gas ?? this.gas, - gasPrice: gasPrice ?? this.gasPrice, - maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, - maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, - isError: isError ?? this.isError, - hasToken: hasToken ?? this.hasToken, - gasCost: gasCost ?? this.gasCost, - gasUsed: gasUsed ?? this.gasUsed, - ); + int? nonce, + }) => EthTxDTO( + hash: hash ?? this.hash, + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + transactionIndex: transactionIndex ?? this.transactionIndex, + timestamp: timestamp ?? this.timestamp, + from: from ?? this.from, + to: to ?? this.to, + value: value ?? this.value, + gas: gas ?? this.gas, + gasPrice: gasPrice ?? this.gasPrice, + maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, + isError: isError ?? this.isError, + hasToken: hasToken ?? this.hasToken, + gasCost: gasCost ?? this.gasCost, + gasUsed: gasUsed ?? this.gasUsed, + nonce: nonce ?? this.nonce, + ); Map toMap() { final map = {}; @@ -135,6 +139,7 @@ class EthTxDTO { map['hasToken'] = hasToken; map['gasCost'] = gasCost.toString(); map['gasUsed'] = gasUsed.toString(); + map['nonce'] = nonce; return map; } diff --git a/lib/dto/ethereum/pending_eth_tx_dto.dart b/lib/dto/ethereum/pending_eth_tx_dto.dart deleted file mode 100644 index a19a95f61..000000000 --- a/lib/dto/ethereum/pending_eth_tx_dto.dart +++ /dev/null @@ -1,160 +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 - * - */ - -/// blockHash : null -/// blockNumber : null -/// from : "0x..." -/// gas : "0x7e562" -/// maxPriorityFeePerGas : "0x444380" -/// maxFeePerGas : "0x342570c00" -/// hash : "0x...da64e4" -/// input : "....." -/// nonce : "0x70" -/// to : "0x00....." -/// transactionIndex : null -/// value : "0x0" -/// type : "0x2" -/// accessList : [] -/// chainId : "0x1" -/// v : "0x0" -/// r : "0xd..." -/// s : "0x17d...6e6" - -class PendingEthTxDto { - PendingEthTxDto({ - required this.blockHash, - required this.blockNumber, - required this.from, - required this.gas, - required this.maxPriorityFeePerGas, - required this.maxFeePerGas, - required this.hash, - required this.input, - required this.nonce, - required this.to, - required this.transactionIndex, - required this.value, - required this.type, - required this.accessList, - required this.chainId, - required this.v, - required this.r, - required this.s, - }); - - factory PendingEthTxDto.fromMap(Map map) => PendingEthTxDto( - blockHash: map['blockHash'] as String?, - blockNumber: map['blockNumber'] as int?, - from: map['from'] as String, - gas: map['gas'] as String, - maxPriorityFeePerGas: map['maxPriorityFeePerGas'] as String, - maxFeePerGas: map['maxFeePerGas'] as String, - hash: map['hash'] as String, - input: map['input'] as String, - nonce: map['nonce'] as String, - to: map['to'] as String, - transactionIndex: map['transactionIndex'] as int?, - value: map['value'] as String, - type: map['type'] as String, - accessList: map['accessList'] as List? ?? [], - chainId: map['chainId'] as String, - v: map['v'] as String, - r: map['r'] as String, - s: map['s'] as String, - ); - - final String? blockHash; - final int? blockNumber; - final String from; - final String gas; - final String maxPriorityFeePerGas; - final String maxFeePerGas; - final String hash; - final String input; - final String nonce; - final String to; - final int? transactionIndex; - final String value; - final String type; - final List accessList; - final String chainId; - final String v; - final String r; - final String s; - - PendingEthTxDto copyWith({ - String? blockHash, - int? blockNumber, - String? from, - String? gas, - String? maxPriorityFeePerGas, - String? maxFeePerGas, - String? hash, - String? input, - String? nonce, - String? to, - int? transactionIndex, - String? value, - String? type, - List? accessList, - String? chainId, - String? v, - String? r, - String? s, - }) => - PendingEthTxDto( - blockHash: blockHash ?? this.blockHash, - blockNumber: blockNumber ?? this.blockNumber, - from: from ?? this.from, - gas: gas ?? this.gas, - maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, - maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, - hash: hash ?? this.hash, - input: input ?? this.input, - nonce: nonce ?? this.nonce, - to: to ?? this.to, - transactionIndex: transactionIndex ?? this.transactionIndex, - value: value ?? this.value, - type: type ?? this.type, - accessList: accessList ?? this.accessList, - chainId: chainId ?? this.chainId, - v: v ?? this.v, - r: r ?? this.r, - s: s ?? this.s, - ); - - Map toMap() { - final map = {}; - map['blockHash'] = blockHash; - map['blockNumber'] = blockNumber; - map['from'] = from; - map['gas'] = gas; - map['maxPriorityFeePerGas'] = maxPriorityFeePerGas; - map['maxFeePerGas'] = maxFeePerGas; - map['hash'] = hash; - map['input'] = input; - map['nonce'] = nonce; - map['to'] = to; - map['transactionIndex'] = transactionIndex; - map['value'] = value; - map['type'] = type; - map['accessList'] = accessList; - map['chainId'] = chainId; - map['v'] = v; - map['r'] = r; - map['s'] = s; - return map; - } - - @override - String toString() { - return toMap().toString(); - } -} diff --git a/lib/electrumx_rpc/cached_electrumx_client.dart b/lib/electrumx_rpc/cached_electrumx_client.dart index c3b8ab56a..7c23af401 100644 --- a/lib/electrumx_rpc/cached_electrumx_client.dart +++ b/lib/electrumx_rpc/cached_electrumx_client.dart @@ -9,9 +9,6 @@ */ import 'dart:convert'; -import 'dart:math'; - -import 'package:string_validator/string_validator.dart'; import '../db/hive/db.dart'; import '../utilities/logger.dart'; @@ -27,105 +24,17 @@ class CachedElectrumXClient { factory CachedElectrumXClient.from({ required ElectrumXClient electrumXClient, - }) => - CachedElectrumXClient( - electrumXClient: electrumXClient, - ); - - Future> getAnonymitySet({ - required String groupId, - String blockhash = "", - required CryptoCurrency cryptoCurrency, - }) async { - try { - final box = - await DB.instance.getAnonymitySetCacheBox(currency: cryptoCurrency); - final cachedSet = box.get(groupId) as Map?; - - Map set; - - // null check to see if there is a cached set - if (cachedSet == null) { - set = { - "setId": groupId, - "blockHash": blockhash, - "setHash": "", - "coins": [], - }; - } else { - set = Map.from(cachedSet); - } - - final newSet = await electrumXClient.getLelantusAnonymitySet( - groupId: groupId, - blockhash: set["blockHash"] as String, - ); - - // update set with new data - if (newSet["setHash"] != "" && set["setHash"] != newSet["setHash"]) { - set["setHash"] = !isHexadecimal(newSet["setHash"] as String) - ? base64ToHex(newSet["setHash"] as String) - : newSet["setHash"]; - set["blockHash"] = !isHexadecimal(newSet["blockHash"] as String) - ? base64ToReverseHex(newSet["blockHash"] as String) - : newSet["blockHash"]; - for (int i = (newSet["coins"] as List).length - 1; i >= 0; i--) { - final dynamic newCoin = newSet["coins"][i]; - final List translatedCoin = []; - translatedCoin.add( - !isHexadecimal(newCoin[0] as String) - ? base64ToHex(newCoin[0] as String) - : newCoin[0], - ); - translatedCoin.add( - !isHexadecimal(newCoin[1] as String) - ? base64ToReverseHex(newCoin[1] as String) - : newCoin[1], - ); - try { - translatedCoin.add( - !isHexadecimal(newCoin[2] as String) - ? base64ToHex(newCoin[2] as String) - : newCoin[2], - ); - } catch (e) { - translatedCoin.add(newCoin[2]); - } - translatedCoin.add( - !isHexadecimal(newCoin[3] as String) - ? base64ToReverseHex(newCoin[3] as String) - : newCoin[3], - ); - set["coins"].insert(0, translatedCoin); - } - // save set to db - await box.put(groupId, set); - Logging.instance.d( - "Updated current anonymity set for ${cryptoCurrency.identifier} with group ID $groupId", - ); - } - - return set; - } catch (e, s) { - Logging.instance.e( - "Failed to process CachedElectrumX.getAnonymitySet(): ", - error: e, - stackTrace: s, - ); - rethrow; - } - } + }) => CachedElectrumXClient(electrumXClient: electrumXClient); String base64ToHex(String source) => - base64Decode(LineSplitter.split(source).join()) - .map((e) => e.toRadixString(16).padLeft(2, '0')) - .join(); + base64Decode( + LineSplitter.split(source).join(), + ).map((e) => e.toRadixString(16).padLeft(2, '0')).join(); String base64ToReverseHex(String source) => - base64Decode(LineSplitter.split(source).join()) - .reversed - .map((e) => e.toRadixString(16).padLeft(2, '0')) - .join(); + base64Decode( + LineSplitter.split(source).join(), + ).reversed.map((e) => e.toRadixString(16).padLeft(2, '0')).join(); /// Call electrumx getTransaction on a per coin basis, storing the result in local db if not already there. /// @@ -140,11 +49,8 @@ class CachedElectrumXClient { final cachedTx = box.get(txHash) as Map?; if (cachedTx == null) { - final Map result = - await electrumXClient.getTransaction( - txHash: txHash, - verbose: verbose, - ); + final Map result = await electrumXClient + .getTransaction(txHash: txHash, verbose: verbose); result.remove("hex"); result.remove("lelantusData"); @@ -171,57 +77,6 @@ class CachedElectrumXClient { } } - Future> getUsedCoinSerials({ - required CryptoCurrency cryptoCurrency, - int startNumber = 0, - }) async { - try { - final box = - await DB.instance.getUsedSerialsCacheBox(currency: cryptoCurrency); - - final _list = box.get("serials") as List?; - - final Set cachedSerials = - _list == null ? {} : List.from(_list).toSet(); - - startNumber = max( - max(0, startNumber), - cachedSerials.length - 100, // 100 being some arbitrary buffer - ); - - final serials = await electrumXClient.getLelantusUsedCoinSerials( - startNumber: startNumber, - ); - - final newSerials = List.from(serials["serials"] as List) - .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e) - .toSet(); - - // ensure we are getting some overlap so we know we are not missing any - if (cachedSerials.isNotEmpty && newSerials.isNotEmpty) { - assert(cachedSerials.intersection(newSerials).isNotEmpty); - } - - cachedSerials.addAll(newSerials); - - final resultingList = cachedSerials.toList(); - - await box.put( - "serials", - resultingList, - ); - - return resultingList; - } catch (e, s) { - Logging.instance.e( - "Failed to process CachedElectrumX.getUsedCoinSerials(): ", - error: e, - stackTrace: s, - ); - rethrow; - } - } - /// Clear all cached transactions for the specified coin Future clearSharedTransactionCache({ required CryptoCurrency cryptoCurrency, diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 2691adf5a..befe70fc3 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -93,11 +93,8 @@ class ElectrumXClient { // StreamChannel? get electrumAdapterChannel => _electrumAdapterChannel; StreamChannel? _electrumAdapterChannel; - ElectrumClient? getElectrumAdapter() => - ClientManager.sharedInstance.getClient( - cryptoCurrency: cryptoCurrency, - netType: netType, - ); + ElectrumClient? getElectrumAdapter() => ClientManager.sharedInstance + .getClient(cryptoCurrency: cryptoCurrency, netType: netType); late Prefs _prefs; late TorService _torService; @@ -109,12 +106,10 @@ class ElectrumXClient { // add finalizer to cancel stream subscription when all references to an // instance of ElectrumX becomes inaccessible - static final Finalizer _finalizer = Finalizer( - (p0) { - p0._torPreferenceListener?.cancel(); - p0._torStatusListener?.cancel(); - }, - ); + static final Finalizer _finalizer = Finalizer((p0) { + p0._torPreferenceListener?.cancel(); + p0._torStatusListener?.cancel(); + }); StreamSubscription? _torPreferenceListener; StreamSubscription? _torStatusListener; @@ -129,8 +124,9 @@ class ElectrumXClient { required this.netType, required List failovers, required this.cryptoCurrency, - this.connectionTimeoutForSpecialCaseJsonRPCClients = - const Duration(seconds: 60), + this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration( + seconds: 60, + ), TorService? torService, EventBus? globalEventBusForTesting, }) { @@ -144,46 +140,45 @@ class ElectrumXClient { final bus = globalEventBusForTesting ?? GlobalEventBus.instance; // Listen for tor status changes. - _torStatusListener = bus.on().listen( - (event) async { - switch (event.newStatus) { - case TorConnectionStatus.connecting: - await _torConnectingLock.acquire(); - _requireMutex = true; - break; - - case TorConnectionStatus.connected: - case TorConnectionStatus.disconnected: - if (_torConnectingLock.isLocked) { - _torConnectingLock.release(); - } - _requireMutex = false; - break; - } - }, - ); + _torStatusListener = bus.on().listen(( + event, + ) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + 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 { - // not sure if we need to do anything specific here - // switch (event.status) { - // case TorStatus.enabled: - // case TorStatus.disabled: - // } - - // setting to null should force the creation of a new json rpc client - // on the next request sent through this electrumx instance - _electrumAdapterChannel = null; - await (await ClientManager.sharedInstance - .remove(cryptoCurrency: cryptoCurrency)) - .$1 - ?.close(); - - // Also close any chain height services that are currently open. - // await ChainHeightServiceManager.dispose(); - }, - ); + _torPreferenceListener = bus.on().listen(( + event, + ) async { + // not sure if we need to do anything specific here + // switch (event.status) { + // case TorStatus.enabled: + // case TorStatus.disabled: + // } + + // setting to null should force the creation of a new json rpc client + // on the next request sent through this electrumx instance + _electrumAdapterChannel = null; + await (await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + )).$1?.close(); + + // Also close any chain height services that are currently open. + // await ChainHeightServiceManager.dispose(); + }); } factory ElectrumXClient.from({ @@ -252,14 +247,16 @@ class ElectrumXClient { if (netType == TorPlainNetworkOption.clear) { _electrumAdapterChannel = null; - await ClientManager.sharedInstance - .remove(cryptoCurrency: cryptoCurrency); + await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + ); } } else { if (netType == TorPlainNetworkOption.tor) { _electrumAdapterChannel = null; - await ClientManager.sharedInstance - .remove(cryptoCurrency: cryptoCurrency); + await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + ); } } @@ -338,24 +335,22 @@ class ElectrumXClient { } if (_requireMutex) { - await _torConnectingLock - .protect(() async => await checkElectrumAdapter()); + await _torConnectingLock.protect( + () async => await checkElectrumAdapter(), + ); } else { await checkElectrumAdapter(); } try { - final response = await getElectrumAdapter()!.request( - command, - args, - ); + final response = await getElectrumAdapter()!.request(command, args); if (response is Map && response.keys.contains("error") && response["error"] != null) { - if (response["error"] - .toString() - .contains("No such mempool or blockchain transaction")) { + if (response["error"].toString().contains( + "No such mempool or blockchain transaction", + )) { throw NoSuchTransactionException( "No such mempool or blockchain transaction", args.first.toString(), @@ -399,11 +394,7 @@ class ElectrumXClient { } } catch (e, s) { final errorMessage = e.toString(); - Logging.instance.w( - "$host $e", - error: e, - stackTrace: s, - ); + Logging.instance.w("$host $e", error: e, stackTrace: s); if (errorMessage.contains("JSON-RPC error")) { currentFailoverIndex = _failovers.length; } @@ -437,8 +428,9 @@ class ElectrumXClient { } if (_requireMutex) { - await _torConnectingLock - .protect(() async => await checkElectrumAdapter()); + await _torConnectingLock.protect( + () async => await checkElectrumAdapter(), + ); } else { await checkElectrumAdapter(); } @@ -531,18 +523,19 @@ class ElectrumXClient { // electrum_adapter returns the result of the request, request() has been // updated to return a bool on a server.ping command as a special case. return await request( - requestID: requestID, - command: 'server.ping', - requestTimeout: const Duration(seconds: 30), - retries: retryCount, - ).timeout( - const Duration(seconds: 30), - onTimeout: () { - Logging.instance.d( - "ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host", - ); - }, - ) as bool; + requestID: requestID, + command: 'server.ping', + requestTimeout: const Duration(seconds: 30), + retries: retryCount, + ).timeout( + const Duration(seconds: 30), + onTimeout: () { + Logging.instance.d( + "ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host", + ); + }, + ) + as bool; } catch (e) { rethrow; } @@ -609,9 +602,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: 'blockchain.transaction.broadcast', - args: [ - rawTx, - ], + args: [rawTx], ); return response as String; } catch (e) { @@ -636,9 +627,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: 'blockchain.scripthash.get_balance', - args: [ - scripthash, - ], + args: [scripthash], ); return Map.from(response as Map); } catch (e) { @@ -673,9 +662,7 @@ class ElectrumXClient { requestID: requestID, command: 'blockchain.scripthash.get_history', requestTimeout: const Duration(minutes: 5), - args: [ - scripthash, - ], + args: [scripthash], ); result = response; retryCount--; @@ -731,9 +718,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: 'blockchain.scripthash.listunspent', - args: [ - scripthash, - ], + args: [scripthash], ); return List>.from(response as List); } catch (e) { @@ -826,14 +811,13 @@ class ElectrumXClient { bool verbose = true, String? requestID, }) async { - Logging.instance.d( - "attempting to fetch blockchain.transaction.get...", - ); + Logging.instance.d("attempting to fetch blockchain.transaction.get..."); await checkElectrumAdapter(); - final dynamic response = await getElectrumAdapter()!.getTransaction(txHash); - Logging.instance.d( - "Fetching blockchain.transaction.get finished", + final dynamic response = await getElectrumAdapter()!.request( + 'blockchain.transaction.get', + [txHash, verbose], ); + Logging.instance.d("Fetching blockchain.transaction.get finished"); if (!verbose) { return {"rawtx": response as String}; @@ -861,16 +845,12 @@ class ElectrumXClient { String blockhash = "", String? requestID, }) async { - Logging.instance.d( - "attempting to fetch lelantus.getanonymityset...", - ); + Logging.instance.d("attempting to fetch lelantus.getanonymityset..."); await checkElectrumAdapter(); - final Map response = - await (getElectrumAdapter() as FiroElectrumClient) - .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); - Logging.instance.d( - "Fetching lelantus.getanonymityset finished", - ); + final Map response = await (getElectrumAdapter() + as FiroElectrumClient) + .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); + Logging.instance.d("Fetching lelantus.getanonymityset finished"); return response; } @@ -882,15 +862,11 @@ class ElectrumXClient { dynamic mints, String? requestID, }) async { - Logging.instance.d( - "attempting to fetch lelantus.getmintmetadata...", - ); + Logging.instance.d("attempting to fetch lelantus.getmintmetadata..."); await checkElectrumAdapter(); final dynamic response = await (getElectrumAdapter() as FiroElectrumClient) .getLelantusMintData(mints: mints); - Logging.instance.d( - "Fetching lelantus.getmintmetadata finished", - ); + Logging.instance.d("Fetching lelantus.getmintmetadata finished"); return response; } @@ -900,9 +876,7 @@ class ElectrumXClient { String? requestID, required int startNumber, }) async { - Logging.instance.d( - "attempting to fetch lelantus.getusedcoinserials...", - ); + Logging.instance.d("attempting to fetch lelantus.getusedcoinserials..."); await checkElectrumAdapter(); int retryCount = 3; @@ -912,9 +886,7 @@ class ElectrumXClient { response = await (getElectrumAdapter() as FiroElectrumClient) .getLelantusUsedCoinSerials(startNumber: startNumber); // TODO add 2 minute timeout. - Logging.instance.d( - "Fetching lelantus.getusedcoinserials finished", - ); + Logging.instance.d("Fetching lelantus.getusedcoinserials finished"); retryCount--; } @@ -926,15 +898,11 @@ class ElectrumXClient { /// /// ex: 1 Future getLelantusLatestCoinId({String? requestID}) async { - Logging.instance.d( - "attempting to fetch lelantus.getlatestcoinid...", - ); + Logging.instance.d("attempting to fetch lelantus.getlatestcoinid..."); await checkElectrumAdapter(); final int response = await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId(); - Logging.instance.d( - "Fetching lelantus.getlatestcoinid finished", - ); + Logging.instance.d("Fetching lelantus.getlatestcoinid finished"); return response; } @@ -961,12 +929,12 @@ class ElectrumXClient { try { final start = DateTime.now(); await checkElectrumAdapter(); - final Map response = - await (getElectrumAdapter() as FiroElectrumClient) - .getSparkAnonymitySet( - coinGroupId: coinGroupId, - startBlockHash: startBlockHash, - ); + final Map response = await (getElectrumAdapter() + as FiroElectrumClient) + .getSparkAnonymitySet( + coinGroupId: coinGroupId, + startBlockHash: startBlockHash, + ); Logging.instance.d( "Finished ElectrumXClient.getSparkAnonymitySet(coinGroupId" "=$coinGroupId, startBlockHash=$startBlockHash). " @@ -1053,34 +1021,23 @@ class ElectrumXClient { /// Returns the latest Spark set id /// /// ex: 1 - Future getSparkLatestCoinId({ - String? requestID, - }) async { + Future getSparkLatestCoinId({String? requestID}) async { try { - Logging.instance.d( - "attempting to fetch spark.getsparklatestcoinid...", - ); + Logging.instance.d("attempting to fetch spark.getsparklatestcoinid..."); await checkElectrumAdapter(); - final int response = await (getElectrumAdapter() as FiroElectrumClient) - .getSparkLatestCoinId(); - Logging.instance.d( - "Fetching spark.getsparklatestcoinid finished", - ); + final int response = + await (getElectrumAdapter() as FiroElectrumClient) + .getSparkLatestCoinId(); + Logging.instance.d("Fetching spark.getsparklatestcoinid finished"); return response; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } - /// Returns the txids of the current transactions found in the mempool - Future> getMempoolTxids({ - String? requestID, - }) async { + /// Returns the txids of the current spark transactions found in the mempool + Future> getMempoolTxids({String? requestID}) async { try { final start = DateTime.now(); final response = await request( @@ -1088,9 +1045,10 @@ class ElectrumXClient { command: "spark.getmempoolsparktxids", ); - final txids = List.from(response as List) - .map((e) => e.toHexReversedFromBase64) - .toSet(); + final txids = + List.from( + response as List, + ).map((e) => e.toHexReversedFromBase64).toSet(); Logging.instance.d( "Finished ElectrumXClient.getMempoolTxids(). " @@ -1099,11 +1057,7 @@ class ElectrumXClient { return txids; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } @@ -1119,9 +1073,7 @@ class ElectrumXClient { requestID: requestID, command: "spark.getmempoolsparktxs", args: [ - { - "txids": txids, - }, + {"txids": txids}, ], ); @@ -1131,11 +1083,13 @@ class ElectrumXClient { result.add( SparkMempoolData( txid: entry.key, - serialContext: - List.from(entry.value["serial_context"] as List), + serialContext: List.from( + entry.value["serial_context"] as List, + ), // the space after lTags is required lol lTags: List.from(entry.value["lTags "] as List), coins: List.from(entry.value["coins"] as List), + isLocked: entry.value["isLocked"] as bool, ), ); } @@ -1163,9 +1117,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: "spark.getusedcoinstagstxhashes", - args: [ - "$startNumber", - ], + args: ["$startNumber"], ); final map = Map.from(response as Map); @@ -1179,14 +1131,85 @@ class ElectrumXClient { return tags; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, + Logging.instance.e(e, error: e, stackTrace: s); + rethrow; + } + } + + Future> getSparkNames({ + String? requestID, + }) async { + try { + final start = DateTime.now(); + await checkElectrumAdapter(); + const command = "spark.getsparknames"; + Logging.instance.d( + "[${getElectrumAdapter()?.host}] => attempting to fetch $command...", ); + + final response = await request(requestID: requestID, command: command); + + if (response is List) { + Logging.instance.d( + "Finished ElectrumXClient.getSparkNames(). " + "names.length: ${response.length}" + "Duration=${DateTime.now().difference(start)}", + ); + + return response + .map( + (e) => ( + name: e["name"] as String, + address: e["address"] as String, + ), + ) + .toList(); + } else if (response["error"] != null) { + Logging.instance.d(response); + throw Exception(response["error"].toString()); + } else { + throw Exception("Failed to parse getSparkNames response: $response"); + } + } catch (e) { rethrow; } } + + Future<({String address, int validUntil, String additionalInfo})> + getSparkNameData({required String sparkName, String? requestID}) async { + try { + final start = DateTime.now(); + await checkElectrumAdapter(); + const command = "spark.getsparknamedata"; + Logging.instance.d( + "[${getElectrumAdapter()?.host}] => attempting to fetch $command...", + ); + + final response = await request( + requestID: requestID, + command: command, + args: [sparkName], + ); + + Logging.instance.d( + "Finished ElectrumXClient.getSparkNameData(). " + "Duration=${DateTime.now().difference(start)}", + ); + if (response["error"] != null) { + Logging.instance.d(response); + throw Exception(response["error"].toString()); + } + + return ( + address: response["address"] as String, + validUntil: response["validUntil"] as int, + additionalInfo: response["additionalInfo"] as String, + ); + } catch (e) { + rethrow; + } + } + // ======== New Paginated Endpoints ========================================== Future getSparkAnonymitySetMeta({ @@ -1203,9 +1226,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: command, - args: [ - "$coinGroupId", - ], + args: ["$coinGroupId"], ); final map = Map.from(response as Map); @@ -1227,11 +1248,7 @@ class ElectrumXClient { return result; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } @@ -1250,12 +1267,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: command, - args: [ - "$coinGroupId", - latestBlock, - "$startIndex", - "$endIndex", - ], + args: ["$coinGroupId", latestBlock, "$startIndex", "$endIndex"], ); final map = Map.from(response as Map); @@ -1275,11 +1287,7 @@ class ElectrumXClient { return result; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } @@ -1296,10 +1304,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: "blockchain.checkifmncollateral", - args: [ - txid, - index.toString(), - ], + args: [txid, index.toString()], ); Logging.instance.d( @@ -1310,11 +1315,7 @@ class ElectrumXClient { return response as bool; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } @@ -1344,9 +1345,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: 'blockchain.estimatefee', - args: [ - blocks, - ], + args: [blocks], ); try { if (response == null || @@ -1371,7 +1370,8 @@ class ElectrumXClient { } return Decimal.parse(response.toString()); } catch (e, s) { - final String msg = "Error parsing fee rate. Response: $response" + final String msg = + "Error parsing fee rate. Response: $response" "\nResult: $response\nError: $e\nStack trace: $s"; Logging.instance.e(msg, error: e, stackTrace: s); throw Exception(msg); diff --git a/lib/main.dart b/lib/main.dart index ccf2ecf6e..0eb4c224c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'dart:ui'; import 'package:coinlib_flutter/coinlib_flutter.dart'; import 'package:compat/compat.dart' as lib_monero_compat; @@ -50,7 +51,6 @@ import 'pages/pinpad_views/create_pin_view.dart'; import 'pages/pinpad_views/lock_screen_view.dart'; import 'pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart'; import 'pages_desktop_specific/password/desktop_login_view.dart'; -import 'providers/db/main_db_provider.dart'; import 'providers/desktop/storage_crypto_handler_provider.dart'; import 'providers/global/auto_swb_service_provider.dart'; import 'providers/global/base_currencies_provider.dart'; @@ -60,6 +60,7 @@ import 'providers/providers.dart'; import 'route_generator.dart'; import 'services/exchange/exchange_data_loading_service.dart'; import 'services/locale_service.dart'; +import 'services/mwebd_service.dart'; import 'services/node_service.dart'; import 'services/notifications_api.dart'; import 'services/notifications_service.dart'; @@ -74,6 +75,7 @@ import 'utilities/logger.dart'; import 'utilities/prefs.dart'; import 'utilities/stack_file_system.dart'; import 'utilities/util.dart'; +import 'wallets/crypto_currency/crypto_currency.dart'; import 'wallets/isar/providers/all_wallets_info_provider.dart'; import 'wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'widgets/crypto_notifications.dart'; @@ -200,6 +202,7 @@ void main(List args) async { await Logging.instance.initialize( (await StackFileSystem.applicationLogsDirectory(Prefs.instance)).path, level: Prefs.instance.logLevel, + debugConsoleLevel: kDebugMode ? Level.trace : null, ); await xelis_api.setUpRustLogger(); @@ -214,6 +217,25 @@ void main(List args) async { await CampfireMigration.init(); } + if (kDebugMode) { + unawaited( + MwebdService.instance + .logsStream(CryptoCurrencyNetwork.main) + .then( + (stream) => + stream.listen((line) => print("[MWEBD: MAINNET]: $line")), + ), + ); + unawaited( + MwebdService.instance + .logsStream(CryptoCurrencyNetwork.test) + .then( + (stream) => + stream.listen((line) => print("[MWEBD: TESTNET]: $line")), + ), + ); + } + // TODO: // This should be moved to happen during the loading animation instead of // showing a blank screen for 4-10 seconds. @@ -356,7 +378,7 @@ class _MaterialAppWithThemeState extends ConsumerState } } - Future load() async { + Future load(bool loadWallets) async { try { if (didLoad) { return; @@ -386,12 +408,15 @@ class _MaterialAppWithThemeState extends ConsumerState prefs: ref.read(prefsChangeNotifierProvider), ); ref.read(priceAnd24hChangeNotifierProvider).start(true); - await ref - .read(pWallets) - .load( - ref.read(prefsChangeNotifierProvider), - ref.read(mainDBProvider), - ); + if (loadWallets) { + await ref + .read(pWallets) + .load( + ref.read(prefsChangeNotifierProvider), + ref.read(mainDBProvider), + false, + ); + } loadingCompleter.complete(); // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet // unawaited(_nodeService.updateCommunityNodes()); @@ -444,6 +469,13 @@ class _MaterialAppWithThemeState extends ConsumerState @override void initState() { + if (Util.isDesktop) { + // set to false for desktop + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(pDuress.notifier).state = false; + }); + } + String themeId; if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) { final brightness = WidgetsBinding.instance.window.platformBrightness; @@ -590,6 +622,21 @@ class _MaterialAppWithThemeState extends ConsumerState } } + @override + Future didRequestAppExit() async { + debugPrint("didRequestAppExit called"); + if (Platform.isMacOS) { + // On macOS, mwebd fails to shut down, hanging the app on close. + // + // Exiting is a hack fix for this issue. + + // await ref.read(pMwebService).shutdown(); + // Something like the above would probably be prudent to make. + exit(0); + } + return AppExitResponse.exit; + } + /// should only be called on android currently Future getOpenFile() async { // update provider with new file content state @@ -780,7 +827,7 @@ class _MaterialAppWithThemeState extends ConsumerState return DesktopLoginView( startupWalletId: startupWalletId, - load: load, + load: () => load(true), ); } else { return const IntroView(); @@ -791,7 +838,7 @@ class _MaterialAppWithThemeState extends ConsumerState }, ) : FutureBuilder( - future: load(), + future: load(false), builder: ( BuildContext context, AsyncSnapshot snapshot, diff --git a/lib/models/electrumx_response/spark_models.dart b/lib/models/electrumx_response/spark_models.dart index 43bf9f61f..22c6cf25f 100644 --- a/lib/models/electrumx_response/spark_models.dart +++ b/lib/models/electrumx_response/spark_models.dart @@ -3,12 +3,14 @@ class SparkMempoolData { final List serialContext; final List lTags; final List coins; + final bool isLocked; SparkMempoolData({ required this.txid, required this.serialContext, required this.lTags, required this.coins, + required this.isLocked, }); @override @@ -17,7 +19,8 @@ class SparkMempoolData { "txid: $txid, " "serialContext: $serialContext, " "lTags: $lTags, " - "coins: $coins" + "coins: $coins, " + "isLocked: $isLocked" "}"; } } diff --git a/lib/models/exchange/aggregate_currency.dart b/lib/models/exchange/aggregate_currency.dart index 841262c1d..b2fa09f30 100644 --- a/lib/models/exchange/aggregate_currency.dart +++ b/lib/models/exchange/aggregate_currency.dart @@ -8,12 +8,14 @@ * */ +import 'package:tuple/tuple.dart'; + +import '../../services/exchange/trocador/trocador_exchange.dart'; import '../isar/exchange_cache/currency.dart'; import '../isar/exchange_cache/pair.dart'; -import 'package:tuple/tuple.dart'; class AggregateCurrency { - final Map _map = {}; + final Map _map = {}; AggregateCurrency({ required List> exchangeCurrencyPairs, @@ -29,23 +31,50 @@ class AggregateCurrency { return _map[exchangeName]; } - String get ticker => _map.values.first!.ticker; + String? networkFor(String exchangeName) => forExchange(exchangeName)?.network; + + String get ticker => _map.values.first.ticker; - String get name => _map.values.first!.name; + String get name { + if (_map.values.length > 2) { + return _map.values + .firstWhere((e) => e.exchangeName != TrocadorExchange.exchangeName) + .name; + } + + // trocador hack + return _map.values.first.name.split(" (Mainnet").first; + } - String get image => _map.values.first!.image; + String get image => _map.values.first.image; - SupportedRateType get rateType => _map.values.first!.rateType; + SupportedRateType get rateType => _map.values.first.rateType; - bool get isStackCoin => _map.values.first!.isStackCoin; + bool get isStackCoin => _map.values.first.isStackCoin; + + String get fuzzyNet => _map.values.first.getFuzzyNet(); @override String toString() { String str = "AggregateCurrency: {"; for (final key in _map.keys) { - str += " $key: ${_map[key]},"; + str += "\n $key: ${_map[key]},"; } - str += " }"; + str += "\n}"; return str; } + + @override + bool operator ==(Object other) { + return other is AggregateCurrency && + other.ticker == ticker && + other._map.isNotEmpty && + other._map.length == _map.length && + other._map.values.first.getFuzzyNet() == + _map.values.first.getFuzzyNet(); + } + + @override + int get hashCode => + Object.hash(ticker, _map.values.first.getFuzzyNet(), _map.length); } diff --git a/lib/models/exchange/change_now/cn_exchange_transaction.dart b/lib/models/exchange/change_now/cn_exchange_transaction.dart new file mode 100644 index 000000000..bd904f8f5 --- /dev/null +++ b/lib/models/exchange/change_now/cn_exchange_transaction.dart @@ -0,0 +1,174 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-04-26 + * + */ + +import 'package:decimal/decimal.dart'; +import 'package:uuid/uuid.dart'; + +import 'cn_exchange_transaction_status.dart'; + +class CNExchangeTransaction { + /// The amount being sent from the user. + final Decimal fromAmount; + + /// The amount the user will receive. + final Decimal toAmount; + + /// The type of exchange flow. Either "standard" or "fixed-rate". + final String flow; + + /// Direction of the exchange: "direct" or "reverse". + final String type; + + /// The address to which the user sends the input currency. + final String payinAddress; + + /// The address where the exchanged currency will be sent. + final String payoutAddress; + + /// Extra ID for payout address (e.g., memo, tag). Empty string if not used. + final String payoutExtraId; + + /// Currency ticker being exchanged from (e.g., "btc"). + final String fromCurrency; + + /// Currency ticker being exchanged to (e.g., "xmr"). + final String toCurrency; + + /// Refund address in case of failure or timeout. + final String refundAddress; + + /// Extra ID for the refund address (if needed). Empty string if not used. + final String refundExtraId; + + /// Deadline until which the estimated rate or transaction is valid. + final DateTime? validUntil; + + /// Date when transaction was created. + final DateTime date; + + /// Unique transaction identifier. + final String id; + + /// The user-defined or system-determined amount in a "directed" flow. + final Decimal? directedAmount; + + /// Network of the currency being sent. + final String fromNetwork; + + /// Network of the currency being received. + final String toNetwork; + + final String uuid; + + final CNExchangeTransactionStatus? statusObject; + + const CNExchangeTransaction({ + required this.fromAmount, + required this.toAmount, + required this.flow, + required this.type, + required this.payinAddress, + required this.payoutAddress, + required this.payoutExtraId, + required this.fromCurrency, + required this.toCurrency, + required this.refundAddress, + required this.refundExtraId, + required this.validUntil, + required this.date, + required this.id, + required this.directedAmount, + required this.fromNetwork, + required this.toNetwork, + required this.uuid, + this.statusObject, + }); + + factory CNExchangeTransaction.fromJson(Map json) { + return CNExchangeTransaction( + fromAmount: Decimal.parse(json["fromAmount"].toString()), + toAmount: Decimal.parse(json["toAmount"].toString()), + flow: json["flow"] as String, + type: json["type"] as String, + payinAddress: json["payinAddress"] as String, + payoutAddress: json["payoutAddress"] as String, + payoutExtraId: json["payoutExtraId"] as String? ?? "", + fromCurrency: json["fromCurrency"] as String, + toCurrency: json["toCurrency"] as String, + refundAddress: json["refundAddress"] as String, + refundExtraId: json["refundExtraId"] as String, + validUntil: DateTime.tryParse(json["validUntil"] as String? ?? ""), + date: DateTime.parse(json["date"] as String), + id: json["id"] as String, + directedAmount: Decimal.tryParse(json["directedAmount"].toString()), + fromNetwork: json["fromNetwork"] as String? ?? "", + toNetwork: json["toNetwork"] as String? ?? "", + uuid: json["uuid"] as String? ?? const Uuid().v1(), + statusObject: + json["statusObject"] is Map + ? CNExchangeTransactionStatus.fromMap( + json["statusObject"] as Map, + ) + : null, + ); + } + + Map toMap() { + return { + "fromAmount": fromAmount, + "toAmount": toAmount, + "flow": flow, + "type": type, + "payinAddress": payinAddress, + "payoutAddress": payoutAddress, + "payoutExtraId": payoutExtraId, + "fromCurrency": fromCurrency, + "toCurrency": toCurrency, + "refundAddress": refundAddress, + "refundExtraId": refundExtraId, + "validUntil": validUntil?.toIso8601String(), + "date": date.toIso8601String(), + "id": id, + "directedAmount": directedAmount, + "fromNetwork": fromNetwork, + "uuid": uuid, + "statusObject": statusObject?.toMap(), + }; + } + + CNExchangeTransaction copyWithStatus(CNExchangeTransactionStatus? status) { + return CNExchangeTransaction( + fromAmount: fromAmount, + toAmount: toAmount, + flow: flow, + type: type, + payinAddress: payinAddress, + payoutAddress: payoutAddress, + payoutExtraId: payoutExtraId, + fromCurrency: fromCurrency, + toCurrency: toCurrency, + refundAddress: refundAddress, + refundExtraId: refundExtraId, + validUntil: validUntil, + date: date, + id: id, + directedAmount: directedAmount, + fromNetwork: fromNetwork, + toNetwork: toNetwork, + uuid: uuid, + statusObject: status, + ); + } + + @override + String toString() { + return "CNExchangeTransaction: ${toMap()}"; + } +} diff --git a/lib/models/exchange/change_now/cn_exchange_transaction_status.dart b/lib/models/exchange/change_now/cn_exchange_transaction_status.dart new file mode 100644 index 000000000..14d7c03e5 --- /dev/null +++ b/lib/models/exchange/change_now/cn_exchange_transaction_status.dart @@ -0,0 +1,172 @@ +import 'package:decimal/decimal.dart'; + +enum ChangeNowTransactionStatus { + New, + Waiting, + Confirming, + Exchanging, + Sending, + Finished, + Failed, + Refunded, + Verifying, +} + +extension ChangeNowTransactionStatusExt on ChangeNowTransactionStatus { + String get lowerCaseName => name.toLowerCase(); +} + +ChangeNowTransactionStatus changeNowTransactionStatusFromStringIgnoreCase( + String string, +) { + for (final value in ChangeNowTransactionStatus.values) { + if (value.lowerCaseName == string.toLowerCase()) { + return value; + } + } + throw ArgumentError( + "String value does not match any known ChangeNowTransactionStatus", + ); +} + +class CNExchangeTransactionStatus { + final String id; + final ChangeNowTransactionStatus status; + final bool actionsAvailable; + final String fromCurrency; + final String fromNetwork; + final String toCurrency; + final String toNetwork; + final String? expectedAmountFrom; + final String? expectedAmountTo; + final String? amountFrom; + final String? amountTo; + final String payinAddress; + final String payoutAddress; + final String? payinExtraId; + final String? payoutExtraId; + final String? refundAddress; + final String? refundExtraId; + final String createdAt; + final String updatedAt; + final String? depositReceivedAt; + final String? payinHash; + final String? payoutHash; + final String fromLegacyTicker; + final String toLegacyTicker; + final String? refundHash; + final String? refundAmount; + final int? userId; + final String? validUntil; + + const CNExchangeTransactionStatus({ + required this.id, + required this.status, + required this.actionsAvailable, + required this.fromCurrency, + required this.fromNetwork, + required this.toCurrency, + required this.toNetwork, + this.expectedAmountFrom, + this.expectedAmountTo, + this.amountFrom, + this.amountTo, + required this.payinAddress, + required this.payoutAddress, + this.payinExtraId, + this.payoutExtraId, + this.refundAddress, + this.refundExtraId, + required this.createdAt, + required this.updatedAt, + this.depositReceivedAt, + this.payinHash, + this.payoutHash, + required this.fromLegacyTicker, + required this.toLegacyTicker, + this.refundHash, + this.refundAmount, + this.userId, + this.validUntil, + }); + + factory CNExchangeTransactionStatus.fromMap(Map map) { + return CNExchangeTransactionStatus( + id: map["id"] as String, + status: changeNowTransactionStatusFromStringIgnoreCase( + map["status"] as String, + ), + actionsAvailable: map["actionsAvailable"] as bool, + fromCurrency: map["fromCurrency"] as String? ?? "", + fromNetwork: map["fromNetwork"] as String? ?? "", + toCurrency: map["toCurrency"] as String? ?? "", + toNetwork: map["toNetwork"] as String? ?? "", + expectedAmountFrom: _get(map["expectedAmountFrom"]), + expectedAmountTo: _get(map["expectedAmountTo"]), + amountFrom: _get(map["amountFrom"]), + amountTo: _get(map["amountTo"]), + payinAddress: map["payinAddress"] as String? ?? "", + payoutAddress: map["payoutAddress"] as String? ?? "", + payinExtraId: map["payinExtraId"] as String?, + payoutExtraId: map["payoutExtraId"] as String?, + refundAddress: map["refundAddress"] as String?, + refundExtraId: map["refundExtraId"] as String?, + createdAt: map["createdAt"] as String? ?? "", + updatedAt: map["updatedAt"] as String? ?? "", + depositReceivedAt: map["depositReceivedAt"] as String?, + payinHash: map["payinHash"] as String?, + payoutHash: map["payoutHash"] as String?, + fromLegacyTicker: map["fromLegacyTicker"] as String? ?? "", + toLegacyTicker: map["toLegacyTicker"] as String? ?? "", + refundHash: map["refundHash"] as String?, + refundAmount: _get(map["refundAmount"]), + userId: + map["userId"] is int + ? map["userId"] as int + : int.tryParse(map["userId"].toString()), + validUntil: map["validUntil"] as String?, + ); + } + + Map toMap() { + return { + "id": id, + "status": status, + "actionsAvailable": actionsAvailable, + "fromCurrency": fromCurrency, + "fromNetwork": fromNetwork, + "toCurrency": toCurrency, + "toNetwork": toNetwork, + "expectedAmountFrom": expectedAmountFrom, + "expectedAmountTo": expectedAmountTo, + "amountFrom": amountFrom, + "amountTo": amountTo, + "payinAddress": payinAddress, + "payoutAddress": payoutAddress, + "payinExtraId": payinExtraId, + "payoutExtraId": payoutExtraId, + "refundAddress": refundAddress, + "refundExtraId": refundExtraId, + "createdAt": createdAt, + "updatedAt": updatedAt, + "depositReceivedAt": depositReceivedAt, + "payinHash": payinHash, + "payoutHash": payoutHash, + "fromLegacyTicker": fromLegacyTicker, + "toLegacyTicker": toLegacyTicker, + "refundHash": refundHash, + "refundAmount": refundAmount, + "userId": userId, + "validUntil": validUntil, + }; + } + + static String? _get(dynamic value) { + if (value is String) return value; + if (value is num) return Decimal.tryParse(value.toString())?.toString(); + return null; + } + + @override + String toString() => "CNExchangeTransactionStatus: ${toMap()}"; +} diff --git a/lib/models/exchange/change_now/estimated_exchange_amount.dart b/lib/models/exchange/change_now/estimated_exchange_amount.dart index 62eee2f60..b36efe480 100644 --- a/lib/models/exchange/change_now/estimated_exchange_amount.dart +++ b/lib/models/exchange/change_now/estimated_exchange_amount.dart @@ -1,96 +1,142 @@ -/* - * 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:decimal/decimal.dart"; -import 'package:decimal/decimal.dart'; - -import '../../../utilities/logger.dart'; +import "../../../services/exchange/change_now/change_now_api.dart"; +/// Immutable model representing exchange rate information. class EstimatedExchangeAmount { - /// Estimated exchange amount - final Decimal estimatedAmount; + /// Ticker of the currency you want to exchange. + final String fromCurrency; - /// Dash-separated min and max estimated time in minutes - final String transactionSpeedForecast; + /// Network of the currency you want to exchange. + final String fromNetwork; - /// Some warnings like warnings that transactions on this network - /// take longer or that the currency has moved to another network - final String? warningMessage; + /// Ticker of the currency you want to receive. + final String toCurrency; + + /// Network of the currency you want to receive. + final String toNetwork; - /// (Optional) Use rateId for fixed-rate flow. If this field is true, you - /// could use returned field "rateId" in next method for creating transaction - /// to freeze estimated amount that you got in this method. Current estimated - /// amount would be valid until time in field "validUntil" + /// Type of exchange flow. Either `standard` or `fixed-rate`. + final CNFlow flow; + + /// Direction of exchange flow. Either `direct` or `reverse`. + /// + /// - `direct`: set amount for `fromCurrency`, get amount of `toCurrency`. + /// - `reverse`: set amount for `toCurrency`, get amount of `fromCurrency`. + final CNExchangeType type; + + /// RateId is needed for fixed-rate flow. Used to freeze estimated amount. final String? rateId; - /// ONLY for fixed rate. - /// Network fee for transferring funds between wallets, it should be deducted - /// from the result. Formula for calculating the estimated amount is given below - /// estimatedAmount = (rate * amount) - networkFee - final Decimal? networkFee; + /// Date and time before which the estimated amount is valid if using `rateId`. + final DateTime? validUntil; + + /// Dash-separated min and max estimated time in minutes. + final String? transactionSpeedForecast; + + /// Some warnings, such as if a currency has moved to another network or transactions take longer. + final String? warningMessage; + + /// Deposit fee in the selected currency. + final Decimal depositFee; + + /// Withdrawal fee in the selected currency. + final Decimal withdrawalFee; + + /// A personal and permanent identifier under which information is stored in the database. + /// + /// Only enabled for special partners. + final String? userId; + + /// Exchange amount of `fromCurrency`. + /// + /// If `type=reverse`, this is an estimated value. + final Decimal fromAmount; - EstimatedExchangeAmount({ - required this.estimatedAmount, - required this.transactionSpeedForecast, - required this.warningMessage, + /// Exchange amount of `toCurrency`. + /// + /// If `type=direct`, this is an estimated value. + final Decimal toAmount; + + /// Creates an immutable [EstimatedExchangeAmount] instance. + const EstimatedExchangeAmount({ + required this.fromCurrency, + required this.fromNetwork, + required this.toCurrency, + required this.toNetwork, + required this.flow, + required this.type, required this.rateId, - this.networkFee, + required this.validUntil, + this.transactionSpeedForecast, + this.warningMessage, + required this.depositFee, + required this.withdrawalFee, + this.userId, + required this.fromAmount, + required this.toAmount, }); + /// Creates an instance of [EstimatedExchangeAmount] from a JSON map. factory EstimatedExchangeAmount.fromJson(Map json) { - try { - return EstimatedExchangeAmount( - estimatedAmount: Decimal.parse( - json["estimatedAmount"]?.toString() ?? - json["estimatedDeposit"].toString(), - ), - transactionSpeedForecast: - json["transactionSpeedForecast"] as String? ?? "", - warningMessage: json["warningMessage"] as String?, - rateId: json["rateId"] as String?, - networkFee: Decimal.tryParse(json["networkFee"].toString()), - ); - } catch (e, s) { - Logging.instance.e( - "Failed to parse: $json", - error: e, - stackTrace: s, - ); - rethrow; - } + return EstimatedExchangeAmount( + fromCurrency: json["fromCurrency"] as String, + fromNetwork: json["fromNetwork"] as String, + toCurrency: json["toCurrency"] as String, + toNetwork: json["toNetwork"] as String, + flow: _parseFlow(json["flow"] as String), + type: _parseType(json["type"] as String), + rateId: json["rateId"] as String?, + validUntil: DateTime.tryParse(json["validUntil"] as String? ?? ""), + transactionSpeedForecast: json["transactionSpeedForecast"] as String?, + warningMessage: json["warningMessage"] as String?, + depositFee: Decimal.parse(json["depositFee"].toString()), + withdrawalFee: Decimal.parse(json["withdrawalFee"].toString()), + userId: json["userId"]?.toString(), + fromAmount: Decimal.parse(json["fromAmount"].toString()), + toAmount: Decimal.parse(json["toAmount"].toString()), + ); } + /// Converts this [EstimatedExchangeAmount] instance to a JSON map. Map toJson() { return { - "estimatedAmount": estimatedAmount, + "fromCurrency": fromCurrency, + "fromNetwork": fromNetwork, + "toCurrency": toCurrency, + "toNetwork": toNetwork, + "flow": flow.name.replaceAll("fixedRate", "fixed-rate"), + "type": type.name, + "rateId": rateId, + "validUntil": validUntil?.toIso8601String(), "transactionSpeedForecast": transactionSpeedForecast, "warningMessage": warningMessage, - "rateId": rateId, - "networkFee": networkFee, + "depositFee": depositFee.toString(), + "withdrawalFee": withdrawalFee.toString(), + "userId": userId, + "fromAmount": fromAmount.toString(), + "toAmount": toAmount.toString(), }; } - EstimatedExchangeAmount copyWith({ - Decimal? estimatedAmount, - String? transactionSpeedForecast, - String? warningMessage, - String? rateId, - Decimal? networkFee, - }) { - return EstimatedExchangeAmount( - estimatedAmount: estimatedAmount ?? this.estimatedAmount, - transactionSpeedForecast: - transactionSpeedForecast ?? this.transactionSpeedForecast, - warningMessage: warningMessage ?? this.warningMessage, - rateId: rateId ?? this.rateId, - networkFee: networkFee ?? this.networkFee, - ); + static CNFlow _parseFlow(String value) { + switch (value) { + case "fixed-rate": + return CNFlow.fixedRate; + case "standard": + default: + return CNFlow.standard; + } + } + + static CNExchangeType _parseType(String value) { + switch (value) { + case "reverse": + return CNExchangeType.reverse; + case "direct": + default: + return CNExchangeType.direct; + } } @override diff --git a/lib/models/exchange/change_now/exchange_transaction.dart b/lib/models/exchange/change_now/exchange_transaction.dart index 6bd514546..1de49bfd0 100644 --- a/lib/models/exchange/change_now/exchange_transaction.dart +++ b/lib/models/exchange/change_now/exchange_transaction.dart @@ -1,6 +1,6 @@ -/* +/* * 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. @@ -77,6 +77,7 @@ class ExchangeTransaction { // @HiveField(14) final ExchangeTransactionStatus? statusObject; + @Deprecated("Only kept for legacy reasons") ExchangeTransaction({ required this.id, required this.payinAddress, @@ -96,6 +97,7 @@ class ExchangeTransaction { }); /// Important to pass a "date": DateTime in or it will default to 1970 + @Deprecated("Only kept for legacy reasons") factory ExchangeTransaction.fromJson(Map json) { try { return ExchangeTransaction( @@ -111,14 +113,16 @@ class ExchangeTransaction { refundExtraId: json["refundExtraId"] as String? ?? "", payoutExtraIdName: json["payoutExtraIdName"] as String? ?? "", uuid: json["uuid"] as String? ?? const Uuid().v1(), - date: DateTime.tryParse(json["date"] as String? ?? "") ?? + date: + DateTime.tryParse(json["date"] as String? ?? "") ?? DateTime.fromMillisecondsSinceEpoch(0), statusString: json["statusString"] as String? ?? "", - statusObject: json["statusObject"] is Map - ? ExchangeTransactionStatus.fromJson( - json["statusObject"] as Map, - ) - : null, + statusObject: + json["statusObject"] is Map + ? ExchangeTransactionStatus.fromJson( + json["statusObject"] as Map, + ) + : null, ); } catch (e) { rethrow; diff --git a/lib/models/exchange/change_now/exchange_transaction_status.dart b/lib/models/exchange/change_now/exchange_transaction_status.dart index 7874b8b80..5a904690f 100644 --- a/lib/models/exchange/change_now/exchange_transaction_status.dart +++ b/lib/models/exchange/change_now/exchange_transaction_status.dart @@ -1,6 +1,6 @@ -/* +/* * 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. @@ -11,38 +11,14 @@ import 'package:hive/hive.dart'; import '../../../utilities/logger.dart'; +import 'cn_exchange_transaction_status.dart' + show + ChangeNowTransactionStatus, + changeNowTransactionStatusFromStringIgnoreCase; part '../../type_adaptors/exchange_transaction_status.g.dart'; -enum ChangeNowTransactionStatus { - New, - Waiting, - Confirming, - Exchanging, - Sending, - Finished, - Failed, - Refunded, - Verifying, -} - -extension ChangeNowTransactionStatusExt on ChangeNowTransactionStatus { - String get lowerCaseName => name.toLowerCase(); -} - -ChangeNowTransactionStatus changeNowTransactionStatusFromStringIgnoreCase( - String string, -) { - for (final value in ChangeNowTransactionStatus.values) { - if (value.lowerCaseName == string.toLowerCase()) { - return value; - } - } - throw ArgumentError( - "String value does not match any known ChangeNowTransactionStatus", - ); -} - +@Deprecated("Only kept for legacy reasons") @HiveType(typeId: 16) class ExchangeTransactionStatus { /// Transaction status @@ -156,6 +132,7 @@ class ExchangeTransactionStatus { @HiveField(26) final bool isPartner; + @Deprecated("Only kept for legacy reasons") ExchangeTransactionStatus({ required this.status, required this.payinAddress, @@ -186,6 +163,7 @@ class ExchangeTransactionStatus { required this.payload, }); + @Deprecated("Only kept for legacy reasons") factory ExchangeTransactionStatus.fromJson(Map json) { Logging.instance.d(json, stackTrace: StackTrace.current); try { @@ -199,12 +177,14 @@ class ExchangeTransactionStatus { toCurrency: json["toCurrency"] as String? ?? "", id: json["id"] as String, updatedAt: json["updatedAt"] as String? ?? "", - expectedSendAmountDecimal: json["expectedSendAmount"] == null - ? "" - : json["expectedSendAmount"].toString(), - expectedReceiveAmountDecimal: json["expectedReceiveAmount"] == null - ? "" - : json["expectedReceiveAmount"].toString(), + expectedSendAmountDecimal: + json["expectedSendAmount"] == null + ? "" + : json["expectedSendAmount"].toString(), + expectedReceiveAmountDecimal: + json["expectedReceiveAmount"] == null + ? "" + : json["expectedReceiveAmount"].toString(), createdAt: json["createdAt"] as String? ?? "", isPartner: json["isPartner"] as bool, depositReceivedAt: json["depositReceivedAt"] as String? ?? "", @@ -216,9 +196,10 @@ class ExchangeTransactionStatus { payoutExtraId: json["payoutExtraId"] as String? ?? "", amountSendDecimal: json["amountSend"] == null ? "" : json["amountSend"].toString(), - amountReceiveDecimal: json["amountReceive"] == null - ? "" - : json["amountReceive"].toString(), + amountReceiveDecimal: + json["amountReceive"] == null + ? "" + : json["amountReceive"].toString(), tokensDestination: json["tokensDestination"] as String? ?? "", refundAddress: json["refundAddress"] as String? ?? "", refundExtraId: json["refundExtraId"] as String? ?? "", @@ -233,6 +214,7 @@ class ExchangeTransactionStatus { } } + @Deprecated("Only kept for legacy reasons") Map toJson() { final map = { "status": status.name, @@ -267,6 +249,7 @@ class ExchangeTransactionStatus { return map; } + @Deprecated("Only kept for legacy reasons") ExchangeTransactionStatus copyWith({ ChangeNowTransactionStatus? status, String? payinAddress, diff --git a/lib/models/exchange/incomplete_exchange.dart b/lib/models/exchange/incomplete_exchange.dart index 1397afb1a..86441bc90 100644 --- a/lib/models/exchange/incomplete_exchange.dart +++ b/lib/models/exchange/incomplete_exchange.dart @@ -10,13 +10,18 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; + +import '../../utilities/enums/exchange_rate_type_enum.dart'; +import '../isar/exchange_cache/currency.dart'; import 'response_objects/estimate.dart'; import 'response_objects/trade.dart'; -import '../../utilities/enums/exchange_rate_type_enum.dart'; class IncompleteExchangeModel extends ChangeNotifier { - final String sendTicker; - final String receiveTicker; + final Currency sendCurrency; + final Currency receiveCurrency; + + String get sendTicker => sendCurrency.ticker; + String get receiveTicker => receiveCurrency.ticker; final String rateInfo; @@ -73,8 +78,8 @@ class IncompleteExchangeModel extends ChangeNotifier { } IncompleteExchangeModel({ - required this.sendTicker, - required this.receiveTicker, + required this.sendCurrency, + required this.receiveCurrency, required this.rateInfo, required this.sendAmount, required this.receiveAmount, @@ -82,5 +87,6 @@ class IncompleteExchangeModel extends ChangeNotifier { required this.reversed, required this.walletInitiated, Estimate? estimate, - }) : _estimate = estimate; + }) : _estimate = estimate, + assert(sendCurrency.exchangeName == receiveCurrency.exchangeName); } diff --git a/lib/models/exchange/response_objects/trade.dart b/lib/models/exchange/response_objects/trade.dart index 03685a0ab..e6f996f9b 100644 --- a/lib/models/exchange/response_objects/trade.dart +++ b/lib/models/exchange/response_objects/trade.dart @@ -11,6 +11,7 @@ import 'package:hive/hive.dart'; import '../../../services/exchange/change_now/change_now_exchange.dart'; +import '../change_now/cn_exchange_transaction.dart'; import '../change_now/exchange_transaction.dart'; part 'trade.g.dart'; @@ -225,9 +226,10 @@ class Trade { timestamp: exTx.date, updatedAt: DateTime.tryParse(exTx.statusObject!.updatedAt) ?? exTx.date, payInCurrency: exTx.fromCurrency, - payInAmount: exTx.statusObject!.amountSendDecimal.isEmpty - ? exTx.statusObject!.expectedSendAmountDecimal - : exTx.statusObject!.amountSendDecimal, + payInAmount: + exTx.statusObject!.amountSendDecimal.isEmpty + ? exTx.statusObject!.expectedSendAmountDecimal + : exTx.statusObject!.amountSendDecimal, payInAddress: exTx.payinAddress, payInNetwork: "", payInExtraId: exTx.payinExtraId, @@ -245,6 +247,42 @@ class Trade { ); } + factory Trade.fromCNExchangeTransaction( + CNExchangeTransaction exTx, + bool reversed, + ) { + return Trade( + uuid: exTx.uuid, + tradeId: exTx.id, + rateType: "", + direction: reversed ? "reverse" : "direct", + timestamp: exTx.date, + updatedAt: DateTime.tryParse(exTx.statusObject!.updatedAt) ?? exTx.date, + payInCurrency: exTx.fromCurrency, + payInAmount: + exTx.statusObject!.amountFrom ?? + exTx.statusObject!.expectedAmountFrom ?? + exTx.fromAmount.toString(), + payInAddress: exTx.payinAddress, + payInNetwork: exTx.fromNetwork, + payInExtraId: "", + payInTxid: exTx.statusObject!.payinHash ?? "", + payOutCurrency: exTx.toCurrency, + payOutAmount: + exTx.statusObject!.amountTo ?? + exTx.statusObject!.expectedAmountTo ?? + exTx.toAmount.toString(), + payOutAddress: exTx.payoutAddress, + payOutNetwork: exTx.toNetwork, + payOutExtraId: exTx.payoutExtraId, + payOutTxid: exTx.statusObject!.payoutHash ?? "", + refundAddress: exTx.refundAddress, + refundExtraId: exTx.refundExtraId, + status: exTx.statusObject!.status.name, + exchangeName: ChangeNowExchange.exchangeName, + ); + } + @override String toString() { return toMap().toString(); diff --git a/lib/models/input.dart b/lib/models/input.dart new file mode 100644 index 000000000..2fcfde38d --- /dev/null +++ b/lib/models/input.dart @@ -0,0 +1,113 @@ +/* + * 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:coinlib_flutter/coinlib_flutter.dart'; + +import '../db/drift/database.dart'; +import '../utilities/enums/derive_path_type_enum.dart'; +import 'isar/models/isar_models.dart'; + +abstract class BaseInput { + BaseInput(this._utxo, {this.key}); + + final Object _utxo; + HDKey? key; + + String get id; + + String? get address; + + BigInt get value; + + int? get blockTime; + + @override + String toString() { + return "BaseInput{\n" + " _utxo: $_utxo,\n" + " key: $key,\n" + "}"; + } +} + +class StandardInput extends BaseInput { + StandardInput(UTXO super.utxo, {this.derivePathType, super.key}); + + final DerivePathType? derivePathType; + + UTXO get utxo => _utxo as UTXO; + + @override + String get id => utxo.txid; + + @override + String? get address => utxo.address; + + @override + BigInt get value => BigInt.from(utxo.value); + + @override + int? get blockTime => utxo.blockTime; + + @override + String toString() { + return "StandardInput{\n" + " derivePathType: $derivePathType,\n" + " utxo: $utxo,\n" + " key: $key,\n" + "}"; + } + + @override + bool operator ==(Object other) { + return other is StandardInput && + other.utxo.walletId == utxo.walletId && + other.utxo.txid == utxo.txid && + other.utxo.vout == utxo.vout && + other.derivePathType == derivePathType; + } + + @override + int get hashCode => Object.hashAll([utxo.walletId, utxo.txid, utxo.vout]); +} + +class MwebInput extends BaseInput { + MwebInput(MwebUtxo super.utxo); + + MwebUtxo get utxo => _utxo as MwebUtxo; + + @override + String get id => utxo.outputId; + + @override + String get address => utxo.address; + + @override + BigInt get value => BigInt.from(utxo.value); + + @override + int? get blockTime => utxo.blockTime < 1 ? null : utxo.blockTime; + + @override + String toString() { + return "MwebInput{\n" + " utxo: $utxo,\n" + " key: $key,\n" + "}"; + } + + @override + bool operator ==(Object other) { + return other is MwebInput && other.utxo == utxo; + } + + @override + int get hashCode => Object.hashAll([utxo.hashCode]); +} diff --git a/lib/models/isar/exchange_cache/currency.dart b/lib/models/isar/exchange_cache/currency.dart index 29fed140d..67a7f4098 100644 --- a/lib/models/isar/exchange_cache/currency.dart +++ b/lib/models/isar/exchange_cache/currency.dart @@ -11,6 +11,10 @@ import 'package:isar/isar.dart'; import '../../../app_config.dart'; +import '../../../services/exchange/change_now/change_now_exchange.dart'; +import '../../../services/exchange/exchange.dart'; +import '../../../services/exchange/nanswap/nanswap_exchange.dart'; +import '../../../services/exchange/trocador/trocador_exchange.dart'; import 'pair.dart'; part 'currency.g.dart'; @@ -23,12 +27,7 @@ class Currency { final String exchangeName; /// Currency ticker - @Index( - composite: [ - CompositeIndex("exchangeName"), - CompositeIndex("name"), - ], - ) + @Index(composite: [CompositeIndex("exchangeName"), CompositeIndex("name")]) final String ticker; /// Currency name @@ -68,6 +67,37 @@ class Currency { rateType == SupportedRateType.estimated || rateType == SupportedRateType.both; + // used to group coins across providers + @ignore + String? _fuzzyCache; + String getFuzzyNet() { + // hack for legacy support + if (exchangeName == "Majestic Bank") { + return ticker.toLowerCase(); + } + + return _fuzzyCache ??= switch (Exchange.fromName( + exchangeName, + ).runtimeType) { + // already lower case ticker basically + const (ChangeNowExchange) => network, + + // not used at the time being + // case const (SimpleSwapExchange): + + // currently a hardcoded of coins so we can just + // const (MajesticBankExchange) => ticker.toLowerCase(), + const (TrocadorExchange) => + (network == "Mainnet" ? ticker.toLowerCase() : network), + + // only a few coins and `network` is the ticker + const (NanswapExchange) => + network.isNotEmpty ? network.toLowerCase() : ticker.toLowerCase(), + + _ => throw Exception("Unknown exchange: $exchangeName"), + }; + } + Currency({ required this.exchangeName, required this.ticker, diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 8947d6c46..c1dfb3a24 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -101,7 +101,8 @@ class Address extends CryptoCurrencyAddress { } @override - String toString() => "{ " + String toString() => + "{ " "id: $id, " "walletId: $walletId, " "value: $value, " @@ -130,10 +131,7 @@ class Address extends CryptoCurrencyAddress { return jsonEncode(result); } - static Address fromJsonString( - String jsonString, { - String? overrideWalletId, - }) { + static Address fromJsonString(String jsonString, {String? overrideWalletId}) { final json = jsonDecode(jsonString); final derivationPathString = json["derivationPath"] as String?; @@ -176,7 +174,9 @@ enum AddressType { p2tr, solana, cardanoShelley, - xelis; + xelis, + fact0rn, + mweb; String get readableName { switch (this) { @@ -216,6 +216,10 @@ enum AddressType { return "Cardano Shelley"; case AddressType.xelis: return "Xelis"; + case AddressType.fact0rn: + return "FACT0RN"; + case AddressType.mweb: + return "MWEB"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index a9289a481..e3df46a6b 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -280,6 +280,8 @@ const _AddresstypeEnumValueMap = { 'solana': 15, 'cardanoShelley': 16, 'xelis': 17, + 'fact0rn': 18, + 'mweb': 19, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -300,6 +302,8 @@ const _AddresstypeValueEnumMap = { 15: AddressType.solana, 16: AddressType.cardanoShelley, 17: AddressType.xelis, + 18: AddressType.fact0rn, + 19: AddressType.mweb, }; Id _addressGetId(Address object) { diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index d5b3572fe..417856079 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -151,7 +151,8 @@ class Transaction { } @override - toString() => "{ " + toString() => + "{ " "id: $id, " "walletId: $walletId, " "txid: $txid, " @@ -217,12 +218,14 @@ class Transaction { slateId: json["slateId"] as String?, otherData: json["otherData"] as String?, nonce: json["nonce"] as int?, - inputs: List.from(json["inputs"] as List) - .map((e) => Input.fromJsonString(e)) - .toList(), - outputs: List.from(json["outputs"] as List) - .map((e) => Output.fromJsonString(e)) - .toList(), + inputs: + List.from( + json["inputs"] as List, + ).map((e) => Input.fromJsonString(e)).toList(), + outputs: + List.from( + json["outputs"] as List, + ).map((e) => Output.fromJsonString(e)).toList(), numberOfMessages: json["numberOfMessages"] as int, ); if (json["address"] == null) { @@ -241,7 +244,7 @@ enum TransactionType { outgoing, incoming, sentToSelf, // should we keep this? - unknown; + unknown, } // Used in Isar db and stored there as int indexes so adding/removing values @@ -256,5 +259,6 @@ enum TransactionSubType { cashFusion, sparkMint, // firo specific sparkSpend, // firo specific - ordinal; + ordinal, + mweb, } diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index 0d34d133d..3d9a3d409 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -368,6 +368,7 @@ const _TransactionsubTypeEnumValueMap = { 'sparkMint': 6, 'sparkSpend': 7, 'ordinal': 8, + 'mweb': 9, }; const _TransactionsubTypeValueEnumMap = { 0: TransactionSubType.none, @@ -379,6 +380,7 @@ const _TransactionsubTypeValueEnumMap = { 6: TransactionSubType.sparkMint, 7: TransactionSubType.sparkSpend, 8: TransactionSubType.ordinal, + 9: TransactionSubType.mweb, }; const _TransactiontypeEnumValueMap = { 'outgoing': 0, diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index b7a92c26e..3a2026785 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -135,8 +135,9 @@ class TransactionV2 { return Amount.zeroWith(fractionDigits: fractionDigits); } - final inSum = - inputs.map((e) => e.value).reduce((value, element) => value += element); + final inSum = inputs + .map((e) => e.value) + .reduce((value, element) => value += element); final outSum = outputs .map((e) => e.value) .reduce((value, element) => value += element); @@ -161,15 +162,20 @@ class TransactionV2 { } Amount getAmountSparkSelfMinted({required int fractionDigits}) { - final outSum = outputs.where((e) { - final op = e.scriptPubKeyHex.substring(0, 2).toUint8ListFromHex.first; - return e.walletOwns && (op == OP_SPARKMINT); - }).fold(BigInt.zero, (p, e) => p + e.value); + final outSum = outputs + .where((e) { + final op = e.scriptPubKeyHex.substring(0, 2).toUint8ListFromHex.first; + return e.walletOwns && (op == OP_SPARKMINT); + }) + .fold(BigInt.zero, (p, e) => p + e.value); return Amount(rawValue: outSum, fractionDigits: fractionDigits); } - Amount getAmountSentFromThisWallet({required int fractionDigits}) { + Amount getAmountSentFromThisWallet({ + required int fractionDigits, + required bool subtractFee, + }) { if (_isMonero()) { if (type == TransactionType.outgoing) { return _getMoneroAmount()!; @@ -182,15 +188,11 @@ class TransactionV2 { .where((e) => e.walletOwns) .fold(BigInt.zero, (p, e) => p + e.value); - Amount amount = Amount( - rawValue: inSum, - fractionDigits: fractionDigits, - ) - - getAmountReceivedInThisWallet( - fractionDigits: fractionDigits, - ); + Amount amount = + Amount(rawValue: inSum, fractionDigits: fractionDigits) - + getAmountReceivedInThisWallet(fractionDigits: fractionDigits); - if (subType != TransactionSubType.ethToken) { + if (subtractFee) { amount = amount - getFee(fractionDigits: fractionDigits); } @@ -204,9 +206,9 @@ class TransactionV2 { } Set associatedAddresses() => { - ...inputs.map((e) => e.addresses).expand((e) => e), - ...outputs.map((e) => e.addresses).expand((e) => e), - }; + ...inputs.map((e) => e.addresses).expand((e) => e), + ...outputs.map((e) => e.addresses).expand((e) => e), + }; Amount? _getOverrideFee() { try { @@ -238,7 +240,8 @@ class TransactionV2 { required int minConfirms, required int minCoinbaseConfirms, }) { - String prettyConfirms() => "(" + String prettyConfirms() => + "(" "${getConfirmations(currentChainHeight)}" "/" "${(isCoinbase() ? minCoinbaseConfirms : minConfirms)}" @@ -326,6 +329,10 @@ class TransactionV2 { bool isCoinbase() => type == TransactionType.incoming && inputs.any((e) => e.coinbase != null); + @ignore + bool get isInstantLock => + _getFromOtherData(key: TxV2OdKeys.isInstantLock) == true; + @override String toString() { return 'TransactionV2(\n' @@ -359,4 +366,5 @@ abstract final class TxV2OdKeys { static const moneroAmount = "moneroAmount"; static const moneroAccountIndex = "moneroAccountIndex"; static const isMoneroTransaction = "isMoneroTransaction"; + static const isInstantLock = "isInstantLock"; } diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart index 68d8a18c5..6c2ffde6d 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart @@ -389,6 +389,7 @@ const _TransactionV2subTypeEnumValueMap = { 'sparkMint': 6, 'sparkSpend': 7, 'ordinal': 8, + 'mweb': 9, }; const _TransactionV2subTypeValueEnumMap = { 0: TransactionSubType.none, @@ -400,6 +401,7 @@ const _TransactionV2subTypeValueEnumMap = { 6: TransactionSubType.sparkMint, 7: TransactionSubType.sparkSpend, 8: TransactionSubType.ordinal, + 9: TransactionSubType.mweb, }; const _TransactionV2typeEnumValueMap = { 'outgoing': 0, diff --git a/lib/models/isar/models/firo_specific/lelantus_coin.dart b/lib/models/isar/models/firo_specific/lelantus_coin.dart deleted file mode 100644 index ca4c11919..000000000 --- a/lib/models/isar/models/firo_specific/lelantus_coin.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:isar/isar.dart'; - -part 'lelantus_coin.g.dart'; - -@collection -class LelantusCoin { - Id id = Isar.autoIncrement; - - @Index() - final String walletId; - - final String txid; - - final String value; // can't use BigInt in isar :shrug: - - @Index( - unique: true, - replace: false, - composite: [ - CompositeIndex("walletId"), - ], - ) - final int mintIndex; - - final int anonymitySetId; - - final bool isUsed; - - final bool isJMint; - - final String? otherData; - - LelantusCoin({ - required this.walletId, - required this.txid, - required this.value, - required this.mintIndex, - required this.anonymitySetId, - required this.isUsed, - required this.isJMint, - required this.otherData, - }); - - LelantusCoin copyWith({ - String? walletId, - String? publicCoin, - String? txid, - String? value, - int? mintIndex, - int? anonymitySetId, - bool? isUsed, - bool? isJMint, - String? otherData, - }) { - return LelantusCoin( - walletId: walletId ?? this.walletId, - txid: txid ?? this.txid, - value: value ?? this.value, - mintIndex: mintIndex ?? this.mintIndex, - anonymitySetId: anonymitySetId ?? this.anonymitySetId, - isUsed: isUsed ?? this.isUsed, - isJMint: isJMint ?? this.isJMint, - otherData: otherData ?? this.otherData, - ); - } - - @override - String toString() { - return 'LelantusCoin{' - 'id: $id, ' - 'walletId: $walletId, ' - 'txid: $txid, ' - 'value: $value, ' - 'mintIndex: $mintIndex, ' - 'anonymitySetId: $anonymitySetId, ' - 'otherData: $otherData, ' - 'isJMint: $isJMint, ' - 'isUsed: $isUsed' - '}'; - } -} diff --git a/lib/models/isar/models/firo_specific/lelantus_coin.g.dart b/lib/models/isar/models/firo_specific/lelantus_coin.g.dart deleted file mode 100644 index 1c0f4d405..000000000 --- a/lib/models/isar/models/firo_specific/lelantus_coin.g.dart +++ /dev/null @@ -1,1629 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'lelantus_coin.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, always_specify_types - -extension GetLelantusCoinCollection on Isar { - IsarCollection get lelantusCoins => this.collection(); -} - -const LelantusCoinSchema = CollectionSchema( - name: r'LelantusCoin', - id: -6795633185033299066, - properties: { - r'anonymitySetId': PropertySchema( - id: 0, - name: r'anonymitySetId', - type: IsarType.long, - ), - r'isJMint': PropertySchema( - id: 1, - name: r'isJMint', - type: IsarType.bool, - ), - r'isUsed': PropertySchema( - id: 2, - name: r'isUsed', - type: IsarType.bool, - ), - r'mintIndex': PropertySchema( - id: 3, - name: r'mintIndex', - type: IsarType.long, - ), - r'otherData': PropertySchema( - id: 4, - name: r'otherData', - type: IsarType.string, - ), - r'txid': PropertySchema( - id: 5, - name: r'txid', - type: IsarType.string, - ), - r'value': PropertySchema( - id: 6, - name: r'value', - type: IsarType.string, - ), - r'walletId': PropertySchema( - id: 7, - name: r'walletId', - type: IsarType.string, - ) - }, - estimateSize: _lelantusCoinEstimateSize, - serialize: _lelantusCoinSerialize, - deserialize: _lelantusCoinDeserialize, - deserializeProp: _lelantusCoinDeserializeProp, - idName: r'id', - indexes: { - r'walletId': IndexSchema( - id: -1783113319798776304, - name: r'walletId', - unique: false, - replace: false, - properties: [ - IndexPropertySchema( - name: r'walletId', - type: IndexType.hash, - caseSensitive: true, - ) - ], - ), - r'mintIndex_walletId': IndexSchema( - id: -9147309777276196770, - name: r'mintIndex_walletId', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'mintIndex', - type: IndexType.value, - caseSensitive: false, - ), - IndexPropertySchema( - name: r'walletId', - type: IndexType.hash, - caseSensitive: true, - ) - ], - ) - }, - links: {}, - embeddedSchemas: {}, - getId: _lelantusCoinGetId, - getLinks: _lelantusCoinGetLinks, - attach: _lelantusCoinAttach, - version: '3.1.8', -); - -int _lelantusCoinEstimateSize( - LelantusCoin object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final value = object.otherData; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - bytesCount += 3 + object.txid.length * 3; - bytesCount += 3 + object.value.length * 3; - bytesCount += 3 + object.walletId.length * 3; - return bytesCount; -} - -void _lelantusCoinSerialize( - LelantusCoin object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.anonymitySetId); - writer.writeBool(offsets[1], object.isJMint); - writer.writeBool(offsets[2], object.isUsed); - writer.writeLong(offsets[3], object.mintIndex); - writer.writeString(offsets[4], object.otherData); - writer.writeString(offsets[5], object.txid); - writer.writeString(offsets[6], object.value); - writer.writeString(offsets[7], object.walletId); -} - -LelantusCoin _lelantusCoinDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = LelantusCoin( - anonymitySetId: reader.readLong(offsets[0]), - isJMint: reader.readBool(offsets[1]), - isUsed: reader.readBool(offsets[2]), - mintIndex: reader.readLong(offsets[3]), - otherData: reader.readStringOrNull(offsets[4]), - txid: reader.readString(offsets[5]), - value: reader.readString(offsets[6]), - walletId: reader.readString(offsets[7]), - ); - object.id = id; - return object; -} - -P _lelantusCoinDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLong(offset)) as P; - case 1: - return (reader.readBool(offset)) as P; - case 2: - return (reader.readBool(offset)) as P; - case 3: - return (reader.readLong(offset)) as P; - case 4: - return (reader.readStringOrNull(offset)) as P; - case 5: - return (reader.readString(offset)) as P; - case 6: - return (reader.readString(offset)) as P; - case 7: - return (reader.readString(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _lelantusCoinGetId(LelantusCoin object) { - return object.id; -} - -List> _lelantusCoinGetLinks(LelantusCoin object) { - return []; -} - -void _lelantusCoinAttach( - IsarCollection col, Id id, LelantusCoin object) { - object.id = id; -} - -extension LelantusCoinByIndex on IsarCollection { - Future getByMintIndexWalletId(int mintIndex, String walletId) { - return getByIndex(r'mintIndex_walletId', [mintIndex, walletId]); - } - - LelantusCoin? getByMintIndexWalletIdSync(int mintIndex, String walletId) { - return getByIndexSync(r'mintIndex_walletId', [mintIndex, walletId]); - } - - Future deleteByMintIndexWalletId(int mintIndex, String walletId) { - return deleteByIndex(r'mintIndex_walletId', [mintIndex, walletId]); - } - - bool deleteByMintIndexWalletIdSync(int mintIndex, String walletId) { - return deleteByIndexSync(r'mintIndex_walletId', [mintIndex, walletId]); - } - - Future> getAllByMintIndexWalletId( - List mintIndexValues, List walletIdValues) { - final len = mintIndexValues.length; - assert(walletIdValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([mintIndexValues[i], walletIdValues[i]]); - } - - return getAllByIndex(r'mintIndex_walletId', values); - } - - List getAllByMintIndexWalletIdSync( - List mintIndexValues, List walletIdValues) { - final len = mintIndexValues.length; - assert(walletIdValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([mintIndexValues[i], walletIdValues[i]]); - } - - return getAllByIndexSync(r'mintIndex_walletId', values); - } - - Future deleteAllByMintIndexWalletId( - List mintIndexValues, List walletIdValues) { - final len = mintIndexValues.length; - assert(walletIdValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([mintIndexValues[i], walletIdValues[i]]); - } - - return deleteAllByIndex(r'mintIndex_walletId', values); - } - - int deleteAllByMintIndexWalletIdSync( - List mintIndexValues, List walletIdValues) { - final len = mintIndexValues.length; - assert(walletIdValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([mintIndexValues[i], walletIdValues[i]]); - } - - return deleteAllByIndexSync(r'mintIndex_walletId', values); - } - - Future putByMintIndexWalletId(LelantusCoin object) { - return putByIndex(r'mintIndex_walletId', object); - } - - Id putByMintIndexWalletIdSync(LelantusCoin object, {bool saveLinks = true}) { - return putByIndexSync(r'mintIndex_walletId', object, saveLinks: saveLinks); - } - - Future> putAllByMintIndexWalletId(List objects) { - return putAllByIndex(r'mintIndex_walletId', objects); - } - - List putAllByMintIndexWalletIdSync(List objects, - {bool saveLinks = true}) { - return putAllByIndexSync(r'mintIndex_walletId', objects, - saveLinks: saveLinks); - } -} - -extension LelantusCoinQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension LelantusCoinQueryWhere - 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, - )); - } - }); - } - - QueryBuilder - mintIndexEqualToAnyWalletId(int mintIndex) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'mintIndex_walletId', - value: [mintIndex], - )); - }); - } - - QueryBuilder - mintIndexNotEqualToAnyWalletId(int mintIndex) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [], - upper: [mintIndex], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [mintIndex], - includeLower: false, - upper: [], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [mintIndex], - includeLower: false, - upper: [], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [], - upper: [mintIndex], - includeUpper: false, - )); - } - }); - } - - QueryBuilder - mintIndexGreaterThanAnyWalletId( - int mintIndex, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [mintIndex], - includeLower: include, - upper: [], - )); - }); - } - - QueryBuilder - mintIndexLessThanAnyWalletId( - int mintIndex, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [], - upper: [mintIndex], - includeUpper: include, - )); - }); - } - - QueryBuilder - mintIndexBetweenAnyWalletId( - int lowerMintIndex, - int upperMintIndex, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [lowerMintIndex], - includeLower: includeLower, - upper: [upperMintIndex], - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - mintIndexWalletIdEqualTo(int mintIndex, String walletId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'mintIndex_walletId', - value: [mintIndex, walletId], - )); - }); - } - - QueryBuilder - mintIndexEqualToWalletIdNotEqualTo(int mintIndex, String walletId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [mintIndex], - upper: [mintIndex, walletId], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [mintIndex, walletId], - includeLower: false, - upper: [mintIndex], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [mintIndex, walletId], - includeLower: false, - upper: [mintIndex], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'mintIndex_walletId', - lower: [mintIndex], - upper: [mintIndex, walletId], - includeUpper: false, - )); - } - }); - } -} - -extension LelantusCoinQueryFilter - on QueryBuilder { - QueryBuilder - anonymitySetIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'anonymitySetId', - value: value, - )); - }); - } - - QueryBuilder - anonymitySetIdGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'anonymitySetId', - value: value, - )); - }); - } - - QueryBuilder - anonymitySetIdLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'anonymitySetId', - value: value, - )); - }); - } - - QueryBuilder - anonymitySetIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'anonymitySetId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - 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 - isJMintEqualTo(bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'isJMint', - value: value, - )); - }); - } - - QueryBuilder isUsedEqualTo( - bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'isUsed', - value: value, - )); - }); - } - - QueryBuilder - mintIndexEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'mintIndex', - value: value, - )); - }); - } - - QueryBuilder - mintIndexGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'mintIndex', - value: value, - )); - }); - } - - QueryBuilder - mintIndexLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'mintIndex', - value: value, - )); - }); - } - - QueryBuilder - mintIndexBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'mintIndex', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - otherDataIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'otherData', - )); - }); - } - - QueryBuilder - otherDataIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'otherData', - )); - }); - } - - QueryBuilder - otherDataEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataBetween( - 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'otherData', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'otherData', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'otherData', - value: '', - )); - }); - } - - QueryBuilder - otherDataIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'otherData', - value: '', - )); - }); - } - - QueryBuilder txidEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'txid', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - txidGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'txid', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder txidLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'txid', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder txidBetween( - 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'txid', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - txidStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'txid', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder txidEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'txid', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder txidContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'txid', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder txidMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'txid', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - txidIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'txid', - value: '', - )); - }); - } - - QueryBuilder - txidIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'txid', - value: '', - )); - }); - } - - QueryBuilder valueEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'value', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - valueGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'value', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder valueLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'value', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder valueBetween( - 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'value', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - valueStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'value', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder valueEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'value', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder valueContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'value', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder valueMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'value', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - valueIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'value', - value: '', - )); - }); - } - - QueryBuilder - valueIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'value', - value: '', - )); - }); - } - - 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 LelantusCoinQueryObject - on QueryBuilder {} - -extension LelantusCoinQueryLinks - on QueryBuilder {} - -extension LelantusCoinQuerySortBy - on QueryBuilder { - QueryBuilder - sortByAnonymitySetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'anonymitySetId', Sort.asc); - }); - } - - QueryBuilder - sortByAnonymitySetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'anonymitySetId', Sort.desc); - }); - } - - QueryBuilder sortByIsJMint() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isJMint', Sort.asc); - }); - } - - QueryBuilder sortByIsJMintDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isJMint', Sort.desc); - }); - } - - QueryBuilder sortByIsUsed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isUsed', Sort.asc); - }); - } - - QueryBuilder sortByIsUsedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isUsed', Sort.desc); - }); - } - - QueryBuilder sortByMintIndex() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mintIndex', Sort.asc); - }); - } - - QueryBuilder sortByMintIndexDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mintIndex', Sort.desc); - }); - } - - QueryBuilder sortByOtherData() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'otherData', Sort.asc); - }); - } - - QueryBuilder sortByOtherDataDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'otherData', Sort.desc); - }); - } - - QueryBuilder sortByTxid() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'txid', Sort.asc); - }); - } - - QueryBuilder sortByTxidDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'txid', Sort.desc); - }); - } - - QueryBuilder sortByValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'value', Sort.asc); - }); - } - - QueryBuilder sortByValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'value', Sort.desc); - }); - } - - QueryBuilder sortByWalletId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'walletId', Sort.asc); - }); - } - - QueryBuilder sortByWalletIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'walletId', Sort.desc); - }); - } -} - -extension LelantusCoinQuerySortThenBy - on QueryBuilder { - QueryBuilder - thenByAnonymitySetId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'anonymitySetId', Sort.asc); - }); - } - - QueryBuilder - thenByAnonymitySetIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'anonymitySetId', Sort.desc); - }); - } - - 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 thenByIsJMint() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isJMint', Sort.asc); - }); - } - - QueryBuilder thenByIsJMintDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isJMint', Sort.desc); - }); - } - - QueryBuilder thenByIsUsed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isUsed', Sort.asc); - }); - } - - QueryBuilder thenByIsUsedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isUsed', Sort.desc); - }); - } - - QueryBuilder thenByMintIndex() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mintIndex', Sort.asc); - }); - } - - QueryBuilder thenByMintIndexDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mintIndex', Sort.desc); - }); - } - - QueryBuilder thenByOtherData() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'otherData', Sort.asc); - }); - } - - QueryBuilder thenByOtherDataDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'otherData', Sort.desc); - }); - } - - QueryBuilder thenByTxid() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'txid', Sort.asc); - }); - } - - QueryBuilder thenByTxidDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'txid', Sort.desc); - }); - } - - QueryBuilder thenByValue() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'value', Sort.asc); - }); - } - - QueryBuilder thenByValueDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'value', 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 LelantusCoinQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByAnonymitySetId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'anonymitySetId'); - }); - } - - QueryBuilder distinctByIsJMint() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isJMint'); - }); - } - - QueryBuilder distinctByIsUsed() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isUsed'); - }); - } - - QueryBuilder distinctByMintIndex() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'mintIndex'); - }); - } - - QueryBuilder distinctByOtherData( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'otherData', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByTxid( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'txid', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByValue( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'value', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByWalletId( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); - }); - } -} - -extension LelantusCoinQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder anonymitySetIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'anonymitySetId'); - }); - } - - QueryBuilder isJMintProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isJMint'); - }); - } - - QueryBuilder isUsedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isUsed'); - }); - } - - QueryBuilder mintIndexProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'mintIndex'); - }); - } - - QueryBuilder otherDataProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'otherData'); - }); - } - - QueryBuilder txidProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'txid'); - }); - } - - QueryBuilder valueProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'value'); - }); - } - - QueryBuilder walletIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'walletId'); - }); - } -} diff --git a/lib/models/isar/models/isar_models.dart b/lib/models/isar/models/isar_models.dart index 9de91fc84..ce7652a46 100644 --- a/lib/models/isar/models/isar_models.dart +++ b/lib/models/isar/models/isar_models.dart @@ -15,6 +15,5 @@ export 'blockchain_data/output.dart'; export 'blockchain_data/transaction.dart'; export 'blockchain_data/utxo.dart'; export 'ethereum/eth_contract.dart'; -export 'firo_specific/lelantus_coin.dart'; export 'log.dart'; export 'transaction_note.dart'; diff --git a/lib/models/lelantus_fee_data.dart b/lib/models/lelantus_fee_data.dart deleted file mode 100644 index 1ba930a29..000000000 --- a/lib/models/lelantus_fee_data.dart +++ /dev/null @@ -1,21 +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 - * - */ - -class LelantusFeeData { - int changeToMint; - int fee; - List spendCoinIndexes; - LelantusFeeData(this.changeToMint, this.fee, this.spendCoinIndexes); - - @override - String toString() { - return "{changeToMint: $changeToMint, fee: $fee, spendCoinIndexes: $spendCoinIndexes}"; - } -} diff --git a/lib/models/models.dart b/lib/models/models.dart index 3f6aaa525..51282c824 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -9,7 +9,6 @@ */ export 'lelantus_coin.dart'; -export 'lelantus_fee_data.dart'; export 'paymint/fee_object_model.dart'; export 'paymint/transactions_model.dart'; export 'paymint/utxo_model.dart'; diff --git a/lib/models/node_model.dart b/lib/models/node_model.dart index 1a27d1345..1224d7e5b 100644 --- a/lib/models/node_model.dart +++ b/lib/models/node_model.dart @@ -43,6 +43,10 @@ class NodeModel { final bool torEnabled; // @HiveField(12) final bool clearnetEnabled; + // @HiveField(13) + final bool forceNoTor; + // @HiveField(14) + final bool isPrimary; NodeModel({ required this.host, @@ -56,6 +60,8 @@ class NodeModel { required this.isDown, required this.torEnabled, required this.clearnetEnabled, + required this.isPrimary, + this.forceNoTor = false, this.loginName, this.trusted, }); @@ -72,7 +78,9 @@ class NodeModel { bool? isDown, required bool? trusted, bool? torEnabled, + bool? forceNoTor, bool? clearnetEnabled, + bool? isPrimary, }) { return NodeModel( host: host ?? this.host, @@ -88,6 +96,8 @@ class NodeModel { trusted: trusted, torEnabled: torEnabled ?? this.torEnabled, clearnetEnabled: clearnetEnabled ?? this.clearnetEnabled, + forceNoTor: forceNoTor ?? this.forceNoTor, + isPrimary: isPrimary ?? this.isPrimary, ); } @@ -111,6 +121,8 @@ class NodeModel { map['trusted'] = trusted; map['torEnabled'] = torEnabled; map['clearEnabled'] = clearnetEnabled; + map['forceNoTor'] = forceNoTor; + map['isPrimary'] = isPrimary; return map; } diff --git a/lib/models/paymint/fee_object_model.dart b/lib/models/paymint/fee_object_model.dart index 3745f997d..0c00f807f 100644 --- a/lib/models/paymint/fee_object_model.dart +++ b/lib/models/paymint/fee_object_model.dart @@ -9,9 +9,9 @@ */ class FeeObject { - final int fast; - final int medium; - final int slow; + final BigInt fast; + final BigInt medium; + final BigInt slow; final int numberOfBlocksFast; final int numberOfBlocksAverage; @@ -26,19 +26,22 @@ class FeeObject { required this.slow, }); - factory FeeObject.fromJson(Map json) { - return FeeObject( - fast: json['fast'] as int, - medium: json['average'] as int, - slow: json['slow'] as int, - numberOfBlocksFast: json['numberOfBlocksFast'] as int, - numberOfBlocksAverage: json['numberOfBlocksAverage'] as int, - numberOfBlocksSlow: json['numberOfBlocksSlow'] as int, - ); - } - @override String toString() { return "{fast: $fast, medium: $medium, slow: $slow, numberOfBlocksFast: $numberOfBlocksFast, numberOfBlocksAverage: $numberOfBlocksAverage, numberOfBlocksSlow: $numberOfBlocksSlow}"; } } + +class EthFeeObject extends FeeObject { + final BigInt suggestBaseFee; + + EthFeeObject({ + required this.suggestBaseFee, + required super.numberOfBlocksFast, + required super.numberOfBlocksAverage, + required super.numberOfBlocksSlow, + required super.fast, + required super.medium, + required super.slow, + }); +} diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart deleted file mode 100644 index 265a021db..000000000 --- a/lib/models/signing_data.dart +++ /dev/null @@ -1,34 +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:coinlib_flutter/coinlib_flutter.dart'; -import 'isar/models/isar_models.dart'; -import '../utilities/enums/derive_path_type_enum.dart'; - -class SigningData { - SigningData({ - required this.derivePathType, - required this.utxo, - this.keyPair, - }); - - final DerivePathType derivePathType; - final UTXO utxo; - HDPrivateKey? keyPair; - - @override - String toString() { - return "SigningData{\n" - " derivePathType: $derivePathType,\n" - " utxo: $utxo,\n" - " keyPair: $keyPair,\n" - "}"; - } -} diff --git a/lib/models/type_adaptors/node_model.g.dart b/lib/models/type_adaptors/node_model.g.dart index 32490fd50..218731a69 100644 --- a/lib/models/type_adaptors/node_model.g.dart +++ b/lib/models/type_adaptors/node_model.g.dart @@ -30,13 +30,15 @@ class NodeModelAdapter extends TypeAdapter { trusted: fields[10] as bool?, torEnabled: fields[11] as bool? ?? true, clearnetEnabled: fields[12] as bool? ?? true, + forceNoTor: fields[13] as bool? ?? false, + isPrimary: fields[14] as bool? ?? false, ); } @override void write(BinaryWriter writer, NodeModel obj) { writer - ..writeByte(13) + ..writeByte(15) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -62,7 +64,11 @@ class NodeModelAdapter extends TypeAdapter { ..writeByte(11) ..write(obj.torEnabled) ..writeByte(12) - ..write(obj.clearnetEnabled); + ..write(obj.clearnetEnabled) + ..writeByte(13) + ..write(obj.forceNoTor) + ..writeByte(14) + ..write(obj.isPrimary); } @override diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart index 210fc954f..8d141eb15 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -29,9 +29,7 @@ import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/stack_dialog.dart'; class AddCustomTokenView extends ConsumerStatefulWidget { - const AddCustomTokenView({ - super.key, - }); + const AddCustomTokenView({super.key}); static const routeName = "/addCustomToken"; @@ -56,60 +54,62 @@ class _AddCustomTokenViewState extends ConsumerState { Widget build(BuildContext context) { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 10, - left: 16, - right: 16, - bottom: 16, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only( + top: 10, + left: 16, + right: 16, + bottom: 16, + ), + child: child, + ), + ), ), - child: child, ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => Column( children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Add custom ETH token", - style: STextStyles.desktopH3(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Add custom ETH token", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 16, + ), + child: child, ), ), - const DesktopDialogCloseButton(), ], ), - Flexible( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - top: 16, - ), - child: child, - ), - ), - ], - ), child: Column( children: [ if (!isDesktop) @@ -117,10 +117,7 @@ class _AddCustomTokenViewState extends ConsumerState { "Add custom ETH token", style: STextStyles.pageTitleH1(context), ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), TextField( autocorrect: !isDesktop, enableSuggestions: !isDesktop, @@ -131,9 +128,7 @@ class _AddCustomTokenViewState extends ConsumerState { hintStyle: STextStyles.fieldLabel(context), ), ), - SizedBox( - height: isDesktop ? 16 : 8, - ), + SizedBox(height: isDesktop ? 16 : 8), PrimaryButton( label: "Search", onPressed: () async { @@ -157,10 +152,11 @@ class _AddCustomTokenViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => StackOkDialog( - title: "Failed to look up token", - message: response!.exception?.message, - ), + builder: + (context) => StackOkDialog( + title: "Failed to look up token", + message: response!.exception?.message, + ), ), ); } @@ -170,9 +166,7 @@ class _AddCustomTokenViewState extends ConsumerState { }); }, ), - SizedBox( - height: isDesktop ? 16 : 8, - ), + SizedBox(height: isDesktop ? 16 : 8), TextField( enabled: enableSubFields, autocorrect: !isDesktop, @@ -184,9 +178,7 @@ class _AddCustomTokenViewState extends ConsumerState { hintStyle: STextStyles.fieldLabel(context), ), ), - SizedBox( - height: isDesktop ? 16 : 8, - ), + SizedBox(height: isDesktop ? 16 : 8), if (isDesktop) Row( children: [ @@ -203,9 +195,7 @@ class _AddCustomTokenViewState extends ConsumerState { ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: TextField( enabled: enableSubFields, @@ -245,10 +235,7 @@ class _AddCustomTokenViewState extends ConsumerState { hintStyle: STextStyles.fieldLabel(context), ), ), - if (!isDesktop) - const SizedBox( - height: 8, - ), + if (!isDesktop) const SizedBox(height: 8), if (!isDesktop) TextField( enabled: enableSubFields, @@ -273,9 +260,7 @@ class _AddCustomTokenViewState extends ConsumerState { hintStyle: STextStyles.fieldLabel(context), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), const Spacer(), Row( children: [ @@ -286,10 +271,7 @@ class _AddCustomTokenViewState extends ConsumerState { onPressed: Navigator.of(context).pop, ), ), - if (isDesktop) - const SizedBox( - width: 16, - ), + if (isDesktop) const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Add token", diff --git a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart index 46fa6f333..3294ce18f 100644 --- a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart @@ -96,10 +96,11 @@ class _EditWalletTokensViewState extends ConsumerState { } Future onNextPressed() async { - final selectedTokens = tokenEntities - .where((e) => e.selected) - .map((e) => e.token.address) - .toList(); + final selectedTokens = + tokenEntities + .where((e) => e.selected) + .map((e) => e.token.address) + .toList(); final ethWallet = ref.read(pWallets).getWallet(widget.walletId) as EthereumWallet; @@ -110,14 +111,13 @@ class _EditWalletTokensViewState extends ConsumerState { Navigator.of(context).pop(42); } else { if (isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName(DesktopHomeView.routeName), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); } else { - await Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ); + await Navigator.of( + context, + ).pushNamedAndRemoveUntil(HomeView.routeName, (route) => false); } if (mounted) { unawaited( @@ -138,16 +138,17 @@ class _EditWalletTokensViewState extends ConsumerState { if (isDesktop) { contract = await showDialog( context: context, - builder: (context) => const DesktopDialog( - maxWidth: 580, - maxHeight: 500, - child: AddCustomTokenView(), - ), + builder: + (context) => const DesktopDialog( + maxWidth: 580, + maxHeight: 500, + child: AddCustomTokenView(), + ), ); } else { - final result = await Navigator.of(context).pushNamed( - AddCustomTokenView.routeName, - ); + final result = await Navigator.of( + context, + ).pushNamed(AddCustomTokenView.routeName); contract = result as EthContract?; } @@ -159,8 +160,9 @@ class _EditWalletTokensViewState extends ConsumerState { if (tokenEntities .where((e) => e.token.address == contract!.address) .isEmpty) { - tokenEntities - .add(AddTokenListElementData(contract!)..selected = true); + tokenEntities.add( + AddTokenListElementData(contract!)..selected = true, + ); tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); } }); @@ -178,7 +180,9 @@ class _EditWalletTokensViewState extends ConsumerState { if (contracts.isEmpty) { contracts.addAll(DefaultTokens.list); - MainDB.instance.putEthContracts(contracts).then( + MainDB.instance + .putEthContracts(contracts) + .then( (_) => ref.read(priceAnd24hChangeNotifierProvider).updatePrice(), ); } @@ -214,149 +218,135 @@ class _EditWalletTokensViewState extends ConsumerState { if (isDesktop) { return ConditionalParent( condition: !widget.isDesktopPopup, - builder: (child) => DesktopScaffold( - appBar: DesktopAppBar( - isCompactHeight: false, - useSpacers: false, - leading: const AppBarBackButton(), - overlayCenter: Text( - walletName, - style: STextStyles.desktopSubtitleH2(context), - ), - trailing: widget.contractsToMarkSelected == null - ? Padding( - padding: const EdgeInsets.only( - right: 24, - ), - child: SizedBox( - height: 56, - child: TextButton( - style: Theme.of(context) - .extension()! - .getSmallSecondaryEnabledButtonStyle(context), - onPressed: _addToken, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 30, - ), - child: Text( - "Add custom token", - style: - STextStyles.desktopButtonSmallSecondaryEnabled( - context, + builder: + (child) => DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: false, + useSpacers: false, + leading: const AppBarBackButton(), + overlayCenter: Text( + walletName, + style: STextStyles.desktopSubtitleH2(context), + ), + trailing: + widget.contractsToMarkSelected == null + ? Padding( + padding: const EdgeInsets.only(right: 24), + child: SizedBox( + height: 56, + child: TextButton( + style: Theme.of(context) + .extension()! + .getSmallSecondaryEnabledButtonStyle(context), + onPressed: _addToken, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + ), + child: Text( + "Add custom token", + style: + STextStyles.desktopButtonSmallSecondaryEnabled( + context, + ), + ), + ), ), ), + ) + : null, + ), + body: SizedBox( + width: 480, + child: Column( + children: [ + const AddTokenText(isDesktop: true), + const SizedBox(height: 16), + Expanded( + child: RoundedWhiteContainer( + radiusMultiplier: 2, + padding: const EdgeInsets.only( + left: 20, + top: 20, + right: 20, + bottom: 0, ), + child: child, ), ), - ) - : null, - ), - body: SizedBox( - width: 480, - child: Column( - children: [ - const AddTokenText( - isDesktop: true, - ), - const SizedBox( - height: 16, - ), - Expanded( - child: RoundedWhiteContainer( - radiusMultiplier: 2, - padding: const EdgeInsets.only( - left: 20, - top: 20, - right: 20, - bottom: 0, + const SizedBox(height: 26), + SizedBox( + height: 70, + width: 480, + child: PrimaryButton( + label: + widget.contractsToMarkSelected != null + ? "Save" + : "Next", + onPressed: onNextPressed, + ), ), - child: child, - ), - ), - const SizedBox( - height: 26, - ), - SizedBox( - height: 70, - width: 480, - child: PrimaryButton( - label: widget.contractsToMarkSelected != null - ? "Save" - : "Next", - onPressed: onNextPressed, - ), - ), - const SizedBox( - height: 32, + const SizedBox(height: 32), + ], ), - ], + ), ), - ), - ), child: ConditionalParent( condition: widget.isDesktopPopup, - builder: (child) => DesktopDialog( - maxHeight: 670, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Edit tokens", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Edit tokens", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - child: child, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Add custom token", - buttonHeight: ButtonHeight.l, - onPressed: _addToken, + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, ), + child: child, ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Done", - buttonHeight: ButtonHeight.l, - onPressed: onNextPressed, - ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Add custom token", + buttonHeight: ButtonHeight.l, + onPressed: _addToken, + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Done", + buttonHeight: ButtonHeight.l, + onPressed: onNextPressed, + ), + ), + ], ), - ], - ), - ), - const SizedBox( - height: 32, + ), + const SizedBox(height: 32), + ], ), - ], - ), - ), + ), child: Column( children: [ ClipRRect( @@ -373,17 +363,15 @@ class _EditWalletTokensViewState extends ConsumerState { _searchTerm = value; }); }, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), + style: STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2), decoration: standardInputDecoration( "Search", _searchFocusNode, context, ).copyWith( - contentPadding: const EdgeInsets.symmetric( - vertical: 10, - ), + contentPadding: const EdgeInsets.symmetric(vertical: 10), prefixIcon: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, @@ -393,40 +381,37 @@ class _EditWalletTokensViewState extends ConsumerState { Assets.svg.search, width: 24, height: 24, - color: Theme.of(context) - .extension()! - .textFieldDefaultSearchIconLeft, + color: + Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, ), ), - suffixIcon: _searchFieldController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon( - width: 24, - height: 24, + suffixIcon: + _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 10), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(width: 24, height: 24), + onTap: () async { + setState(() { + _searchFieldController.text = ""; + _searchTerm = ""; + }); + }, ), - onTap: () async { - setState(() { - _searchFieldController.text = ""; - _searchTerm = ""; - }); - }, - ), - ], + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Expanded( child: AddTokenList( walletId: widget.walletId, @@ -434,9 +419,7 @@ class _EditWalletTokensViewState extends ConsumerState { addFunction: isDesktop ? null : _addToken, ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), ], ), ), @@ -454,11 +437,7 @@ class _EditWalletTokensViewState extends ConsumerState { ), actions: [ Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 20, - ), + padding: const EdgeInsets.only(top: 10, bottom: 10, right: 20), child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( @@ -468,9 +447,10 @@ class _EditWalletTokensViewState extends ConsumerState { Theme.of(context).extension()!.background, icon: SvgPicture.asset( Assets.svg.circlePlusFilled, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, width: 20, height: 20, ), @@ -480,92 +460,89 @@ class _EditWalletTokensViewState extends ConsumerState { ), ], ), - body: Container( - color: Theme.of(context).extension()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - AddTokenText( - isDesktop: false, - walletName: walletName, - ), - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autofocus: isDesktop, - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchFieldController, - focusNode: _searchFocusNode, - onChanged: (value) => setState(() => _searchTerm = value), - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + body: SafeArea( + child: Container( + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AddTokenText(isDesktop: false, walletName: walletName), + const SizedBox(height: 16), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: + (value) => setState(() => _searchTerm = value), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), - ), - suffixIcon: _searchFieldController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchFieldController.text = ""; - _searchTerm = ""; - }); - }, + suffixIcon: + _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchFieldController.text = + ""; + _searchTerm = ""; + }); + }, + ), + ], ), - ], - ), - ), - ) - : null, + ), + ) + : null, + ), ), ), - ), - const SizedBox( - height: 10, - ), - Expanded( - child: AddTokenList( - walletId: widget.walletId, - items: filter(_searchTerm, tokenEntities), - addFunction: _addToken, + const SizedBox(height: 10), + Expanded( + child: AddTokenList( + walletId: widget.walletId, + items: filter(_searchTerm, tokenEntities), + addFunction: _addToken, + ), ), - ), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: widget.contractsToMarkSelected != null - ? "Save" - : "Next", - onPressed: onNextPressed, - ), - ], + const SizedBox(height: 16), + PrimaryButton( + label: + widget.contractsToMarkSelected != null + ? "Save" + : "Next", + onPressed: onNextPressed, + ), + ], + ), ), ), ), diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart index 89a818529..9137e21f4 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -46,28 +46,49 @@ class AddTokenListElement extends ConsumerStatefulWidget { class _AddTokenListElementState extends ConsumerState { final bool isDesktop = Util.isDesktop; + Currency? currency; + @override - Widget build(BuildContext context) { - final currency = ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - widget.data.token.address, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirstSync(); + void initState() { + super.initState(); + + ExchangeDataLoadingService.instance.isar.then((isar) async { + final currency = + await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + widget.data.token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirst(); + + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + this.currency = currency; + }); + } + }); + } + }); + } + @override + Widget build(BuildContext context) { final String mainLabel = widget.data.token.name; final double iconSize = isDesktop ? 32 : 24; return RoundedWhiteContainer( padding: EdgeInsets.all(isDesktop ? 16 : 12), - borderColor: isDesktop - ? Theme.of(context).extension()!.backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of(context).extension()!.backgroundAppBar + : null, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -75,71 +96,68 @@ class _AddTokenListElementState extends ConsumerState { children: [ currency != null ? SvgPicture.network( - currency.image, - width: iconSize, - height: iconSize, - placeholderBuilder: (_) => AppIcon( - width: iconSize, - height: iconSize, - ), - ) + currency!.image, + width: iconSize, + height: iconSize, + placeholderBuilder: + (_) => AppIcon(width: iconSize, height: iconSize), + ) : SvgPicture.asset( - widget.data.token.symbol == "BNB" - ? Assets.svg.bnbIcon - : Assets.svg.ethereum, - width: iconSize, - height: iconSize, - ), - const SizedBox( - width: 12, - ), + widget.data.token.symbol == "BNB" + ? Assets.svg.bnbIcon + : Assets.svg.ethereum, + width: iconSize, + height: iconSize, + ), + const SizedBox(width: 12), ConditionalParent( condition: isDesktop, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - const SizedBox( - height: 2, - ), - Text( - widget.data.token.symbol, - style: STextStyles.desktopTextExtraExtraSmall(context), - overflow: TextOverflow.ellipsis, + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + const SizedBox(height: 2), + Text( + widget.data.token.symbol, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + overflow: TextOverflow.ellipsis, + ), + ], ), - ], - ), child: Text( isDesktop ? mainLabel : "$mainLabel (${widget.data.token.symbol})", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w600_14(context), + style: + isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w600_14(context), overflow: TextOverflow.ellipsis, ), ), ], ), - const SizedBox( - width: 4, - ), + const SizedBox(width: 4), isDesktop ? Checkbox( - value: widget.data.selected, - onChanged: (newValue) => - setState(() => widget.data.selected = newValue!), - ) + value: widget.data.selected, + onChanged: + (newValue) => + setState(() => widget.data.selected = newValue!), + ) : SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: widget.data.selected, - onValueChanged: (newValue) { - widget.data.selected = newValue; - }, - ), + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: widget.data.selected, + onValueChanged: (newValue) { + widget.data.selected = newValue; + }, ), + ), ], ), ); 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 748f5074e..ef4248d0b 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 @@ -61,9 +61,7 @@ class _AddWalletViewState extends ConsumerState { String _searchTerm = ""; - final _coinsTestnet = [ - ...AppConfig.coins.where((e) => e.network.isTestNet), - ]; + final _coinsTestnet = [...AppConfig.coins.where((e) => e.network.isTestNet)]; final _coins = [ ...AppConfig.coins.where((e) => e.network == CryptoCurrencyNetwork.main), ]; @@ -98,16 +96,17 @@ class _AddWalletViewState extends ConsumerState { if (isDesktop) { contract = await showDialog( context: context, - builder: (context) => const DesktopDialog( - maxWidth: 580, - maxHeight: 500, - child: AddCustomTokenView(), - ), + builder: + (context) => const DesktopDialog( + maxWidth: 580, + maxHeight: 500, + child: AddCustomTokenView(), + ), ); } else { - contract = await Navigator.of(context).pushNamed( - AddCustomTokenView.routeName, - ); + contract = await Navigator.of( + context, + ).pushNamed(AddCustomTokenView.routeName); } if (contract != null) { @@ -143,7 +142,9 @@ class _AddWalletViewState extends ConsumerState { if (contracts.isEmpty) { contracts.addAll(DefaultTokens.list); - MainDB.instance.putEthContracts(contracts).then( + MainDB.instance + .putEthContracts(contracts) + .then( (value) => ref.read(priceAnd24hChangeNotifierProvider).updatePrice(), ); @@ -184,12 +185,8 @@ class _AddWalletViewState extends ConsumerState { ), body: Column( children: [ - const AddWalletText( - isDesktop: true, - ), - const SizedBox( - height: 16, - ), + const AddWalletText(isDesktop: true), + const SizedBox(height: 16), Expanded( child: SizedBox( width: 480, @@ -219,10 +216,9 @@ class _AddWalletViewState extends ConsumerState { _searchTerm = value; }); }, - style: - STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), + style: STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2), decoration: standardInputDecoration( "Search", _searchFocusNode, @@ -240,42 +236,44 @@ class _AddWalletViewState extends ConsumerState { Assets.svg.search, width: 24, height: 24, - color: Theme.of(context) - .extension()! - .textFieldDefaultSearchIconLeft, + color: + Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, ), ), - suffixIcon: _searchFieldController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon( - width: 24, - height: 24, + suffixIcon: + _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 10, + ), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon( + width: 24, + height: 24, + ), + onTap: () async { + setState(() { + _searchFieldController + .text = ""; + _searchTerm = ""; + }); + }, ), - onTap: () async { - setState(() { - _searchFieldController.text = - ""; - _searchTerm = ""; - }); - }, - ), - ], + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: SingleChildScrollView( child: Column( @@ -289,8 +287,10 @@ class _AddWalletViewState extends ConsumerState { if (coinTestnetEntities.isNotEmpty) ExpandingSubListItem( title: "Testnet", - entities: - filter(_searchTerm, coinTestnetEntities), + entities: filter( + _searchTerm, + coinTestnetEntities, + ), initialState: ExpandableState.expanded, animationDurationMultiplier: 0.5, ), @@ -308,27 +308,19 @@ class _AddWalletViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), ], ), ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), const SizedBox( height: 70, width: 480, - child: AddWalletNextButton( - isDesktop: true, - ), - ), - const SizedBox( - height: 32, + child: AddWalletNextButton(isDesktop: true), ), + const SizedBox(height: 32), ], ), ); @@ -344,113 +336,109 @@ class _AddWalletViewState extends ConsumerState { }, ), ), - body: Container( - color: Theme.of(context).extension()!.background, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const AddWalletText( - isDesktop: false, - ), - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: Semantics( - label: - "Search Text Field. Inputs Text To Search In Wallets.", - excludeSemantics: true, - child: TextField( - autofocus: isDesktop, - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchFieldController, - focusNode: _searchFocusNode, - onChanged: (value) => - setState(() => _searchTerm = value), - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + body: SafeArea( + child: Container( + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const AddWalletText(isDesktop: false), + const SizedBox(height: 16), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: Semantics( + label: + "Search Text Field. Inputs Text To Search In Wallets.", + excludeSemantics: true, + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: + (value) => setState(() => _searchTerm = value), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), ), - ), - suffixIcon: _searchFieldController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchFieldController.text = ""; - _searchTerm = ""; - }); - }, + suffixIcon: + _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchFieldController.text = + ""; + _searchTerm = ""; + }); + }, + ), + ], ), - ], - ), - ), - ) - : null, + ), + ) + : null, + ), ), ), ), - ), - const SizedBox( - height: 10, - ), - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - ExpandingSubListItem( - title: "Coins", - entities: filter(_searchTerm, coinEntities), - initialState: ExpandableState.expanded, - ), - if (coinTestnetEntities.isNotEmpty) - ExpandingSubListItem( - title: "Testnet", - entities: - filter(_searchTerm, coinTestnetEntities), - initialState: ExpandableState.expanded, - ), - if (tokenEntities.isNotEmpty) + const SizedBox(height: 10), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ ExpandingSubListItem( - title: "Tokens", - entities: filter(_searchTerm, tokenEntities), + title: "Coins", + entities: filter(_searchTerm, coinEntities), initialState: ExpandableState.expanded, ), - ], + if (coinTestnetEntities.isNotEmpty) + ExpandingSubListItem( + title: "Testnet", + entities: filter( + _searchTerm, + coinTestnetEntities, + ), + initialState: ExpandableState.expanded, + ), + if (tokenEntities.isNotEmpty) + ExpandingSubListItem( + title: "Tokens", + entities: filter(_searchTerm, tokenEntities), + initialState: ExpandableState.expanded, + ), + ], + ), ), ), - ), - const SizedBox( - height: 16, - ), - const AddWalletNextButton( - isDesktop: false, - ), - ], + const SizedBox(height: 16), + const AddWalletNextButton(isDesktop: false), + ], + ), ), ), ), diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart index 68527ccaf..2506b4195 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart @@ -14,6 +14,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 '../../../../models/add_wallet_list_entity/add_wallet_list_entity.dart'; import '../../../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import '../../../../models/isar/exchange_cache/currency.dart'; @@ -27,99 +28,117 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; -class CoinSelectItem extends ConsumerWidget { - const CoinSelectItem({ - super.key, - required this.entity, - }); +class CoinSelectItem extends ConsumerStatefulWidget { + const CoinSelectItem({super.key, required this.entity}); final AddWalletListEntity entity; @override - Widget build(BuildContext context, WidgetRef ref) { - debugPrint("BUILD: CoinSelectItem for ${entity.name}"); - final selectedEntity = ref.watch(addWalletSelectedEntityStateProvider); + ConsumerState createState() => _CoinSelectItemState(); +} - final isDesktop = Util.isDesktop; +class _CoinSelectItemState extends ConsumerState { + String? tokenImageUri; + + @override + void initState() { + super.initState(); + + if (widget.entity is EthTokenEntity) { + ExchangeDataLoadingService.instance.isar.then((isar) async { + final currency = + await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + (widget.entity as EthTokenEntity).token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirst(); - String? tokenImageUri; - if (entity is EthTokenEntity) { - final currency = ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - (entity as EthTokenEntity).token.address, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirstSync(); - tokenImageUri = currency?.image; + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + tokenImageUri = currency?.image; + }); + } + }); + } + }); } + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: CoinSelectItem for ${widget.entity.name}"); + final selectedEntity = ref.watch(addWalletSelectedEntityStateProvider); + + final isDesktop = Util.isDesktop; return Container( decoration: BoxDecoration( - color: selectedEntity == entity - ? Theme.of(context).extension()!.textFieldActiveBG - : Theme.of(context).extension()!.popupBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + color: + selectedEntity == widget.entity + ? Theme.of(context).extension()!.textFieldActiveBG + : Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: MaterialButton( - key: Key("coinSelectItemButtonKey_${entity.name}${entity.ticker}"), - padding: isDesktop - ? const EdgeInsets.only(left: 24) - : const EdgeInsets.all(12), + key: Key( + "coinSelectItemButtonKey_${widget.entity.name}${widget.entity.ticker}", + ), + padding: + isDesktop + ? const EdgeInsets.only(left: 24) + : const EdgeInsets.all(12), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: isDesktop ? 70 : 0, - ), + constraints: BoxConstraints(minHeight: isDesktop ? 70 : 0), child: Row( children: [ tokenImageUri != null - ? SvgPicture.network( - tokenImageUri, - width: 26, - height: 26, - ) + ? SvgPicture.network(tokenImageUri!, width: 26, height: 26) : SvgPicture.file( - File( - ref.watch(coinIconProvider(entity.cryptoCurrency)), - ), - width: 26, - height: 26, + File( + ref.watch(coinIconProvider(widget.entity.cryptoCurrency)), ), - SizedBox( - width: isDesktop ? 12 : 10, - ), + width: 26, + height: 26, + ), + SizedBox(width: isDesktop ? 12 : 10), Text( - "${entity.name} (${entity.ticker})", - style: isDesktop - ? STextStyles.desktopTextMedium(context) - : STextStyles.subtitle600(context).copyWith( - fontSize: 14, - ), + "${widget.entity.name} (${widget.entity.ticker})", + style: + isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.subtitle600( + context, + ).copyWith(fontSize: 14), ), - if (isDesktop && selectedEntity == entity) const Spacer(), - if (isDesktop && selectedEntity == entity) + if (isDesktop && selectedEntity == widget.entity) const Spacer(), + if (isDesktop && selectedEntity == widget.entity) Padding( - padding: const EdgeInsets.only( - right: 18, - ), + padding: const EdgeInsets.only(right: 18), child: SizedBox( width: 24, height: 24, child: SvgPicture.asset( Assets.svg.check, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -127,7 +146,8 @@ class CoinSelectItem extends ConsumerWidget { ), ), onPressed: () { - ref.read(addWalletSelectedEntityStateProvider.state).state = entity; + ref.read(addWalletSelectedEntityStateProvider.state).state = + widget.entity; }, ), ); diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart index d934526b8..db55898d2 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart @@ -30,7 +30,7 @@ class ExpandingSubListItem extends StatefulWidget { double? animationDurationMultiplier, this.curve = Curves.easeInOutCubicEmphasized, }) : animationDurationMultiplier = - animationDurationMultiplier ?? entities.length * 0.11; + animationDurationMultiplier ?? entities.length * 0.11; final String title; final List entities; @@ -85,23 +85,21 @@ class _ExpandingSubListItemState extends State { header: Container( color: Colors.transparent, child: Padding( - padding: const EdgeInsets.only( - top: 8.0, - bottom: 8.0, - right: 10, - ), + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( widget.title, - style: isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.desktopTextMedium(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), RotateIcon( @@ -109,9 +107,10 @@ class _ExpandingSubListItemState extends State { Assets.svg.chevronDown, width: isDesktop ? 20 : 12, height: isDesktop ? 10 : 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), curve: widget.curve, animationDurationMultiplier: widget.animationDurationMultiplier, 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 d377eff72..131e60004 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,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; + import '../../../../frost_route_generator.dart'; import '../../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../../themes/stack_colors.dart'; @@ -43,76 +44,77 @@ class _SelectNewFrostImportTypeViewState 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, - ), - ), + 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(), - ); + builder: + (content) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); }, ), - ), - ], - ), - 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, + 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: SafeArea( + child: 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( @@ -163,11 +165,12 @@ class _SelectNewFrostImportTypeViewState break; } - await Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - ); + await Navigator.of( + context, + ).pushNamed(FrostStepScaffold.routeName); }, ), + if (Util.isDesktop) const SizedBox(height: 32), ], ), ), @@ -232,9 +235,10 @@ class _ImportOptionCardState extends State<_ImportOptionCard> { child: Radio( value: widget.value, groupValue: widget.groupValue, - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of( + context, + ).extension()!.radioButtonIconEnabled, onChanged: (_) => widget.onPressed(), ), ), @@ -257,18 +261,17 @@ class _ImportOptionCardState extends State<_ImportOptionCard> { ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Row( children: [ Expanded( child: Text( widget.description, style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ), @@ -293,21 +296,14 @@ class _FrostJoinInfoDialog extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Join a group", - style: STextStyles.w600_20(context), - ), - const SizedBox( - height: 12, - ), + Text("Join a group", style: STextStyles.w600_20(context)), + const SizedBox(height: 12), Text( "You should select 'Join a new group' if you are creating a brand " "new wallet with other people.", style: STextStyles.w600_16(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Text( "You should select 'Join an existing group' if you an existing " "group is being edited and you are being added as a participant.", 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 3c59c7524..9d835ea4e 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 @@ -8,12 +8,9 @@ import '../../../../../app_config.dart'; import '../../../../../frost_route_generator.dart'; import '../../../../../notifications/show_flush_bar.dart'; import '../../../../../pages_desktop_specific/desktop_home_view.dart'; -import '../../../../../providers/db/main_db_provider.dart'; import '../../../../../providers/frost_wallet/frost_wallet_providers.dart'; -import '../../../../../providers/global/node_service_provider.dart'; -import '../../../../../providers/global/prefs_provider.dart'; import '../../../../../providers/global/secure_store_provider.dart'; -import '../../../../../providers/global/wallets_provider.dart'; +import '../../../../../providers/providers.dart'; import '../../../../../services/frost.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/assets.dart'; @@ -43,7 +40,8 @@ class FrostCreateStep5 extends ConsumerStatefulWidget { } class _FrostCreateStep5State extends ConsumerState { - static const _warning = "These are your private keys. Please back them up, " + 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. ${AppConfig.prefix} does not keep nor is able to restore your private keys" @@ -79,9 +77,10 @@ class _FrostCreateStep5State extends ConsumerState { child: Text( _warning, style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: + Theme.of( + context, + ).extension()!.warningForeground, ), ), ), @@ -89,25 +88,19 @@ class _FrostCreateStep5State extends ConsumerState { DetailItem( title: "Multisig Config", detail: multisigConfig, - button: Util.isDesktop - ? IconCopyButton( - data: multisigConfig, - ) - : SimpleCopyButton( - data: 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, - ), + button: + Util.isDesktop + ? IconCopyButton(data: serializedKeys) + : SimpleCopyButton(data: serializedKeys), ), if (!Util.isDesktop) const Spacer(), const SizedBox(height: 12), @@ -133,10 +126,7 @@ class _FrostCreateStep5State extends ConsumerState { useSafeArea: true, builder: (ctx) { return const Center( - child: LoadingIndicator( - width: 50, - height: 50, - ), + child: LoadingIndicator(width: 50, height: 50), ); }, ), @@ -177,6 +167,13 @@ class _FrostCreateStep5State extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(wallet); // pop progress dialog @@ -191,9 +188,7 @@ class _FrostCreateStep5State extends ConsumerState { if (Util.isDesktop) { nav.popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), + ModalRoute.withName(DesktopHomeView.routeName), ); } else { unawaited( @@ -219,7 +214,7 @@ class _FrostCreateStep5State extends ConsumerState { ); } } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); // pop progress dialog if (context.mounted && !progressPopped) { 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 83fa55944..356edef59 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 @@ -4,12 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../frost_route_generator.dart'; import '../../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../../pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/frost_wallet/frost_wallet_providers.dart'; -import '../../../../providers/global/node_service_provider.dart'; -import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; -import '../../../../providers/global/wallets_provider.dart'; +import '../../../../providers/providers.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; @@ -68,12 +65,20 @@ class _FrostReshareStep5State extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(wallet); } else { - wallet = ref - .read(pWallets) - .getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) - as BitcoinFrostWallet; + wallet = + ref + .read(pWallets) + .getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; } if (mounted) { @@ -96,22 +101,19 @@ class _FrostReshareStep5State extends ConsumerState { if (mounted) { ref.read(pFrostResharingData).reset(); ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; - ref.read(pFrostScaffoldArgs)?.parentNav.popUntil( - ModalRoute.withName( - _popUntilPath, - ), - ); + ref + .read(pFrostScaffoldArgs) + ?.parentNav + .popUntil(ModalRoute.withName(_popUntilPath)); } } } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); if (mounted) { await showDialog( context: context, - builder: (_) => FrostErrorDialog( - title: "Error", - message: e.toString(), - ), + builder: + (_) => FrostErrorDialog(title: "Error", message: e.toString()), ); } } finally { @@ -119,11 +121,12 @@ class _FrostReshareStep5State extends ConsumerState { } } - String get _popUntilPath => isNew - ? Util.isDesktop - ? DesktopHomeView.routeName - : HomeView.routeName - : Util.isDesktop + String get _popUntilPath => + isNew + ? Util.isDesktop + ? DesktopHomeView.routeName + : HomeView.routeName + : Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName; @@ -134,7 +137,8 @@ class _FrostReshareStep5State extends ConsumerState { ref.read(pFrostResharingData).newWalletData!.serializedKeys; reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId; - isNew = ref.read(pFrostResharingData).incompleteWallet != null && + isNew = + ref.read(pFrostResharingData).incompleteWallet != null && ref.read(pFrostResharingData).incompleteWallet!.walletId == ref.read(pFrostScaffoldArgs)!.walletId!; @@ -151,66 +155,42 @@ class _FrostReshareStep5State extends ConsumerState { "Ensure your reshare ID matches that of each other participant", style: STextStyles.pageTitleH2(context), ), - const SizedBox( - height: 12, - ), + 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, + 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, - ), + const SizedBox(height: 12), DetailItem( title: "Config", detail: config, - button: Util.isDesktop - ? IconCopyButton( - data: config, - ) - : SimpleCopyButton( - data: config, - ), - ), - const SizedBox( - height: 12, + 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, - ), + button: + Util.isDesktop + ? IconCopyButton(data: serializedKeys) + : SimpleCopyButton(data: serializedKeys), ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), - PrimaryButton( - label: "Confirm", - onPressed: _onPressed, - ), + const SizedBox(height: 12), + PrimaryButton(label: "Confirm", onPressed: _onPressed), ], ), ); diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 88f7c3b26..facaeed44 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 @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -9,14 +8,12 @@ import 'package:frostdart/frostdart.dart' as frost; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages_desktop_specific/desktop_home_view.dart'; -import '../../../../providers/db/main_db_provider.dart'; -import '../../../../providers/global/node_service_provider.dart'; -import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; -import '../../../../providers/global/wallets_provider.dart'; +import '../../../../providers/providers.dart'; import '../../../../services/frost.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; +import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/show_loading.dart'; @@ -95,9 +92,7 @@ class _RestoreFrostMsWalletViewState knownSalts: [], participants: participants, myName: myName, - threshold: frost.multisigThreshold( - multisigConfig: config, - ), + threshold: frost.multisigThreshold(multisigConfig: config), ); await ref.read(mainDBProvider).isar.writeTxn(() async { @@ -110,9 +105,14 @@ class _RestoreFrostMsWalletViewState isRescan: false, ); - await info.setMnemonicVerified( - isar: ref.read(mainDBProvider).isar, - ); + await info.setMnemonicVerified(isar: ref.read(mainDBProvider).isar); + + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } return wallet; } @@ -147,17 +147,14 @@ class _RestoreFrostMsWalletViewState if (mounted) { if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); } else { unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), + Navigator.of( + context, + ).pushNamedAndRemoveUntil(HomeView.routeName, (route) => false), ); } @@ -171,20 +168,17 @@ class _RestoreFrostMsWalletViewState ); } } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + Logging.instance.e("", error: e, stackTrace: s); if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Failed to restore", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), + builder: + (_) => StackOkDialog( + title: "Failed to restore", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), ); } } finally { @@ -215,12 +209,10 @@ class _RestoreFrostMsWalletViewState if (Platform.isAndroid || Platform.isIOS) { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); + await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await BarcodeScanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); configFieldController.text = qrResult.rawContent; @@ -235,9 +227,7 @@ class _RestoreFrostMsWalletViewState ); if (qrResult == null) { - Logging.instance.d( - "Qr scanning cancelled", - ); + Logging.instance.d("Qr scanning cancelled"); } else { // TODO [prio=low]: Validate QR code data. configFieldController.text = qrResult; @@ -248,11 +238,26 @@ class _RestoreFrostMsWalletViewState } } } on PlatformException catch (e, s) { - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code: ", + error: e, + stackTrace: s, + ); + } } } @@ -260,67 +265,64 @@ class _RestoreFrostMsWalletViewState 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, ', + 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), ), - ), - 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, + 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, - ), + const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -350,51 +352,53 @@ class _RestoreFrostMsWalletViewState right: 5, ), suffixIcon: Padding( - padding: _configEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + 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(), - ) + 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(), - ), + 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: @@ -410,9 +414,7 @@ class _RestoreFrostMsWalletViewState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -442,51 +444,53 @@ class _RestoreFrostMsWalletViewState right: 5, ), suffixIcon: Padding( - padding: _keysEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + 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(), - ) + 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(), - ), + 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(), + ), ], ), ), @@ -494,13 +498,9 @@ class _RestoreFrostMsWalletViewState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), PrimaryButton( label: "Restore", enabled: !_keysEmpty && !_configEmpty, 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 f17ed06ff..ec7419d96 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 @@ -110,10 +110,7 @@ class _NameYourWalletViewState extends ConsumerState { coin is ViewOnlyOptionCurrencyInterface ? NewWalletOptionsView.routeName : NewWalletRecoveryPhraseWarningView.routeName, - arguments: Tuple2( - name, - coin, - ), + arguments: Tuple2(name, coin), ), ); break; @@ -122,10 +119,7 @@ class _NameYourWalletViewState extends ConsumerState { unawaited( Navigator.of(context).pushNamed( RestoreOptionsView.routeName, - arguments: Tuple2( - name, - coin, - ), + arguments: Tuple2(name, coin), ), ); break; @@ -145,9 +139,7 @@ class _NameYourWalletViewState extends ConsumerState { .where() .nameProperty() .findAll() - .then( - (values) => namesToExclude.addAll(values), - ); + .then((values) => namesToExclude.addAll(values)); generator = NameGenerator(); addWalletType = widget.addWalletType; coin = widget.coin; @@ -177,10 +169,7 @@ class _NameYourWalletViewState extends ConsumerState { trailing: ExitToMyStackButton(), isCompactHeight: false, ), - body: SizedBox( - width: 480, - child: _content(), - ), + body: SizedBox(width: 480, child: _content()), ); } else { return Background( @@ -192,8 +181,9 @@ class _NameYourWalletViewState extends ConsumerState { onPressed: () { if (textFieldFocusNode.hasFocus) { textFieldFocusNode.unfocus(); - Future.delayed(const Duration(milliseconds: 100)) - .then((value) => Navigator.of(context).pop()); + Future.delayed( + const Duration(milliseconds: 100), + ).then((value) => Navigator.of(context).pop()); } else { if (mounted) { Navigator.of(context).pop(); @@ -202,22 +192,23 @@ class _NameYourWalletViewState extends ConsumerState { }, ), ), - 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(), + body: SafeArea( + child: 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()), ), - ), - ); - }, + ); + }, + ), ), ), ), @@ -227,284 +218,251 @@ class _NameYourWalletViewState extends ConsumerState { } Widget _content() => Column( - crossAxisAlignment: - isDesktop ? CrossAxisAlignment.center : CrossAxisAlignment.stretch, - children: [ - if (isDesktop) - const Spacer( - flex: 10, - ), - if (!isDesktop) - const Spacer( - flex: 1, - ), - if (!isDesktop) - CoinImage( - coin: coin, - height: 100, - width: 100, - ), - SizedBox( - height: isDesktop ? 0 : 16, - ), - Text( - "Name your ${coin.prettyName} ${coin is FrostCurrency ? "multisig " : ""}wallet", - textAlign: TextAlign.center, - style: isDesktop + crossAxisAlignment: + isDesktop ? CrossAxisAlignment.center : CrossAxisAlignment.stretch, + children: [ + if (isDesktop) const Spacer(flex: 10), + if (!isDesktop) const Spacer(flex: 1), + if (!isDesktop) CoinImage(coin: coin, height: 100, width: 100), + SizedBox(height: isDesktop ? 0 : 16), + Text( + "Name your ${coin.prettyName} ${coin is FrostCurrency ? "multisig " : ""}wallet", + textAlign: TextAlign.center, + style: + isDesktop ? STextStyles.desktopH2(context) : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 16 : 8, - ), - Text( - "Enter a label for your wallet (e.g. ${coin is FrostCurrency ? "Multisig" : "Savings"})", - textAlign: TextAlign.center, - style: isDesktop + ), + SizedBox(height: isDesktop ? 16 : 8), + Text( + "Enter a label for your wallet (e.g. ${coin is FrostCurrency ? "Multisig" : "Savings"})", + textAlign: TextAlign.center, + style: + isDesktop ? STextStyles.desktopSubtitleH2(context) : STextStyles.subtitle(context), - ), - SizedBox( - height: isDesktop ? 40 : 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - onChanged: (string) { - if (string.isEmpty) { - if (_nextEnabled) { - setState(() { - _nextEnabled = false; - _showDiceIcon = true; - }); - } - } else { - if (!_nextEnabled) { - setState(() { - _nextEnabled = true; - _showDiceIcon = false; - }); - } - } - }, - focusNode: textFieldFocusNode, - controller: textEditingController, - style: isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ) + ), + SizedBox(height: isDesktop ? 40 : 16), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + onChanged: (string) { + if (string.isEmpty) { + if (_nextEnabled) { + setState(() { + _nextEnabled = false; + _showDiceIcon = true; + }); + } + } else { + if (!_nextEnabled) { + setState(() { + _nextEnabled = true; + _showDiceIcon = false; + }); + } + } + }, + focusNode: textFieldFocusNode, + controller: textEditingController, + style: + isDesktop + ? STextStyles.desktopTextMedium(context).copyWith(height: 2) : STextStyles.field(context), - decoration: standardInputDecoration( - "Enter wallet name", - textFieldFocusNode, - context, - ).copyWith( - suffixIcon: Padding( - padding: EdgeInsets.only(right: isDesktop ? 6 : 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - key: const Key("genRandomWalletNameButtonKey"), - child: _showDiceIcon + decoration: standardInputDecoration( + "Enter wallet name", + textFieldFocusNode, + context, + ).copyWith( + suffixIcon: Padding( + padding: EdgeInsets.only(right: isDesktop ? 6 : 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + key: const Key("genRandomWalletNameButtonKey"), + child: + _showDiceIcon ? Semantics( - label: - "Generate Random Wallet Name Button. Generates A Random Name For Wallet.", - excludeSemantics: true, - child: DiceIcon( - width: isDesktop ? 20 : 17, - height: isDesktop ? 20 : 17, - ), - ) + label: + "Generate Random Wallet Name Button. Generates A Random Name For Wallet.", + excludeSemantics: true, + child: DiceIcon( + width: isDesktop ? 20 : 17, + height: isDesktop ? 20 : 17, + ), + ) : Semantics( - label: - "Generate Random Wallet Name Button. Generates A Random Name For Wallet.", - excludeSemantics: true, - child: XIcon( - width: isDesktop ? 21 : 18, - height: isDesktop ? 21 : 18, - ), + label: + "Clear Wallet Name Field Button. Clears the wallet name field.", + excludeSemantics: true, + child: XIcon( + width: isDesktop ? 21 : 18, + height: isDesktop ? 21 : 18, ), - onTap: () async { - if (_showDiceIcon) { - textEditingController.text = - await _generateRandomWalletName(); - setState(() { - _nextEnabled = true; - _showDiceIcon = false; - }); - } else { - textEditingController.text = ""; - setState(() { - _nextEnabled = false; - _showDiceIcon = true; - }); - } - }, - ), - ], + ), + onTap: () async { + if (_showDiceIcon) { + textEditingController.text = + await _generateRandomWalletName(); + setState(() { + _nextEnabled = true; + _showDiceIcon = false; + }); + } else { + textEditingController.text = ""; + setState(() { + _nextEnabled = false; + _showDiceIcon = true; + }); + } + }, ), - ), + ], ), ), ), ), - SizedBox( - height: isDesktop ? 16 : 8, - ), - GestureDetector( - onTap: () async { - textEditingController.text = await _generateRandomWalletName(); - setState(() { - _nextEnabled = true; - _showDiceIcon = false; - }); - }, - child: RoundedWhiteContainer( - child: Center( - child: Text( - "Roll the dice to pick a random name.", - style: isDesktop + ), + ), + SizedBox(height: isDesktop ? 16 : 8), + GestureDetector( + onTap: () async { + textEditingController.text = await _generateRandomWalletName(); + setState(() { + _nextEnabled = true; + _showDiceIcon = false; + }); + }, + child: RoundedWhiteContainer( + child: Center( + child: Text( + "Roll the dice to pick a random name.", + style: + isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ) : STextStyles.itemSubtitle(context), - ), - ), ), ), - if (!isDesktop) - const Spacer( - flex: 4, - ), - if (isDesktop) - const SizedBox( - height: 32, - ), - if (widget.coin is FrostCurrency) - if (widget.addWalletType == AddWalletType.Restore) - PrimaryButton( - label: "Next", - enabled: _nextEnabled, - onPressed: () async { - final name = textEditingController.text; + ), + ), + if (!isDesktop) const Spacer(flex: 4), + if (isDesktop) const SizedBox(height: 32), + if (widget.coin is FrostCurrency) + 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, - frostCurrency: coin, - ), - ); - }, - ), - if (widget.coin is FrostCurrency && - widget.addWalletType == AddWalletType.New) - Column( - children: [ - PrimaryButton( - label: "Create new group", - enabled: _nextEnabled, - onPressed: () async { - final name = textEditingController.text; + await Navigator.of(context).pushNamed( + RestoreFrostMsWalletView.routeName, + arguments: (walletName: name, frostCurrency: coin), + ); + }, + ), + if (widget.coin is FrostCurrency && + widget.addWalletType == AddWalletType.New) + Column( + children: [ + PrimaryButton( + label: "Create new group", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; - await Navigator.of(context).pushNamed( - CreateNewFrostMsWalletView.routeName, - arguments: ( - walletName: name, - frostCurrency: coin, - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - SecondaryButton( - label: "Join group", - enabled: _nextEnabled, - onPressed: () async { - final name = textEditingController.text; + await Navigator.of(context).pushNamed( + CreateNewFrostMsWalletView.routeName, + arguments: (walletName: name, frostCurrency: coin), + ); + }, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Join group", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; - await Navigator.of(context).pushNamed( - SelectNewFrostImportTypeView.routeName, - arguments: ( - walletName: name, - frostCurrency: coin, - ), - ); - }, - ), - // 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, - // ), - // ); - // }, - // ), - ], + await Navigator.of(context).pushNamed( + SelectNewFrostImportTypeView.routeName, + arguments: (walletName: name, frostCurrency: coin), + ); + }, ), - if (widget.coin is! FrostCurrency) - ConstrainedBox( - constraints: BoxConstraints( - minWidth: isDesktop ? 480 : 0, - minHeight: isDesktop ? 70 : 0, - ), - child: TextButton( - onPressed: _nextEnabled ? _nextPressed : null, - style: _nextEnabled + // 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 is! FrostCurrency) + 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 + child: Text( + "Next", + style: + isDesktop ? _nextEnabled ? STextStyles.desktopButtonEnabled(context) : STextStyles.desktopButtonDisabled(context) : STextStyles.button(context), - ), - ), - ), - if (isDesktop) - const Spacer( - flex: 15, ), - ], - ); + ), + ), + if (isDesktop) const Spacer(flex: 15), + ], + ); } diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 7d93d038a..7e3f57d32 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -19,7 +19,6 @@ import 'package:tuple/tuple.dart'; import '../../../app_config.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../services/transaction_notification_tracker.dart'; @@ -32,6 +31,7 @@ import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -81,11 +81,12 @@ class _NewWalletRecoveryPhraseWarningViewState if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Create Wallet Error", - message: ex?.toString() ?? "Unknown error", - maxWidth: 600, - ), + builder: + (_) => StackOkDialog( + title: "Create Wallet Error", + message: ex?.toString() ?? "Unknown error", + maxWidth: 600, + ), ); } return; @@ -95,10 +96,7 @@ class _NewWalletRecoveryPhraseWarningViewState unawaited( nav.pushNamed( NewWalletRecoveryPhraseView.routeName, - arguments: Tuple2( - result.$1, - result.$2, - ), + arguments: Tuple2(result.$1, result.$2), ), ); } @@ -107,12 +105,12 @@ class _NewWalletRecoveryPhraseWarningViewState Future<(Wallet, List)> _initNewFuture() async { try { - String? otherDataJsonString; + Map? otherDataJson; if (widget.coin is Tezos) { - otherDataJsonString = jsonEncode({ + otherDataJson = { WalletInfoKeys.tezosDerivationPath: Tezos.standardDerivationPath.value, - }); + }; // }//todo: probably not needed (broken anyways) // else if (widget.coin is Epiccash) { // final int secondsSinceEpoch = @@ -144,12 +142,16 @@ class _NewWalletRecoveryPhraseWarningViewState // ), // }, // ); - } else if (widget.coin is Firo) { - otherDataJsonString = jsonEncode( - { - WalletInfoKeys.lelantusCoinIsarRescanRequired: false, - }, - ); + } + + if (ref.read(pDuress)) { + otherDataJson ??= {}; + otherDataJson[WalletInfoKeys.duressMarkedVisibleWalletKey] = true; + } + + String? otherDataJsonString; + if (otherDataJson != null && otherDataJson.isNotEmpty) { + otherDataJsonString = jsonEncode(otherDataJson); } final info = WalletInfo.createNew( @@ -159,28 +161,17 @@ class _NewWalletRecoveryPhraseWarningViewState ); var node = ref - .read( - nodeServiceChangeNotifierProvider, - ) - .getPrimaryNodeFor( - currency: coin, - ); + .read(nodeServiceChangeNotifierProvider) + .getPrimaryNodeFor(currency: coin); if (node == null) { - node = coin.defaultNode; + node = coin.defaultNode(isPrimary: true); await ref - .read( - nodeServiceChangeNotifierProvider, - ) - .setPrimaryNodeFor( - coin: coin, - node: node, - ); + .read(nodeServiceChangeNotifierProvider) + .save(node, null, false); } - final txTracker = TransactionNotificationTracker( - walletId: info.walletId, - ); + final txTracker = TransactionNotificationTracker(walletId: info.walletId); String? mnemonicPassphrase; String? mnemonic; @@ -191,66 +182,49 @@ class _NewWalletRecoveryPhraseWarningViewState // TODO: Refactor these to generate each coin in their respective classes // This code should not be in a random view page file - if (coin is Monero || coin is Wownero || coin is Xelis) { + if (coin is Monero || + coin is Wownero || + coin is Xelis || + coin is Salvium) { // currently a special case due to the // xmr/wow libraries handling their // own mnemonic generation - wordCount = ref.read(pNewWalletOptions)?.mnemonicWordsCount ?? + wordCount = + ref.read(pNewWalletOptions)?.mnemonicWordsCount ?? info.coin.defaultSeedPhraseLength; } else if (wordCount > 0) { - if (ref - .read( - pNewWalletOptions.state, - ) - .state != - null) { + if (ref.read(pNewWalletOptions.state).state != null) { if (coin.hasMnemonicPassphraseSupport) { - mnemonicPassphrase = ref - .read( - pNewWalletOptions.state, - ) - .state! - .mnemonicPassphrase; + mnemonicPassphrase = + ref.read(pNewWalletOptions.state).state!.mnemonicPassphrase; } else { - // this may not be epiccash specific? - if (coin is Epiccash) { + // this may not be epiccash and sol specific? + if (coin is Epiccash || coin is Solana) { mnemonicPassphrase = ""; } } - wordCount = ref - .read( - pNewWalletOptions.state, - ) - .state! - .mnemonicWordsCount; + wordCount = + ref.read(pNewWalletOptions.state).state!.mnemonicWordsCount; } else { mnemonicPassphrase = ""; } if (wordCount < 12 || 24 < wordCount || wordCount % 3 != 0) { - throw Exception( - "Invalid word count", - ); + throw Exception("Invalid word count"); } final strength = (wordCount ~/ 3) * 32; - mnemonic = bip39.generateMnemonic( - strength: strength, - ); + mnemonic = bip39.generateMnemonic(strength: strength); } final wallet = await Wallet.create( walletInfo: info, mainDB: ref.read(mainDBProvider), secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read( - nodeServiceChangeNotifierProvider, - ), - prefs: ref.read( - prefsChangeNotifierProvider, - ), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), mnemonicPassphrase: mnemonicPassphrase, mnemonic: mnemonic, privateKey: privateKey, @@ -258,23 +232,21 @@ class _NewWalletRecoveryPhraseWarningViewState if (wallet is LibMoneroWallet) { await wallet.init(wordCount: wordCount); + } else if (wallet is LibSalviumWallet) { + await wallet.init(wordCount: wordCount); } else { await wallet.init(); } // set checkbox back to unchecked to annoy users to agree again :P - ref - .read( - checkBoxStateProvider.state, - ) - .state = false; + ref.read(checkBoxStateProvider.state).state = false; final fetchedMnemonic = await (wallet as MnemonicInterface).getMnemonicAsWords(); return (wallet, fetchedMnemonic); } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); rethrow; } } @@ -297,302 +269,309 @@ class _NewWalletRecoveryPhraseWarningViewState return MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: const AppBarBackButton(), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AppBarIconButton( - semanticsLabel: - "Question Button. Opens A Dialog For Recovery Phrase Explanation.", - icon: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + appBar: + isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: const AppBarBackButton(), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AppBarIconButton( + semanticsLabel: + "Question Button. Opens A Dialog For Recovery Phrase Explanation.", + icon: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + onPressed: () async { + await showDialog( + context: context, + builder: + (context) => + const RecoveryPhraseExplanationDialog(), + ); + }, ), - onPressed: () async { - await showDialog( - context: context, - builder: (context) => - const RecoveryPhraseExplanationDialog(), - ); - }, ), - ), - ], - ), + ], + ), body: SingleChildScrollView( child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: isDesktop ? 480 : double.infinity), + constraints: BoxConstraints( + maxWidth: isDesktop ? 480 : double.infinity, + ), child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.all(16), child: Center( child: Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.center - : CrossAxisAlignment.stretch, + crossAxisAlignment: + isDesktop + ? CrossAxisAlignment.center + : CrossAxisAlignment.stretch, children: [ /*if (isDesktop) const Spacer( flex: 10, ),*/ - if (!isDesktop) - const SizedBox( - height: 4, - ), + if (!isDesktop) const SizedBox(height: 4), if (!isDesktop) Text( walletName, textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - if (!isDesktop) - const SizedBox( - height: 4, + style: STextStyles.label( + context, + ).copyWith(fontSize: 12), ), + if (!isDesktop) const SizedBox(height: 4), Text( "Recovery Phrase", textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 32 : 16, + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), + SizedBox(height: isDesktop ? 32 : 16), RoundedWhiteContainer( padding: const EdgeInsets.all(32), width: isDesktop ? 480 : null, - child: isDesktop - ? Text( - "On the next screen you will see " - "$seedCount " - "words that make up your recovery phrase.\n\nPlease " - "write it down. Keep it safe and never share it with " - "anyone. Your recovery phrase is the only way you can" - " access your funds if you forget your PIN, lose your" - " phone, etc.\n\n${AppConfig.appName} does not keep nor is " - "able to restore your recover phrase. Only you have " - "access to your wallet.", - style: isDesktop - ? STextStyles.desktopTextMediumRegular( + child: + isDesktop + ? Text( + "On the next screen you will see " + "$seedCount " + "words that make up your recovery phrase.\n\nPlease " + "write it down. Keep it safe and never share it with " + "anyone. Your recovery phrase is the only way you can" + " access your funds if you forget your PIN, lose your" + " phone, etc.\n\n${AppConfig.appName} does not keep nor is " + "able to restore your recover phrase. Only you have " + "access to your wallet.", + style: + isDesktop + ? STextStyles.desktopTextMediumRegular( + context, + ) + : STextStyles.subtitle( + context, + ).copyWith(fontSize: 12), + ) + : Column( + children: [ + Text( + "Important", + style: STextStyles.desktopH3( context, - ) - : STextStyles.subtitle(context).copyWith( - fontSize: 12, - ), - ) - : Column( - children: [ - Text( - "Important", - style: - STextStyles.desktopH3(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - ), - const SizedBox( - height: 24, - ), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: STextStyles.desktopH3(context) - .copyWith(fontSize: 18), - children: [ - TextSpan( - text: - "On the next screen you will be given ", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - fontSize: 18, - height: 1.3, - ), - ), - TextSpan( - text: "$seedCount words", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, - fontSize: 18, - height: 1.3, - ), - ), - TextSpan( - text: ". They are your ", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - fontSize: 18, - height: 1.3, - ), - ), - TextSpan( - text: "recovery phrase", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) + ).copyWith( + color: + Theme.of(context) .extension()! .accentColorBlue, - fontSize: 18, - height: 1.3, - ), - ), - TextSpan( - text: ".", - style: STextStyles.desktopH3(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - fontSize: 18, - height: 1.3, - ), - ), - ], + ), ), - ), - const SizedBox( - height: 40, - ), - Column( - children: [ - Row( + const SizedBox(height: 24), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: STextStyles.desktopH3( + context, + ).copyWith(fontSize: 18), children: [ - SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - radiusMultiplier: 20, - padding: const EdgeInsets.all(9), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: SvgPicture.asset( - Assets.svg.pencil, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), + TextSpan( + text: + "On the next screen you will be given ", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + fontSize: 18, + height: 1.3, ), ), - const SizedBox( - width: 20, - ), - Text( - "Write them down.", - style: - STextStyles.navBarTitle(context), + TextSpan( + text: "$seedCount words", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorBlue, + fontSize: 18, + height: 1.3, + ), ), - ], - ), - const SizedBox( - height: 30, - ), - Row( - children: [ - SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - radiusMultiplier: 20, - padding: const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: SvgPicture.asset( - Assets.svg.lock, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), + TextSpan( + text: ". They are your ", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + fontSize: 18, + height: 1.3, ), ), - const SizedBox( - width: 20, + TextSpan( + text: "recovery phrase", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorBlue, + fontSize: 18, + height: 1.3, + ), ), - Text( - "Keep them safe.", - style: - STextStyles.navBarTitle(context), + TextSpan( + text: ".", + style: STextStyles.desktopH3( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + fontSize: 18, + height: 1.3, + ), ), ], ), - const SizedBox( - height: 30, - ), - Row( - children: [ - SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - radiusMultiplier: 20, - padding: const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: SvgPicture.asset( - Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .accentColorDark, + ), + const SizedBox(height: 40), + Column( + children: [ + Row( + children: [ + SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + radiusMultiplier: 20, + padding: const EdgeInsets.all(9), + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, + child: SvgPicture.asset( + Assets.svg.pencil, + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), ), ), - ), - const SizedBox( - width: 20, - ), - Expanded( - child: Text( - "Do not show them to anyone.", + const SizedBox(width: 20), + Text( + "Write them down.", style: STextStyles.navBarTitle( context, ), ), - ), - ], - ), - ], - ), - ], - ), + ], + ), + const SizedBox(height: 30), + Row( + children: [ + SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + radiusMultiplier: 20, + padding: const EdgeInsets.all(8), + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, + child: SvgPicture.asset( + Assets.svg.lock, + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), + ), + ), + const SizedBox(width: 20), + Text( + "Keep them safe.", + style: STextStyles.navBarTitle( + context, + ), + ), + ], + ), + const SizedBox(height: 30), + Row( + children: [ + SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + radiusMultiplier: 20, + padding: const EdgeInsets.all(8), + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, + child: SvgPicture.asset( + Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Text( + "Do not show them to anyone.", + style: STextStyles.navBarTitle( + context, + ), + ), + ), + ], + ), + ], + ), + ], + ), ), if (!isDesktop) const Spacer(), - if (!isDesktop) - const SizedBox( - height: 16, - ), - if (isDesktop) - const SizedBox( - height: 32, - ), + if (!isDesktop) const SizedBox(height: 16), + if (isDesktop) const SizedBox(height: 32), ConstrainedBox( constraints: BoxConstraints( maxWidth: isDesktop ? 480 : 0, @@ -605,9 +584,10 @@ class _NewWalletRecoveryPhraseWarningViewState children: [ GestureDetector( onTap: () { - final value = ref - .read(checkBoxStateProvider.state) - .state; + final value = + ref + .read(checkBoxStateProvider.state) + .state; ref.read(checkBoxStateProvider.state).state = !value; }, @@ -623,11 +603,12 @@ class _NewWalletRecoveryPhraseWarningViewState child: Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: ref - .watch( - checkBoxStateProvider.state, - ) - .state, + value: + ref + .watch( + checkBoxStateProvider.state, + ) + .state, onChanged: (newValue) { ref .read( @@ -637,65 +618,67 @@ class _NewWalletRecoveryPhraseWarningViewState }, ), ), - SizedBox( - width: isDesktop ? 20 : 10, - ), + SizedBox(width: isDesktop ? 20 : 10), Flexible( child: Text( "I understand that ${AppConfig.appName} does not keep and cannot restore my recovery phrase, and If I lose my recovery phrase, I will not be able to access my funds.", - style: isDesktop - ? STextStyles.desktopTextMedium( - context, - ) - : STextStyles.baseXS(context) - .copyWith( - height: 1.3, - ), + style: + isDesktop + ? STextStyles.desktopTextMedium( + context, + ) + : STextStyles.baseXS( + context, + ).copyWith(height: 1.3), ), ), ], ), ), ), - SizedBox( - height: isDesktop ? 32 : 16, - ), + SizedBox(height: isDesktop ? 32 : 16), ConstrainedBox( constraints: BoxConstraints( minHeight: isDesktop ? 70 : 0, ), child: TextButton( - onPressed: ref - .read(checkBoxStateProvider.state) - .state - ? _initNewWallet - : null, - style: ref - .read(checkBoxStateProvider.state) - .state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle( - context, - ), - child: Text( - "View recovery phrase", - style: isDesktop - ? ref - .read( - checkBoxStateProvider.state, - ) - .state - ? STextStyles.desktopButtonEnabled( + onPressed: + ref + .read(checkBoxStateProvider.state) + .state + ? _initNewWallet + : null, + style: + ref + .read(checkBoxStateProvider.state) + .state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle( context, ) - : STextStyles.desktopButtonDisabled( + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle( context, - ) - : STextStyles.button(context), + ), + child: Text( + "View recovery phrase", + style: + isDesktop + ? ref + .read( + checkBoxStateProvider + .state, + ) + .state + ? STextStyles.desktopButtonEnabled( + context, + ) + : STextStyles.desktopButtonDisabled( + context, + ) + : STextStyles.button(context), ), ), ), 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 384cacb0c..5cc90beba 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 @@ -8,10 +8,14 @@ * */ +import 'package:cs_monero/src/deprecated/get_height_by_date.dart' + as cs_monero_deprecated; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:logger/logger.dart'; import 'package:tuple/tuple.dart'; import '../../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; @@ -20,6 +24,7 @@ import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/format.dart'; +import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; @@ -27,13 +32,15 @@ import '../../../../wallets/crypto_currency/interfaces/view_only_option_currency import '../../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../../../widgets/custom_buttons/checkbox_text_button.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/date_picker/date_picker.dart'; import '../../../../widgets/desktop/desktop_app_bar.dart'; import '../../../../widgets/desktop/desktop_scaffold.dart'; import '../../../../widgets/expandable.dart'; +import '../../../../widgets/icon_widgets/x_icon.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_text_field.dart'; +import '../../../../widgets/textfield_icon_button.dart'; import '../../../../widgets/toggle.dart'; import '../../create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import '../restore_view_only_wallet_view.dart'; @@ -44,6 +51,8 @@ import 'sub_widgets/restore_from_date_picker.dart'; import 'sub_widgets/restore_options_next_button.dart'; import 'sub_widgets/restore_options_platform_layout.dart'; +final _pIsUsingDate = StateProvider.autoDispose((_) => true); + class RestoreOptionsView extends ConsumerStatefulWidget { const RestoreOptionsView({ super.key, @@ -66,21 +75,19 @@ class _RestoreOptionsViewState extends ConsumerState { late final bool isDesktop; late TextEditingController _dateController; + late TextEditingController _blockHeightController; + late FocusNode _blockHeightFocusNode; late FocusNode textFieldFocusNode; late final FocusNode passwordFocusNode; late final TextEditingController passwordController; - final bool _nextEnabled = true; + bool _hasBlockHeight = false; DateTime? _restoreFromDate; bool hidePassword = true; - bool get supportsMnemonicPassphrase => coin.hasMnemonicPassphraseSupport; - - bool enableLelantusScanning = false; - bool get supportsLelantus => coin is Firo; - @override void initState() { + super.initState(); walletName = widget.walletName; coin = widget.coin; isDesktop = Util.isDesktop; @@ -89,13 +96,26 @@ class _RestoreOptionsViewState extends ConsumerState { textFieldFocusNode = FocusNode(); passwordController = TextEditingController(); passwordFocusNode = FocusNode(); - - super.initState(); + _blockHeightController = TextEditingController(); + _blockHeightFocusNode = FocusNode(); + + _blockHeightController.addListener(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + if (!ref.read(_pIsUsingDate)) { + setState(() { + _hasBlockHeight = _blockHeightController.text.isNotEmpty; + }); + } + } + }); + }); } @override void dispose() { _dateController.dispose(); + _blockHeightController.dispose(); textFieldFocusNode.dispose(); passwordController.dispose(); passwordFocusNode.dispose(); @@ -116,16 +136,21 @@ class _RestoreOptionsViewState extends ConsumerState { } if (mounted) { + int height = 0; + if (ref.read(_pIsUsingDate)) { + height = getBlockHeightFromDate(_restoreFromDate); + } else { + height = int.tryParse(_blockHeightController.text) ?? 0; + } if (!_showViewOnlyOption) { await Navigator.of(context).pushNamed( RestoreWalletView.routeName, - arguments: Tuple6( + arguments: Tuple5( walletName, coin, ref.read(mnemonicWordCountStateProvider.state).state, - _restoreFromDate, + height, passwordController.text, - enableLelantusScanning, ), ); } else { @@ -134,8 +159,7 @@ class _RestoreOptionsViewState extends ConsumerState { arguments: ( walletName: walletName, coin: coin, - restoreFromDate: _restoreFromDate, - enableLelantusScanning: enableLelantusScanning, + restoreBlockHeight: height, ), ); } @@ -174,9 +198,7 @@ class _RestoreOptionsViewState extends ConsumerState { backgroundColor: Colors.transparent, context: context, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) { return MnemonicWordCountSelectSheet( @@ -186,6 +208,46 @@ class _RestoreOptionsViewState extends ConsumerState { ); } + int getBlockHeightFromDate(DateTime? date) { + try { + int height = 0; + if (date != null) { + if (widget.coin is Monero) { + height = cs_monero_deprecated.getMoneroHeightByDate(date: date); + } + if (widget.coin is Wownero) { + height = cs_monero_deprecated.getWowneroHeightByDate(date: date); + } + if (height < 0) { + height = 0; + } + + if (widget.coin is Epiccash) { + final int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; + const int epicCashFirstBlock = 1565370278; + const double overestimateSecondsPerBlock = 61; + final int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; + final int approximateHeight = + chosenSeconds ~/ overestimateSecondsPerBlock; + + height = approximateHeight; + if (height < 0) { + height = 0; + } + } + } else { + height = 0; + } + return height; + } catch (e) { + Logging.instance.log( + Level.info, + "Error getting block height from date: $e", + ); + return 0; + } + } + bool _showViewOnlyOption = false; @override @@ -194,25 +256,27 @@ class _RestoreOptionsViewState extends ConsumerState { return MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () { - if (textFieldFocusNode.hasFocus) { - textFieldFocusNode.unfocus(); - Future.delayed(const Duration(milliseconds: 100)) - .then((value) => Navigator.of(context).pop()); - } else { - Navigator.of(context).pop(); - } - }, + appBar: + isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () { + if (textFieldFocusNode.hasFocus) { + textFieldFocusNode.unfocus(); + Future.delayed( + const Duration(milliseconds: 100), + ).then((value) => Navigator.of(context).pop()); + } else { + Navigator.of(context).pop(); + } + }, + ), ), - ), body: RestoreOptionsPlatformLayout( isDesktop: isDesktop, child: ConstrainedBox( @@ -222,28 +286,18 @@ class _RestoreOptionsViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Spacer( - flex: isDesktop ? 10 : 1, - ), - if (!isDesktop) - CoinImage( - coin: coin, - height: 100, - width: 100, - ), - SizedBox( - height: isDesktop ? 0 : 16, - ), + Spacer(flex: isDesktop ? 10 : 1), + if (!isDesktop) CoinImage(coin: coin, height: 100, width: 100), + SizedBox(height: isDesktop ? 0 : 16), Text( "Restore options", textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 40 : 24, + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), + SizedBox(height: isDesktop ? 40 : 24), if (coin is ViewOnlyOptionCurrencyInterface) SizedBox( height: isDesktop ? 56 : 48, @@ -254,9 +308,10 @@ class _RestoreOptionsViewState extends ConsumerState { offText: "View Only", onColor: Theme.of(context).extension()!.popupBG, - offColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + offColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, isOn: _showViewOnlyOption, onValueChanged: (value) { setState(() { @@ -272,45 +327,37 @@ class _RestoreOptionsViewState extends ConsumerState { ), ), if (coin is ViewOnlyOptionCurrencyInterface) - SizedBox( - height: isDesktop ? 40 : 24, - ), + SizedBox(height: isDesktop ? 40 : 24), _showViewOnlyOption ? ViewOnlyRestoreOption( - coin: coin, - dateController: _dateController, - dateChooserFunction: - isDesktop ? chooseDesktopDate : chooseDate, - ) + coin: coin, + dateController: _dateController, + dateChooserFunction: + isDesktop ? chooseDesktopDate : chooseDate, + blockHeightController: _blockHeightController, + blockHeightFocusNode: _blockHeightFocusNode, + ) : SeedRestoreOption( - coin: coin, - dateController: _dateController, - pwController: passwordController, - pwFocusNode: passwordFocusNode, - supportsMnemonicPassphrase: supportsMnemonicPassphrase, - dateChooserFunction: - isDesktop ? chooseDesktopDate : chooseDate, - chooseMnemonicLength: chooseMnemonicLength, - lelScanChanged: (value) { - enableLelantusScanning = value; - }, - ), - if (!isDesktop) - const Spacer( - flex: 3, - ), - if (isDesktop) - const SizedBox( - height: 32, - ), + coin: coin, + dateController: _dateController, + blockHeightController: _blockHeightController, + blockHeightFocusNode: _blockHeightFocusNode, + pwController: passwordController, + pwFocusNode: passwordFocusNode, + dateChooserFunction: + isDesktop ? chooseDesktopDate : chooseDate, + chooseMnemonicLength: chooseMnemonicLength, + ), + if (!isDesktop) const Spacer(flex: 3), + SizedBox(height: isDesktop ? 32 : 12), RestoreOptionsNextButton( isDesktop: isDesktop, - onPressed: _nextEnabled ? nextPressed : null, + onPressed: + ref.watch(_pIsUsingDate) || _hasBlockHeight + ? nextPressed + : null, ), - if (isDesktop) - const Spacer( - flex: 15, - ), + if (isDesktop) const Spacer(flex: 15), ], ), ), @@ -324,23 +371,23 @@ class SeedRestoreOption extends ConsumerStatefulWidget { super.key, required this.coin, required this.dateController, + required this.blockHeightController, + required this.blockHeightFocusNode, required this.pwController, required this.pwFocusNode, - required this.supportsMnemonicPassphrase, required this.dateChooserFunction, required this.chooseMnemonicLength, - required this.lelScanChanged, }); final CryptoCurrency coin; final TextEditingController dateController; + final TextEditingController blockHeightController; + final FocusNode blockHeightFocusNode; final TextEditingController pwController; final FocusNode pwFocusNode; - final bool supportsMnemonicPassphrase; final Future Function() dateChooserFunction; final Future Function() chooseMnemonicLength; - final void Function(bool) lelScanChanged; @override ConsumerState createState() => _SeedRestoreOptionState(); @@ -349,76 +396,151 @@ class SeedRestoreOption extends ConsumerStatefulWidget { class _SeedRestoreOptionState extends ConsumerState { bool _hidePassword = true; bool _expandedAdvanced = false; - bool _enableLelantusScanning = false; + bool _blockFieldEmpty = true; @override Widget build(BuildContext context) { final lengths = widget.coin.possibleMnemonicLengths; - final isMoneroAnd25 = widget.coin is Monero && - ref.watch(mnemonicWordCountStateProvider.state).state == 25; - final isWowneroAnd25 = widget.coin is Wownero && - ref.watch(mnemonicWordCountStateProvider.state).state == 25; + final currentLength = ref.watch(mnemonicWordCountStateProvider); + + final isCnAnd25 = widget.coin is CryptonoteCurrency && currentLength == 25; + + final bool supportsPassphrase; + if (widget.coin.hasMnemonicPassphraseSupport) { + supportsPassphrase = true; + } else if (widget.coin is CryptonoteCurrency) { + // partial see offset support. Currently only on restore + // and not wownero 14 word seeds + supportsPassphrase = currentLength == 16 || currentLength == 25; + } else { + supportsPassphrase = false; + } return Column( children: [ - if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - Text( - "Choose start date", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), - if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - RestoreFromDatePicker( - onTap: widget.dateChooserFunction, - controller: widget.dateController, - ), - if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - const SizedBox( - height: 8, + if (isCnAnd25 || widget.coin is Epiccash) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + ref.watch(_pIsUsingDate) ? "Choose start date" : "Block height", + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: + ref.watch(_pIsUsingDate) ? "Use block height" : "Use date", + onTap: + () => + ref.read(_pIsUsingDate.notifier).state = + !ref.read(_pIsUsingDate), + ), + ], ), - if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) + if (isCnAnd25 || widget.coin is Epiccash) + SizedBox(height: Util.isDesktop ? 16 : 8), + if (isCnAnd25 || widget.coin is Epiccash) + ref.watch(_pIsUsingDate) + ? RestoreFromDatePicker( + onTap: widget.dateChooserFunction, + controller: widget.dateController, + ) + : ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + focusNode: widget.blockHeightFocusNode, + controller: widget.blockHeightController, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textInputAction: TextInputAction.done, + style: + Util.isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2) + : STextStyles.field(context), + onChanged: (value) { + setState(() { + _blockFieldEmpty = value.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Start scanning from...", + widget.blockHeightFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: TextFieldIconButton( + child: Semantics( + label: + "Clear Block Height Field Button. Clears the block height field", + excludeSemantics: true, + child: + !_blockFieldEmpty + ? XIcon( + width: Util.isDesktop ? 24 : 16, + height: Util.isDesktop ? 24 : 16, + ) + : const SizedBox.shrink(), + ), + onTap: () { + widget.blockHeightController.text = ""; + setState(() { + _blockFieldEmpty = true; + }); + }, + ), + ), + ), + ), + ), + if (isCnAnd25 || widget.coin is Epiccash) const SizedBox(height: 8), + if (isCnAnd25 || widget.coin is Epiccash) RoundedWhiteContainer( child: Center( child: Text( - "Choose the date you made the wallet (approximate is fine)", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) - : STextStyles.smallMed12(context).copyWith( - fontSize: 10, - ), + ref.watch(_pIsUsingDate) + ? "Choose the date you made the wallet (approximate is fine)" + : "Enter the initial block height of the wallet", + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ) + : STextStyles.smallMed12( + context, + ).copyWith(fontSize: 10), ), ), ), - if (isMoneroAnd25 || widget.coin is Epiccash || isWowneroAnd25) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + if (isCnAnd25 || widget.coin is Epiccash) + SizedBox(height: Util.isDesktop ? 24 : 16), Text( "Choose recovery phrase length", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + SizedBox(height: Util.isDesktop ? 16 : 8), if (Util.isDesktop) DropdownButtonHideUnderline( child: DropdownButton2( @@ -441,32 +563,39 @@ class _SeedRestoreOptionState extends ConsumerState { }, isExpanded: true, iconStyleData: IconStyleData( - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + icon: ConditionalParent( + condition: Util.isDesktop, + builder: + (child) => Padding( + padding: const EdgeInsets.only(right: 10), + child: child, + ), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), ), ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ), @@ -474,11 +603,8 @@ class _SeedRestoreOptionState extends ConsumerState { MobileMnemonicLengthSelector( chooseMnemonicLength: widget.chooseMnemonicLength, ), - if (widget.supportsMnemonicPassphrase) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), - if (widget.supportsMnemonicPassphrase) + if (supportsPassphrase) SizedBox(height: Util.isDesktop ? 24 : 16), + if (supportsPassphrase) Expandable( onExpandChanged: (state) { setState(() { @@ -488,25 +614,28 @@ class _SeedRestoreOptionState extends ConsumerState { header: Container( color: Colors.transparent, child: Padding( - padding: const EdgeInsets.only( + padding: EdgeInsets.only( top: 8.0, bottom: 8.0, right: 10, + left: Util.isDesktop ? 16 : 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Advanced", - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ) - : STextStyles.smallMed12(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), SvgPicture.asset( @@ -515,9 +644,10 @@ class _SeedRestoreOptionState extends ConsumerState { : Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ], ), @@ -527,21 +657,7 @@ class _SeedRestoreOptionState extends ConsumerState { color: Colors.transparent, child: Column( children: [ - if (widget.coin is Firo) - CheckboxTextButton( - label: "Scan for Lelantus transactions", - onChanged: (newValue) { - setState(() { - _enableLelantusScanning = newValue ?? true; - }); - - widget.lelScanChanged(_enableLelantusScanning); - }, - ), - if (widget.coin is Firo) - const SizedBox( - height: 8, - ), + if (widget.coin is Firo) const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -550,31 +666,30 @@ class _SeedRestoreOptionState extends ConsumerState { key: const Key("mnemonicPassphraseFieldKey1"), focusNode: widget.pwFocusNode, controller: widget.pwController, - style: Util.isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ) - : STextStyles.field(context), + style: + Util.isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2) + : STextStyles.field(context), obscureText: _hidePassword, enableSuggestions: false, autocorrect: false, decoration: standardInputDecoration( - "BIP39 passphrase", + widget.coin is CryptonoteCurrency + ? "Seed Offset" + : "BIP39 passphrase", widget.pwFocusNode, context, ).copyWith( suffixIcon: UnconstrainedBox( child: ConditionalParent( condition: Util.isDesktop, - builder: (child) => SizedBox( - height: 70, - child: child, - ), + builder: + (child) => SizedBox(height: 70, child: child), child: Row( children: [ - SizedBox( - width: Util.isDesktop ? 24 : 16, - ), + SizedBox(width: Util.isDesktop ? 24 : 16), GestureDetector( key: const Key( "mnemonicPassphraseFieldShowPasswordButtonKey", @@ -588,16 +703,15 @@ class _SeedRestoreOptionState extends ConsumerState { _hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: Util.isDesktop ? 24 : 16, height: Util.isDesktop ? 24 : 16, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -605,29 +719,33 @@ class _SeedRestoreOptionState extends ConsumerState { ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Center( child: Text( - "If the recovery phrase you are about to restore " - "was created with an optional BIP39 passphrase " - "you can enter it here.", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) - : STextStyles.itemSubtitle(context), + widget.coin is CryptonoteCurrency + ? "(Optional) An offset used to derive a different " + "wallet from the given mnemonic, allowing recovery " + "of a hidden or alternate wallet based on the same " + "seed phrase." + : "If the recovery phrase you are about to restore " + "was created with an optional BIP39 passphrase " + "you can enter it here.", + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ) + : STextStyles.itemSubtitle(context), ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ), @@ -635,77 +753,163 @@ class _SeedRestoreOptionState extends ConsumerState { ], ); } + + @override + void initState() { + super.initState(); + + _blockFieldEmpty = widget.blockHeightController.text.isEmpty; + } } -class ViewOnlyRestoreOption extends StatefulWidget { +class ViewOnlyRestoreOption extends ConsumerStatefulWidget { const ViewOnlyRestoreOption({ super.key, required this.coin, required this.dateController, required this.dateChooserFunction, + required this.blockHeightController, + required this.blockHeightFocusNode, }); final CryptoCurrency coin; final TextEditingController dateController; + final TextEditingController blockHeightController; + final FocusNode blockHeightFocusNode; final Future Function() dateChooserFunction; @override - State createState() => _ViewOnlyRestoreOptionState(); + ConsumerState createState() => + _ViewOnlyRestoreOptionState(); } -class _ViewOnlyRestoreOptionState extends State { +class _ViewOnlyRestoreOptionState extends ConsumerState { + bool _blockFieldEmpty = true; + @override Widget build(BuildContext context) { final showDateOption = widget.coin is CryptonoteCurrency; return Column( children: [ if (showDateOption) - Text( - "Choose start date", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - if (showDateOption) - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), - if (showDateOption) - RestoreFromDatePicker( - onTap: widget.dateChooserFunction, - controller: widget.dateController, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + ref.watch(_pIsUsingDate) ? "Choose start date" : "Block height", + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: + ref.watch(_pIsUsingDate) ? "Use block height" : "Use date", + onTap: () { + ref.read(_pIsUsingDate.notifier).state = + !ref.read(_pIsUsingDate); + }, + ), + ], ), + if (showDateOption) SizedBox(height: Util.isDesktop ? 16 : 8), if (showDateOption) - const SizedBox( - height: 8, - ), + ref.watch(_pIsUsingDate) + ? RestoreFromDatePicker( + onTap: widget.dateChooserFunction, + controller: widget.dateController, + ) + : ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + focusNode: widget.blockHeightFocusNode, + controller: widget.blockHeightController, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textInputAction: TextInputAction.done, + style: + Util.isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2) + : STextStyles.field(context), + onChanged: (value) { + setState(() { + _blockFieldEmpty = value.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Start scanning from...", + widget.blockHeightFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: TextFieldIconButton( + child: Semantics( + label: + "Clear Block Height Field Button. Clears the block height field", + excludeSemantics: true, + child: + !_blockFieldEmpty + ? XIcon( + width: Util.isDesktop ? 24 : 16, + height: Util.isDesktop ? 24 : 16, + ) + : const SizedBox.shrink(), + ), + onTap: () { + widget.blockHeightController.text = ""; + setState(() { + _blockFieldEmpty = true; + }); + }, + ), + ), + ), + ), + ), + if (showDateOption) const SizedBox(height: 8), if (showDateOption) RoundedWhiteContainer( child: Center( child: Text( - "Choose the date you made the wallet (approximate is fine)", - style: Util.isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) - : STextStyles.smallMed12(context).copyWith( - fontSize: 10, - ), + ref.watch(_pIsUsingDate) + ? "Choose the date you made the wallet (approximate is fine)" + : "Enter the initial block height of the wallet", + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ) + : STextStyles.smallMed12( + context, + ).copyWith(fontSize: 10), ), ), ), - if (showDateOption) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + if (showDateOption) SizedBox(height: Util.isDesktop ? 24 : 16), ], ); } + + @override + void initState() { + super.initState(); + + _blockFieldEmpty = widget.blockHeightController.text.isEmpty; + } } diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart index 9123b2f46..a473d83ec 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_view_only_wallet_view.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:cs_monero/src/deprecated/get_height_by_date.dart' - as cs_monero_deprecated; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -13,12 +11,10 @@ import 'package:wakelock_plus/wakelock_plus.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/text_styles.dart'; @@ -51,9 +47,7 @@ class RestoreViewOnlyWalletView extends ConsumerStatefulWidget { super.key, required this.walletName, required this.coin, - required this.restoreFromDate, - this.enableLelantusScanning = false, - this.barcodeScanner = const BarcodeScannerWrapper(), + required this.restoreBlockHeight, this.clipboard = const ClipboardWrapper(), }); @@ -61,9 +55,7 @@ class RestoreViewOnlyWalletView extends ConsumerStatefulWidget { final String walletName; final CryptoCurrency coin; - final DateTime? restoreFromDate; - final bool enableLelantusScanning; - final BarcodeScannerInterface barcodeScanner; + final int restoreBlockHeight; final ClipboardInterface clipboard; @override @@ -91,9 +83,7 @@ class _RestoreViewOnlyWalletViewState if (!Util.isDesktop) { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); } if (mounted) { @@ -102,9 +92,7 @@ class _RestoreViewOnlyWalletViewState useSafeArea: false, barrierDismissible: true, builder: (context) { - return ConfirmRecoveryDialog( - onConfirm: _attemptRestore, - ); + return ConfirmRecoveryDialog(onConfirm: _attemptRestore); }, ); } @@ -114,40 +102,17 @@ class _RestoreViewOnlyWalletViewState } Future _attemptRestore() async { - int height = 0; final Map otherDataJson = { WalletInfoKeys.isViewOnlyKey: true, }; final ViewOnlyWalletType viewOnlyWalletType; if (widget.coin is Bip39HDCurrency) { - if (widget.coin is Firo) { - otherDataJson.addAll( - { - WalletInfoKeys.lelantusCoinIsarRescanRequired: false, - WalletInfoKeys.enableLelantusScanning: - widget.enableLelantusScanning, - }, - ); - } - viewOnlyWalletType = _addressOnly - ? ViewOnlyWalletType.addressOnly - : ViewOnlyWalletType.xPub; + viewOnlyWalletType = + _addressOnly + ? ViewOnlyWalletType.addressOnly + : ViewOnlyWalletType.xPub; } else if (widget.coin is CryptonoteCurrency) { - if (widget.restoreFromDate != null) { - if (widget.coin is Monero) { - height = cs_monero_deprecated.getMoneroHeightByDate( - date: widget.restoreFromDate!, - ); - } - if (widget.coin is Wownero) { - height = cs_monero_deprecated.getWowneroHeightByDate( - date: widget.restoreFromDate!, - ); - } - if (height < 0) height = 0; - } - viewOnlyWalletType = ViewOnlyWalletType.cryptonote; } else { throw Exception( @@ -163,7 +128,7 @@ class _RestoreViewOnlyWalletViewState final info = WalletInfo.createNew( coin: widget.coin, name: widget.walletName, - restoreHeight: height, + restoreHeight: widget.restoreBlockHeight, otherDataJsonString: jsonEncode(otherDataJson), ); @@ -181,10 +146,9 @@ class _RestoreViewOnlyWalletViewState onCancel: () async { isRestoring = false; - await ref.read(pWallets).deleteWallet( - info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(info, ref.read(secureStoreProvider)); }, ); }, @@ -234,11 +198,10 @@ class _RestoreViewOnlyWalletViewState .getPrimaryNodeFor(currency: widget.coin); if (node == null) { - node = widget.coin.defaultNode; - await ref.read(nodeServiceChangeNotifierProvider).setPrimaryNodeFor( - coin: widget.coin, - node: node, - ); + node = widget.coin.defaultNode(isPrimary: true); + await ref + .read(nodeServiceChangeNotifierProvider) + .save(node, null, false); } try { @@ -282,21 +245,25 @@ class _RestoreViewOnlyWalletViewState isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(wallet); if (mounted) { if (Util.isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); } else { unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), + Navigator.of( + context, + ).pushNamedAndRemoveUntil(HomeView.routeName, (route) => false), ); } @@ -344,9 +311,10 @@ class _RestoreViewOnlyWalletViewState viewKeyController = TextEditingController(); if (widget.coin is Bip39HDCurrency) { - _currentDropDownValue = (widget.coin as Bip39HDCurrency) - .supportedHardenedDerivationPaths - .last; + _currentDropDownValue = + (widget.coin as Bip39HDCurrency) + .supportedHardenedDerivationPaths + .last; } } @@ -365,27 +333,28 @@ class _RestoreViewOnlyWalletViewState return MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 50), - ); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, + appBar: + isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 50), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), ), - ), body: Container( color: Theme.of(context).extension()!.background, child: LayoutBuilder( @@ -401,28 +370,21 @@ class _RestoreViewOnlyWalletViewState padding: const EdgeInsets.all(16), child: Column( children: [ - if (isDesktop) - const Spacer( - flex: 10, - ), + if (isDesktop) const Spacer(flex: 10), if (!isDesktop) Text( widget.walletName, style: STextStyles.itemSubtitle(context), ), - SizedBox( - height: isDesktop ? 0 : 4, - ), + SizedBox(height: isDesktop ? 0 : 4), Text( "Enter view only details", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), - if (isElectrumX) - SizedBox( - height: isDesktop ? 24 : 16, - ), + if (isElectrumX) SizedBox(height: isDesktop ? 24 : 16), if (isElectrumX) SizedBox( height: isDesktop ? 56 : 48, @@ -431,12 +393,14 @@ class _RestoreViewOnlyWalletViewState key: UniqueKey(), onText: "Extended pub key", offText: "Single address", - onColor: Theme.of(context) - .extension()! - .popupBG, - offColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + onColor: + Theme.of( + context, + ).extension()!.popupBG, + offColor: + Theme.of(context) + .extension()! + .textFieldDefaultBG, isOn: _addressOnly, onValueChanged: (value) { setState(() { @@ -451,9 +415,7 @@ class _RestoreViewOnlyWalletViewState ), ), ), - SizedBox( - height: isDesktop ? 24 : 16, - ), + SizedBox(height: isDesktop ? 24 : 16), if (!isElectrumX || _addressOnly) FullTextField( key: const Key("viewOnlyAddressRestoreFieldKey"), @@ -467,16 +429,14 @@ class _RestoreViewOnlyWalletViewState }); } else { setState(() { - _enableRestoreButton = newValue.isNotEmpty && + _enableRestoreButton = + newValue.isNotEmpty && viewKeyController.text.isNotEmpty; }); } }, ), - if (!isElectrumX) - SizedBox( - height: isDesktop ? 16 : 12, - ), + if (!isElectrumX) SizedBox(height: isDesktop ? 16 : 12), if (isElectrumX && !_addressOnly) DropdownButtonHideUnderline( child: DropdownButton2( @@ -504,9 +464,10 @@ class _RestoreViewOnlyWalletViewState isExpanded: true, buttonStyleData: ButtonStyleData( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -519,9 +480,10 @@ class _RestoreViewOnlyWalletViewState Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), ), @@ -529,9 +491,10 @@ class _RestoreViewOnlyWalletViewState offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -546,9 +509,7 @@ class _RestoreViewOnlyWalletViewState ), ), if (isElectrumX && !_addressOnly) - SizedBox( - height: isDesktop ? 16 : 12, - ), + SizedBox(height: isDesktop ? 16 : 12), if (!isElectrumX || !_addressOnly) FullTextField( key: const Key("viewOnlyKeyRestoreFieldKey"), @@ -563,26 +524,22 @@ class _RestoreViewOnlyWalletViewState }); } else { setState(() { - _enableRestoreButton = value.isNotEmpty && + _enableRestoreButton = + value.isNotEmpty && addressController.text.isNotEmpty; }); } }, ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 24 : 16, - ), + SizedBox(height: isDesktop ? 24 : 16), PrimaryButton( enabled: _enableRestoreButton, onPressed: _requestRestore, width: isDesktop ? 480 : null, label: "Restore", ), - if (isDesktop) - const Spacer( - flex: 15, - ), + if (isDesktop) const Spacer(flex: 15), ], ), ), 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 972012c00..aeb9ff845 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 @@ -17,20 +17,16 @@ import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/src/wordlists/english.dart' as bip39wordlist; import 'package:cs_monero/cs_monero.dart' as lib_monero; -import 'package:cs_monero/src/deprecated/get_height_by_date.dart' - as cs_monero_deprecated; 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:wakelock_plus/wakelock_plus.dart'; - import 'package:xelis_flutter/src/api/seed_search_engine.dart' as x_seed; import '../../../notifications/show_flush_bar.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../services/transaction_notification_tracker.dart'; @@ -49,9 +45,10 @@ import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; +import '../../../wallets/wallet/impl/salvium_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; -import '../../../wallets/wallet/intermediate/external_wallet.dart'; import '../../../wallets/wallet/impl/xelis_wallet.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; import '../../../wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -79,9 +76,7 @@ class RestoreWalletView extends ConsumerStatefulWidget { required this.coin, required this.seedWordsLength, required this.mnemonicPassphrase, - required this.restoreFromDate, - this.enableLelantusScanning = false, - this.barcodeScanner = const BarcodeScannerWrapper(), + required this.restoreBlockHeight, this.clipboard = const ClipboardWrapper(), }); @@ -91,10 +86,8 @@ class RestoreWalletView extends ConsumerStatefulWidget { final CryptoCurrency coin; final String mnemonicPassphrase; final int seedWordsLength; - final DateTime? restoreFromDate; - final bool enableLelantusScanning; + final int restoreBlockHeight; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -114,8 +107,6 @@ class _RestoreWalletViewState extends ConsumerState { final List _inputStatuses = []; // final List _focusNodes = []; - late final BarcodeScannerInterface scanner; - late final TextSelectionControls textSelectionControls; bool _hideSeedWords = false; @@ -160,11 +151,11 @@ class _RestoreWalletViewState extends ConsumerState { _seedWordCount = widget.seedWordsLength; isDesktop = Util.isDesktop; - textSelectionControls = Platform.isIOS - ? CustomCupertinoTextSelectionControls(onPaste: onControlsPaste) - : CustomMaterialTextSelectionControls(onPaste: onControlsPaste); + textSelectionControls = + Platform.isIOS + ? CustomCupertinoTextSelectionControls(onPaste: onControlsPaste) + : CustomMaterialTextSelectionControls(onPaste: onControlsPaste); - scanner = widget.barcodeScanner; for (int i = 0; i < _seedWordCount; i++) { _controllers.add(TextEditingController()); _inputStatuses.add(FormInputStatus.empty); @@ -172,7 +163,9 @@ class _RestoreWalletViewState extends ConsumerState { } if (widget.coin is Xelis) { - _xelisSeedSearch = x_seed.SearchEngine.init(languageIndex: BigInt.from(0)); + _xelisSeedSearch = x_seed.SearchEngine.init( + languageIndex: BigInt.from(0), + ); } super.initState(); @@ -190,7 +183,8 @@ class _RestoreWalletViewState extends ConsumerState { // TODO: check for wownero wordlist? bool _isValidMnemonicWord(String word) { // TODO: get the actual language - if (widget.coin is Monero) { + if (widget.coin is Monero || widget.coin is Salvium) { + // Salvium use's Monero's wordlists. switch (widget.seedWordsLength) { case 25: return lib_monero.getMoneroWordList("English").contains(word); @@ -215,10 +209,7 @@ class _RestoreWalletViewState extends ConsumerState { OutlineInputBorder _buildOutlineInputBorder(Color color) { return OutlineInputBorder( - borderSide: BorderSide( - width: 1, - color: color, - ), + borderSide: BorderSide(width: 1, color: color), borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ); } @@ -233,70 +224,32 @@ class _RestoreWalletViewState extends ConsumerState { } mnemonic = mnemonic.trim(); - int height = 0; + final int height = widget.restoreBlockHeight; String? otherDataJsonString; - if (widget.restoreFromDate != null) { - if (widget.coin is Monero) { - height = cs_monero_deprecated.getMoneroHeightByDate( - date: widget.restoreFromDate!, - ); - } - if (widget.coin is Wownero) { - height = cs_monero_deprecated.getWowneroHeightByDate( - date: widget.restoreFromDate!, - ); - } - if (height < 0) { - height = 0; - } - } - // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index if (widget.coin is Epiccash) { - if (widget.restoreFromDate != null) { - final int secondsSinceEpoch = - widget.restoreFromDate!.millisecondsSinceEpoch ~/ 1000; - const int epicCashFirstBlock = 1565370278; - const double overestimateSecondsPerBlock = 61; - final int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; - final int approximateHeight = - chosenSeconds ~/ overestimateSecondsPerBlock; - - height = approximateHeight; - } - if (height < 0) { - height = 0; - } - - otherDataJsonString = jsonEncode( - { - WalletInfoKeys.epiccashData: jsonEncode( - ExtraEpiccashWalletInfo( - receivingIndex: 0, - changeIndex: 0, - slatesToAddresses: {}, - slatesToCommits: {}, - lastScannedBlock: height, - restoreHeight: height, - creationHeight: height, - ).toMap(), - ), - }, - ); - } else if (widget.coin is Firo) { - otherDataJsonString = jsonEncode( - { - WalletInfoKeys.lelantusCoinIsarRescanRequired: false, - WalletInfoKeys.enableLelantusScanning: - widget.enableLelantusScanning, - }, - ); + otherDataJsonString = jsonEncode({ + WalletInfoKeys.epiccashData: jsonEncode( + ExtraEpiccashWalletInfo( + receivingIndex: 0, + changeIndex: 0, + slatesToAddresses: {}, + slatesToCommits: {}, + lastScannedBlock: height, + restoreHeight: height, + creationHeight: height, + ).toMap(), + ), + }); } // TODO: do actual check to make sure it is a valid mnemonic for monero + xelis if (bip39.validateMnemonic(mnemonic) == false && - !(widget.coin is Monero || widget.coin is Wownero || widget.coin is Xelis)) { + !(widget.coin is Monero || + widget.coin is Wownero || + widget.coin is Salvium || + widget.coin is Xelis)) { unawaited( showFloatingFlushBar( type: FlushBarType.warning, @@ -330,10 +283,9 @@ class _RestoreWalletViewState extends ConsumerState { if (mounted) setState(() => _hideSeedWords = false); - await ref.read(pWallets).deleteWallet( - info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(info, ref.read(secureStoreProvider)); }, ); }, @@ -346,15 +298,15 @@ class _RestoreWalletViewState extends ConsumerState { .getPrimaryNodeFor(currency: widget.coin); if (node == null) { - node = widget.coin.defaultNode; - await ref.read(nodeServiceChangeNotifierProvider).setPrimaryNodeFor( - coin: widget.coin, - node: node, - ); + node = widget.coin.defaultNode(isPrimary: true); + await ref + .read(nodeServiceChangeNotifierProvider) + .save(node, null, false); } - final txTracker = - TransactionNotificationTracker(walletId: info.walletId); + final txTracker = TransactionNotificationTracker( + walletId: info.walletId, + ); try { final wallet = await Wallet.create( @@ -381,6 +333,10 @@ class _RestoreWalletViewState extends ConsumerState { await (wallet as WowneroWallet).init(isRestore: true); break; + case const (SalviumWallet): + await (wallet as SalviumWallet).init(isRestore: true); + break; + case const (XelisWallet): await (wallet as XelisWallet).init(isRestore: true); break; @@ -401,15 +357,24 @@ class _RestoreWalletViewState extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(wallet); - final isCreateSpecialEthWallet = - ref.read(createSpecialEthWalletRoutingFlag); + final isCreateSpecialEthWallet = ref.read( + createSpecialEthWalletRoutingFlag, + ); if (isCreateSpecialEthWallet) { ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; - ref.read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state).state = - !ref + ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state = !ref .read( newEthWalletTriggerTempUntilHiveCompletelyDeleted.state, ) @@ -418,17 +383,13 @@ class _RestoreWalletViewState extends ConsumerState { if (mounted) { if (isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); } else { if (isCreateSpecialEthWallet) { Navigator.of(context).popUntil( - ModalRoute.withName( - SelectWalletForTokenView.routeName, - ), + ModalRoute.withName(SelectWalletForTokenView.routeName), ); } else { unawaited( @@ -521,57 +482,54 @@ class _RestoreWalletViewState extends ConsumerState { break; case FormInputStatus.invalid: color = Theme.of(context).extension()!.textFieldErrorBG; - prefixColor = Theme.of(context) - .extension()! - .textFieldErrorSearchIconLeft; + prefixColor = + Theme.of( + context, + ).extension()!.textFieldErrorSearchIconLeft; borderColor = Theme.of(context).extension()!.textFieldErrorBorder; suffixIcon = SvgPicture.asset( Assets.svg.alertCircle, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .textFieldErrorSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldErrorSearchIconRight, ); break; case FormInputStatus.valid: color = Theme.of(context).extension()!.textFieldSuccessBG; - prefixColor = Theme.of(context) - .extension()! - .textFieldSuccessSearchIconLeft; + prefixColor = + Theme.of( + context, + ).extension()!.textFieldSuccessSearchIconLeft; borderColor = Theme.of(context).extension()!.textFieldSuccessBorder; suffixIcon = SvgPicture.asset( Assets.svg.checkCircle, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .textFieldSuccessSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldSuccessSearchIconRight, ); break; } return InputDecoration( fillColor: color, filled: true, - contentPadding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), + contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), prefixIcon: Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.only( - left: 12, - bottom: 2, - ), + padding: const EdgeInsets.only(left: 12, bottom: 2), child: Text( prefix, - style: STextStyles.fieldLabel(context).copyWith( - color: prefixColor, - fontSize: Util.isDesktop ? 16 : 14, - ), + style: STextStyles.fieldLabel( + context, + ).copyWith(color: prefixColor, fontSize: Util.isDesktop ? 16 : 14), ), ), ), @@ -638,7 +596,7 @@ class _RestoreWalletViewState extends ConsumerState { Future scanMnemonicQr() async { try { - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final results = AddressUtils.decodeQRSeedData(qrResult.rawContent); @@ -655,19 +613,35 @@ class _RestoreWalletViewState extends ConsumerState { } } on PlatformException catch (e, s) { // likely failed to get camera permissions - Logging.instance.e( - "Restore wallet qr scan failed: $e", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Restore wallet qr scan failed: $e", + error: e, + stackTrace: s, + ); + } } } Future pasteMnemonic() async { //todo: check if print needed // debugPrint("restoreWalletPasteButton tapped"); - final ClipboardData? data = - await widget.clipboard.getData(Clipboard.kTextPlain); + final ClipboardData? data = await widget.clipboard.getData( + Clipboard.kTextPlain, + ); if (data?.text != null && data!.text!.isNotEmpty) { final content = data.text!.trim(); @@ -679,9 +653,7 @@ class _RestoreWalletViewState extends ConsumerState { Future requestRestore() async { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); if (mounted) { await showDialog( @@ -689,9 +661,7 @@ class _RestoreWalletViewState extends ConsumerState { useSafeArea: false, barrierDismissible: true, builder: (context) { - return ConfirmRecoveryDialog( - onConfirm: attemptRestore, - ); + return ConfirmRecoveryDialog(onConfirm: attemptRestore); }, ); } @@ -702,85 +672,90 @@ class _RestoreWalletViewState extends ConsumerState { final isDesktop = Util.isDesktop; return MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 50), - ); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - semanticsLabel: - "View QR Code Button. Opens Camera To Scan QR Code For Restoring Wallet.", - key: const Key("restoreWalletViewQrCodeButton"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: QrCodeIcon( - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + appBar: + isDesktop + ? const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 50), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + semanticsLabel: + "View QR Code Button. Opens Camera To Scan QR Code For Restoring Wallet.", + key: const Key("restoreWalletViewQrCodeButton"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: QrCodeIcon( + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + onPressed: scanMnemonicQr, ), - onPressed: scanMnemonicQr, ), ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard For Restoring Wallet.", - key: const Key("restoreWalletPasteButton"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: ClipboardIcon( - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard For Restoring Wallet.", + key: const Key("restoreWalletPasteButton"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: ClipboardIcon( + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + onPressed: pasteMnemonic, ), - onPressed: pasteMnemonic, ), ), - ), - ], - ), + ], + ), body: Container( color: Theme.of(context).extension()!.background, child: Padding( @@ -798,27 +773,23 @@ class _RestoreWalletViewState extends ConsumerState { widget.walletName, style: STextStyles.itemSubtitle(context), ), - SizedBox( - height: isDesktop ? 0 : 4, - ), + SizedBox(height: isDesktop ? 0 : 4), Text( "Recovery phrase", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 16 : 8, + 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, + style: + isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), ), + SizedBox(height: isDesktop ? 16 : 10), if (isDesktop) Row( mainAxisAlignment: MainAxisAlignment.center, @@ -836,19 +807,18 @@ class _RestoreWalletViewState extends ConsumerState { Assets.svg.clipboard, width: 22, height: 22, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - const SizedBox( - width: 8, + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), + const SizedBox(width: 8), Text( "Paste", - style: STextStyles - .desktopButtonSmallSecondaryEnabled( - context, - ), + style: + STextStyles.desktopButtonSmallSecondaryEnabled( + context, + ), ), ], ), @@ -856,15 +826,10 @@ class _RestoreWalletViewState extends ConsumerState { ), ], ), - if (isDesktop) - const SizedBox( - height: 20, - ), + if (isDesktop) const SizedBox(height: 20), if (isDesktop) ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 1008, - ), + constraints: const BoxConstraints(maxWidth: 1008), child: Builder( builder: (BuildContext context) { const cols = 4; @@ -901,24 +866,23 @@ class _RestoreWalletViewState extends ConsumerState { ), decoration: _getInputDecorationFor( - _inputStatuses[ - i * 4 + j - 1], - "${i * 4 + j}", - ), + _inputStatuses[i * 4 + + j - + 1], + "${i * 4 + j}", + ), autovalidateMode: AutovalidateMode .onUserInteraction, - selectionControls: i * 4 + - j - - 1 == - 1 - ? textSelectionControls - : null, + selectionControls: + i * 4 + j - 1 == 1 + ? textSelectionControls + : null, // focusNode: // _focusNodes[i * 4 + j - 1], onChanged: (value) { final FormInputStatus - formInputStatus; + formInputStatus; if (value.isEmpty) { formInputStatus = @@ -950,25 +914,31 @@ class _RestoreWalletViewState extends ConsumerState { // } setState(() { _inputStatuses[i * 4 + - j - - 1] = formInputStatus; + j - + 1] = + formInputStatus; }); }, - controller: _controllers[ - i * 4 + j - 1], - style: - STextStyles.field(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textRestore, + 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] == + if (_inputStatuses[i * 4 + + j - + 1] == FormInputStatus.invalid) Align( alignment: @@ -976,23 +946,22 @@ class _RestoreWalletViewState extends ConsumerState { child: Padding( padding: const EdgeInsets.only( - left: 12.0, - bottom: 4.0, - ), + left: 12.0, + bottom: 4.0, + ), child: Text( "Please check spelling", textAlign: TextAlign.left, - style: - STextStyles.label( + style: STextStyles.label( context, ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textError, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textError, ), ), ), @@ -1007,19 +976,23 @@ class _RestoreWalletViewState extends ConsumerState { TableViewRow( spacing: 16, cells: [ - for (int i = rows * cols; - i < _seedWordCount - remainder; - i++) ...[ + for ( + int i = rows * cols; + i < _seedWordCount - remainder; + i++ + ) ...[ const TableViewCell( flex: 1, child: Column( - // ... (existing code for input field) - ), + // ... (existing code for input field) + ), ), ], - for (int i = _seedWordCount - remainder; - i < _seedWordCount; - i++) ...[ + for ( + int i = _seedWordCount - remainder; + i < _seedWordCount; + i++ + ) ...[ TableViewCell( flex: 1, child: Column( @@ -1035,18 +1008,19 @@ class _RestoreWalletViewState extends ConsumerState { ), decoration: _getInputDecorationFor( - _inputStatuses[i], - "${i + 1}", - ), + _inputStatuses[i], + "${i + 1}", + ), autovalidateMode: AutovalidateMode .onUserInteraction, - selectionControls: i == 1 - ? textSelectionControls - : null, + selectionControls: + i == 1 + ? textSelectionControls + : null, onChanged: (value) { final FormInputStatus - formInputStatus; + formInputStatus; if (value.isEmpty) { formInputStatus = @@ -1070,13 +1044,15 @@ class _RestoreWalletViewState extends ConsumerState { }); }, controller: _controllers[i], - style: - STextStyles.field(context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .overlay, + style: STextStyles.field( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .overlay, fontSize: isDesktop ? 16 : 14, ), @@ -1089,23 +1065,22 @@ class _RestoreWalletViewState extends ConsumerState { child: Padding( padding: const EdgeInsets.only( - left: 12.0, - bottom: 4.0, - ), + left: 12.0, + bottom: 4.0, + ), child: Text( "Please check spelling", textAlign: TextAlign.left, - style: - STextStyles.label( + style: STextStyles.label( context, ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textError, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textError, ), ), ), @@ -1114,9 +1089,11 @@ class _RestoreWalletViewState extends ConsumerState { ), ), ], - for (int i = 0; - i < cols - remainder; - i++) ...[ + for ( + int i = 0; + i < cols - remainder; + i++ + ) ...[ TableViewCell( flex: 1, child: Container(), @@ -1128,9 +1105,7 @@ class _RestoreWalletViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), PrimaryButton( label: "Restore wallet", width: 480, @@ -1157,8 +1132,9 @@ class _RestoreWalletViewState extends ConsumerState { Column( children: [ Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric( + vertical: 4, + ), child: TextFormField( obscureText: _hideSeedWords, autocorrect: !isDesktop, @@ -1202,9 +1178,10 @@ class _RestoreWalletViewState extends ConsumerState { }, controller: _controllers[i - 1], style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension()! - .textRestore, + color: + Theme.of(context) + .extension()! + .textRestore, fontSize: isDesktop ? 16 : 14, ), ), @@ -1221,11 +1198,13 @@ class _RestoreWalletViewState extends ConsumerState { child: Text( "Please check spelling", textAlign: TextAlign.left, - style: - STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .textError, + style: STextStyles.label( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textError, ), ), ), @@ -1233,9 +1212,7 @@ class _RestoreWalletViewState extends ConsumerState { ], ), Padding( - padding: const EdgeInsets.only( - top: 8.0, - ), + padding: const EdgeInsets.only(top: 8.0), child: PrimaryButton( onPressed: requestRestore, label: "Restore", diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart index 933b72ba5..6cdaa5423 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart @@ -17,10 +17,7 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; class MnemonicWordCountSelectSheet extends ConsumerWidget { - const MnemonicWordCountSelectSheet({ - super.key, - required this.lengthOptions, - }); + const MnemonicWordCountSelectSheet({super.key, required this.lengthOptions}); final List lengthOptions; @@ -35,9 +32,7 @@ class MnemonicWordCountSelectSheet extends ConsumerWidget { child: Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Padding( padding: const EdgeInsets.only( @@ -46,116 +41,115 @@ class MnemonicWordCountSelectSheet extends ConsumerWidget { top: 10, bottom: 0, ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), + width: 60, + height: 4, ), - width: 60, - height: 4, ), - ), - const SizedBox( - height: 36, - ), - // Expanded( - // child: SingleChildScrollView( - // child: - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Phrase length", - style: STextStyles.pageTitleH2(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 16, - ), - for (int i = 0; i < lengthOptions.length; i++) - Column( - children: [ - GestureDetector( - onTap: () { - final state = ref - .read(mnemonicWordCountStateProvider.state) - .state; - if (state != lengthOptions[i]) { - ref - .read(mnemonicWordCountStateProvider.state) - .state = lengthOptions[i]; - } + const SizedBox(height: 36), + // Expanded( + // child: SingleChildScrollView( + // child: + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Phrase length", + style: STextStyles.pageTitleH2(context), + textAlign: TextAlign.left, + ), + const SizedBox(height: 16), + for (int i = 0; i < lengthOptions.length; i++) + Column( + children: [ + GestureDetector( + onTap: () { + final state = + ref + .read( + mnemonicWordCountStateProvider.state, + ) + .state; + if (state != lengthOptions[i]) { + ref + .read(mnemonicWordCountStateProvider.state) + .state = lengthOptions[i]; + } - Navigator.of(context).pop(); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Column( - // mainAxisAlignment: MainAxisAlignment.start, - // children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: lengthOptions[i], - groupValue: ref - .watch( - mnemonicWordCountStateProvider.state, - ) - .state, - onChanged: (x) { - ref - .read( - mnemonicWordCountStateProvider - .state, - ) - .state = lengthOptions[i]; - Navigator.of(context).pop(); - }, + Navigator.of(context).pop(); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Column( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: lengthOptions[i], + groupValue: + ref + .watch( + mnemonicWordCountStateProvider + .state, + ) + .state, + onChanged: (x) { + ref + .read( + mnemonicWordCountStateProvider + .state, + ) + .state = lengthOptions[i]; + Navigator.of(context).pop(); + }, + ), ), - ), - // ], - // ), - const SizedBox( - width: 12, - ), - Text( - "${lengthOptions[i]} words", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + // ], + // ), + const SizedBox(width: 12), + Text( + "${lengthOptions[i]} words", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), - ), - const SizedBox( - height: 16, - ), - ], - ), - const SizedBox( - height: 8, - ), - ], - ), - // ), - // ) - ], + const SizedBox(height: 16), + ], + ), + const SizedBox(height: 8), + ], + ), + // ), + // ) + ], + ), ), ), ), diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 04dc94d33..c562ec88e 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -14,6 +14,8 @@ import 'dart:math'; import 'package:cs_monero/src/deprecated/get_height_by_date.dart' as cs_monero_deprecated; +import 'package:cs_salvium/src/deprecated/get_height_by_date.dart' + as cs_salvium_deprecated; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -23,7 +25,6 @@ import '../../../models/keys/view_only_wallet_data.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; @@ -36,12 +37,12 @@ import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; -import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; @@ -116,16 +117,9 @@ class _VerifyRecoveryPhraseViewState final ViewOnlyWalletType viewOnlyWalletType; if (widget.wallet is ExtendedKeysInterface) { - if (widget.wallet.cryptoCurrency is Firo) { - otherDataJson.addAll( - { - WalletInfoKeys.lelantusCoinIsarRescanRequired: false, - WalletInfoKeys.enableLelantusScanning: false, - }, - ); - } viewOnlyWalletType = ViewOnlyWalletType.xPub; - } else if (widget.wallet is LibMoneroWallet) { + } else if (widget.wallet is LibMoneroWallet || + widget.wallet is LibSalviumWallet) { if (widget.wallet.cryptoCurrency is Monero) { height = cs_monero_deprecated.getMoneroHeightByDate( date: DateTime.now().subtract(const Duration(days: 7)), @@ -136,6 +130,11 @@ class _VerifyRecoveryPhraseViewState date: DateTime.now().subtract(const Duration(days: 7)), ); } + if (widget.wallet.cryptoCurrency is Salvium) { + height = cs_salvium_deprecated.getSalviumHeightByDate( + date: DateTime.now().subtract(const Duration(days: 7)), + ); + } if (height < 0) height = 0; viewOnlyWalletType = ViewOnlyWalletType.cryptonote; @@ -184,8 +183,25 @@ class _VerifyRecoveryPhraseViewState } else if (widget.wallet is LibMoneroWallet) { final w = widget.wallet as LibMoneroWallet; - final info = await w - .hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing(); + final info = + await w + .hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing(); + final address = info.$1; + final privateViewKey = info.$2; + + await w.exit(); + + viewOnlyData = CryptonoteViewOnlyWalletData( + walletId: voInfo.walletId, + address: address, + privateViewKey: privateViewKey, + ); + } else if (widget.wallet is LibSalviumWallet) { + final w = widget.wallet as LibSalviumWallet; + + final info = + await w + .hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing(); final address = info.$1; final privateViewKey = info.$2; @@ -241,23 +257,27 @@ class _VerifyRecoveryPhraseViewState isar: ref.read(mainDBProvider).isar, ); + if (ref.read(pDuress)) { + await voWallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } + ref.read(pWallets).addWallet(voWallet); await voWallet.exit(); - await ref.read(pWallets).deleteWallet( - widget.wallet.info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(widget.wallet.info, ref.read(secureStoreProvider)); } catch (e) { - await ref.read(pWallets).deleteWallet( - widget.wallet.info, - ref.read(secureStoreProvider), - ); - await ref.read(pWallets).deleteWallet( - voWallet.info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(widget.wallet.info, ref.read(secureStoreProvider)); + await ref + .read(pWallets) + .deleteWallet(voWallet.info, ref.read(secureStoreProvider)); rethrow; } @@ -274,20 +294,27 @@ class _VerifyRecoveryPhraseViewState } } - await ref.read(pWalletInfo(widget.wallet.walletId)).setMnemonicVerified( - isar: ref.read(mainDBProvider).isar, - ); + await widget.wallet.info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + if (ref.read(pDuress)) { + await widget.wallet.info.updateDuressVisibilityStatus( + isDuressVisible: true, + isar: ref.read(mainDBProvider).isar, + ); + } ref.read(pWallets).addWallet(widget.wallet); - final isCreateSpecialEthWallet = - ref.read(createSpecialEthWalletRoutingFlag); + final isCreateSpecialEthWallet = ref.read( + createSpecialEthWalletRoutingFlag, + ); if (isCreateSpecialEthWallet) { ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; ref - .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) - .state = - !ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state = !ref .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) .state; } @@ -311,23 +338,22 @@ class _VerifyRecoveryPhraseViewState throw ex!; } } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), + builder: + (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), ); } if (mounted) { Navigator.of(context).popUntil( - ModalRoute.withName( - NewWalletRecoveryPhraseView.routeName, - ), + ModalRoute.withName(NewWalletRecoveryPhraseView.routeName), ); } return; @@ -337,17 +363,13 @@ class _VerifyRecoveryPhraseViewState if (mounted) { if (isDesktop) { if (isCreateSpecialEthWallet) { - Navigator.of(context).popUntil( - ModalRoute.withName( - SelectWalletForTokenView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(SelectWalletForTokenView.routeName)); } else { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(DesktopHomeView.routeName)); if (_coin is Ethereum) { unawaited( Navigator.of(context).pushNamed( @@ -359,17 +381,14 @@ class _VerifyRecoveryPhraseViewState } } else { if (isCreateSpecialEthWallet) { - Navigator.of(context).popUntil( - ModalRoute.withName( - SelectWalletForTokenView.routeName, - ), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(SelectWalletForTokenView.routeName)); } else { unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), + Navigator.of( + context, + ).pushNamedAndRemoveUntil(HomeView.routeName, (route) => false), ); if (_coin is Ethereum) { unawaited( @@ -460,10 +479,9 @@ class _VerifyRecoveryPhraseViewState } Future delete() async { - await ref.read(pWallets).deleteWallet( - widget.wallet.info, - ref.read(secureStoreProvider), - ); + await ref + .read(pWallets) + .deleteWallet(widget.wallet.info, ref.read(secureStoreProvider)); } @override @@ -476,40 +494,41 @@ class _VerifyRecoveryPhraseViewState onWillPop: onWillPop, child: MasterScaffold( isDesktop: isDesktop, - appBar: isDesktop - ? DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).popUntil( - ModalRoute.withName( - NewWalletRecoveryPhraseView.routeName, - ), - ); - }, - ), - trailing: ExitToMyStackButton( - onPressed: () async { - await delete(); - if (context.mounted) { + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton( + onPressed: () async { Navigator.of(context).popUntil( - ModalRoute.withName(DesktopHomeView.routeName), + ModalRoute.withName( + NewWalletRecoveryPhraseView.routeName, + ), ); - } - }, - ), - ) - : AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).popUntil( - ModalRoute.withName( - NewWalletRecoveryPhraseView.routeName, - ), - ); - }, + }, + ), + trailing: ExitToMyStackButton( + onPressed: () async { + await delete(); + if (context.mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopHomeView.routeName), + ); + } + }, + ), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).popUntil( + ModalRoute.withName( + NewWalletRecoveryPhraseView.routeName, + ), + ); + }, + ), ), - ), body: SizedBox( width: isDesktop ? 410 : null, child: Padding( @@ -518,40 +537,32 @@ class _VerifyRecoveryPhraseViewState child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (isDesktop) - const Spacer( - flex: 10, - ), - SizedBox( - height: isDesktop ? 24 : 4, - ), + if (isDesktop) const Spacer(flex: 10), + SizedBox(height: isDesktop ? 24 : 4), Text( "Verify recovery phrase", textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - SizedBox( - height: isDesktop ? 16 : 4, + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.label(context).copyWith(fontSize: 12), ), + SizedBox(height: isDesktop ? 16 : 4), Text( isDesktop ? "Select word number" : "Tap word number ", textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.desktopSubtitleH1(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 16 : 12, + style: + isDesktop + ? STextStyles.desktopSubtitleH1(context) + : STextStyles.pageTitleH1(context), ), + SizedBox(height: isDesktop ? 16 : 12), Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -564,76 +575,79 @@ class _VerifyRecoveryPhraseViewState child: Text( "${correctIndex + 1}", textAlign: TextAlign.center, - style: STextStyles.subtitle600(context).copyWith( - fontSize: 32, - letterSpacing: 0.25, - ), + style: STextStyles.subtitle600( + context, + ).copyWith(fontSize: 32, letterSpacing: 0.25), ), ), ), - if (isDesktop) - const SizedBox( - height: 40, - ), + if (isDesktop) const SizedBox(height: 40), WordTable( words: randomize(_mnemonic, correctIndex, 9).item1, isDesktop: isDesktop, ), if (!isDesktop) const Spacer(), - if (isDesktop) - const SizedBox( - height: 40, - ), + if (isDesktop) const SizedBox(height: 40), Row( children: [ Expanded( child: Consumer( builder: (_, ref, __) { - final selectedWord = ref - .watch( - verifyMnemonicSelectedWordStateProvider.state, - ) - .state; - final correctWord = ref - .watch( - verifyMnemonicCorrectWordStateProvider.state, - ) - .state; + final selectedWord = + ref + .watch( + verifyMnemonicSelectedWordStateProvider + .state, + ) + .state; + final correctWord = + ref + .watch( + verifyMnemonicCorrectWordStateProvider + .state, + ) + .state; return ConstrainedBox( constraints: BoxConstraints( minHeight: isDesktop ? 70 : 0, ), child: TextButton( - onPressed: selectedWord.isNotEmpty - ? () async { - await _continue( - correctWord == selectedWord, - ); - } - : null, - style: selectedWord.isNotEmpty - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: isDesktop - ? Text( - "Verify", - style: selectedWord.isNotEmpty - ? STextStyles.desktopButtonEnabled( - context, - ) - : STextStyles.desktopButtonDisabled( - context, - ), - ) - : Text( - "Continue", - style: STextStyles.button(context), - ), + onPressed: + selectedWord.isNotEmpty + ? () async { + await _continue( + correctWord == selectedWord, + ); + } + : null, + style: + selectedWord.isNotEmpty + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle( + context, + ), + child: + isDesktop + ? Text( + "Verify", + style: + selectedWord.isNotEmpty + ? STextStyles.desktopButtonEnabled( + context, + ) + : STextStyles.desktopButtonDisabled( + context, + ), + ) + : Text( + "Continue", + style: STextStyles.button(context), + ), ), ); }, @@ -641,10 +655,7 @@ class _VerifyRecoveryPhraseViewState ), ], ), - if (isDesktop) - const Spacer( - flex: 15, - ), + if (isDesktop) const Spacer(flex: 15), ], ), ), diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index 852803f6f..64af27507 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -15,7 +15,6 @@ import 'package:flutter_svg/svg.dart'; import '../../app_config.dart'; import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/isar/models/contact_entry.dart'; -import '../../providers/db/main_db_provider.dart'; import '../../providers/global/address_book_service_provider.dart'; import '../../providers/providers.dart'; import '../../providers/ui/address_book_providers/address_book_filter_provider.dart'; @@ -38,11 +37,7 @@ import 'subviews/add_address_book_entry_view.dart'; import 'subviews/address_book_filter_view.dart'; class AddressBookView extends ConsumerStatefulWidget { - const AddressBookView({ - super.key, - this.coin, - this.filterTerm, - }); + const AddressBookView({super.key, this.coin, this.filterTerm}); static const String routeName = "/addressBook"; @@ -67,9 +62,7 @@ class _AddressBookViewState extends ConsumerState { if (widget.coin == null) { final coins = [...AppConfig.coins]; - coins.removeWhere( - (e) => e is Firo && e.network.isTestNet, - ); + coins.removeWhere((e) => e is Firo && e.network.isTestNet); final bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; @@ -77,7 +70,9 @@ class _AddressBookViewState extends ConsumerState { if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); } else { - ref.read(addressBookFilterProvider).addAll( + ref + .read(addressBookFilterProvider) + .addAll( coins.where((e) => e.network != CryptoCurrencyNetwork.test), false, ); @@ -132,8 +127,9 @@ class _AddressBookViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final contacts = - ref.watch(addressBookServiceProvider.select((value) => value.contacts)); + final contacts = ref.watch( + addressBookServiceProvider.select((value) => value.contacts), + ); final isDesktop = Util.isDesktop; return ConditionalParent( @@ -166,21 +162,23 @@ class _AddressBookViewState extends ConsumerState { key: const Key("addressBookFilterViewButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), onPressed: () { - Navigator.of(context).pushNamed( - AddressBookFilterView.routeName, - ); + Navigator.of( + context, + ).pushNamed(AddressBookFilterView.routeName); }, ), ), @@ -197,56 +195,60 @@ class _AddressBookViewState extends ConsumerState { key: const Key("addressBookAddNewContactViewButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.plus, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), onPressed: () { - Navigator.of(context).pushNamed( - AddAddressBookEntryView.routeName, - ); + Navigator.of( + context, + ).pushNamed(AddAddressBookEntryView.routeName); }, ), ), ), ], ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: - MediaQuery.of(context).size.height - 271, + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + MediaQuery.of(context).size.height - 271, + ), + child: child, ), - child: child, ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); @@ -258,66 +260,63 @@ class _AddressBookViewState extends ConsumerState { borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), - child: !isDesktop - ? TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + child: + !isDesktop + ? TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; - _searchTerm = ""; - }); - }, + 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 = ""; + _searchTerm = ""; + }); + }, + ), + ], ), - ], - ), - ), - ) - : null, - ), - ) - : null, + ), + ) + : null, + ), + ) + : null, ), if (!isDesktop) const SizedBox(height: 16), - Text( - "Favorites", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), + Text("Favorites", style: STextStyles.smallMed12(context)), + const SizedBox(height: 12), if (contacts.isNotEmpty) RoundedWhiteContainer( padding: EdgeInsets.all(!isDesktop ? 0 : 15), @@ -325,15 +324,16 @@ class _AddressBookViewState extends ConsumerState { children: [ ...contacts .where( - (element) => element.addressesSorted - .where( - (e) => ref.watch( - addressBookFilterProvider.select( - (value) => value.coins.contains(e.coin), - ), - ), - ) - .isNotEmpty, + (element) => + element.addressesSorted + .where( + (e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), + ), + ) + .isNotEmpty, ) .where( (e) => @@ -361,16 +361,9 @@ class _AddressBookViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), - Text( - "All contacts", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 16), + Text("All contacts", style: STextStyles.smallMed12(context)), + const SizedBox(height: 12), if (contacts.isNotEmpty) Column( children: [ @@ -382,15 +375,17 @@ class _AddressBookViewState extends ConsumerState { children: [ ...contacts .where( - (element) => element.addressesSorted - .where( - (e) => ref.watch( - addressBookFilterProvider.select( - (value) => value.coins.contains(e.coin), - ), - ), - ) - .isNotEmpty, + (element) => + element.addressesSorted + .where( + (e) => ref.watch( + addressBookFilterProvider.select( + (value) => + value.coins.contains(e.coin), + ), + ), + ) + .isNotEmpty, ) .where( (e) => ref @@ -399,8 +394,9 @@ class _AddressBookViewState extends ConsumerState { ) .map( (e) => AddressBookCard( - key: - Key("desktopContactCard_${e.customId}_key"), + key: Key( + "desktopContactCard_${e.customId}_key", + ), contactId: e.customId, ), ), diff --git a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart index 66b4b1aa9..84e9be9fa 100644 --- a/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart +++ b/lib/pages/address_book_views/subviews/add_address_book_entry_view.dart @@ -21,7 +21,6 @@ import '../../../providers/ui/address_book_providers/contact_name_is_not_empty_s import '../../../providers/ui/address_book_providers/valid_contact_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/text_styles.dart'; @@ -43,13 +42,11 @@ import 'new_contact_address_entry_form.dart'; class AddAddressBookEntryView extends ConsumerStatefulWidget { const AddAddressBookEntryView({ super.key, - this.barcodeScanner = const BarcodeScannerWrapper(), this.clipboard = const ClipboardWrapper(), }); static const String routeName = "/addAddressBookEntry"; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -63,7 +60,6 @@ class _AddAddressBookEntryViewState late final FocusNode nameFocusNode; late final ScrollController scrollController; - late final BarcodeScannerInterface scanner; late final ClipboardInterface clipboard; Emoji? _selectedEmoji; @@ -72,7 +68,7 @@ class _AddAddressBookEntryViewState @override initState() { ref.refresh(addressEntryDataProviderFamilyRefresher); - scanner = widget.barcodeScanner; + clipboard = widget.clipboard; nameController = TextEditingController(); @@ -114,7 +110,6 @@ class _AddAddressBookEntryViewState key: Key("contactAddressEntryForm_$id"), id: ref.read(addressEntryDataProvider(id)).id, clipboard: clipboard, - barcodeScanner: scanner, ), ); setState(() {}); @@ -163,18 +158,20 @@ class _AddAddressBookEntryViewState key: const Key("addAddressBookEntryFavoriteButtonKey"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.star, - color: _isFavorite - ? Theme.of(context) - .extension()! - .favoriteStarActive - : Theme.of(context) - .extension()! - .favoriteStarInactive, + color: + _isFavorite + ? Theme.of( + context, + ).extension()!.favoriteStarActive + : Theme.of(context) + .extension()! + .favoriteStarInactive, width: 20, height: 20, ), @@ -188,7 +185,7 @@ class _AddAddressBookEntryViewState ), ], ), - body: child, + body: SafeArea(child: child), ), ); }, @@ -252,155 +249,160 @@ class _AddAddressBookEntryViewState if (!isDesktop) const SizedBox(height: 4), isDesktop ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - height: 56, - width: 56, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - if (_selectedEmoji != null) { - setState(() { - _selectedEmoji = null; - }); - return; - } + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: 56, + width: 56, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } - showDialog( - context: context, - builder: (context) { - return const DesktopDialog( - maxHeight: 700, - maxWidth: 600, - child: Padding( - padding: EdgeInsets.only( - left: 32, - right: 20, - top: 32, - bottom: 32, - ), - child: EmojiSelectSheet(), + showDialog( + context: context, + builder: (context) { + return const DesktopDialog( + maxHeight: 700, + maxWidth: 600, + child: Padding( + padding: EdgeInsets.only( + left: 32, + right: 20, + top: 32, + bottom: 32, ), - ); - }, - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); - }, - child: Stack( - children: [ - Container( - height: 56, - width: 56, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(100), - color: Theme.of(context) - .extension()! - .textFieldActiveBG, + child: EmojiSelectSheet(), ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( + ); + }, + ).then((value) { + if (value is Emoji) { + setState(() { + _selectedEmoji = value; + }); + } + }); + }, + child: Stack( + children: [ + Container( + height: 56, + width: 56, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(100), + color: + Theme.of(context) + .extension()! + .textFieldActiveBG, + ), + child: Center( + child: + _selectedEmoji == null + ? SvgPicture.asset( Assets.svg.user, height: 30, width: 30, ) - : Text( + : Text( _selectedEmoji!.char, - style: STextStyles - .pageTitleH1( - context, - ), + style: + STextStyles.pageTitleH1( + context, + ), ), - ), ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular( - 14, - ), - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(14), + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), + child: Center( + child: + _selectedEmoji == null + ? SvgPicture.asset( Assets.svg.plus, - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textWhite, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textWhite, width: 12, height: 12, ) - : SvgPicture.asset( + : SvgPicture.asset( Assets.svg.thickX, - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textWhite, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textWhite, width: 8, height: 8, ), - ), ), ), - ], - ), + ), + ], ), ), ), - const SizedBox(width: 8), - SizedBox( - width: isDesktop ? 450 : null, - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: - Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, - controller: nameController, - focusNode: nameFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter contact name", - nameFocusNode, + ), + const SizedBox(width: 8), + SizedBox( + width: isDesktop ? 450 : null, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: + Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel( context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: ref - .read( - contactNameIsNotEmptyStateProvider - .state, - ) - .state - ? Padding( + ), + suffixIcon: + ref + .read( + contactNameIsNotEmptyStateProvider + .state, + ) + .state + ? Padding( padding: const EdgeInsets.only( - right: 0, - ), + right: 0, + ), child: UnconstrainedBox( child: Row( children: [ @@ -417,143 +419,153 @@ class _AddAddressBookEntryViewState ), ), ) - : null, - ), - onChanged: (newValue) { - ref - .read( - contactNameIsNotEmptyStateProvider - .state, - ) - .state = newValue.isNotEmpty; - }, + : null, ), + onChanged: (newValue) { + ref + .read( + contactNameIsNotEmptyStateProvider + .state, + ) + .state = newValue.isNotEmpty; + }, ), ), - ], - ) + ), + ], + ) : Column( - children: [ - GestureDetector( - onTap: () { - if (_selectedEmoji != null) { + children: [ + GestureDetector( + onTap: () { + if (_selectedEmoji != null) { + setState(() { + _selectedEmoji = null; + }); + return; + } + + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => const EmojiSelectSheet(), + ).then((value) { + if (value is Emoji) { setState(() { - _selectedEmoji = null; + _selectedEmoji = value; }); - return; } - - showModalBottomSheet( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const EmojiSelectSheet(), - ).then((value) { - if (value is Emoji) { - setState(() { - _selectedEmoji = value; - }); - } - }); - }, - child: SizedBox( - height: 48, - width: 48, - child: Stack( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(24), - color: Theme.of(context) - .extension()! - .textFieldActiveBG, + }); + }, + child: SizedBox( + height: 48, + width: 48, + child: Stack( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 24, ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( + color: + Theme.of(context) + .extension()! + .textFieldActiveBG, + ), + child: Center( + child: + _selectedEmoji == null + ? SvgPicture.asset( Assets.svg.user, height: 24, width: 24, ) - : Text( + : Text( _selectedEmoji!.char, - style: STextStyles - .pageTitleH1(context), + style: + STextStyles.pageTitleH1( + context, + ), ), - ), ), - Align( - alignment: Alignment.bottomRight, - child: Container( - height: 14, - width: 14, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(14), - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( + ), + Align( + alignment: Alignment.bottomRight, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(14), + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), + child: Center( + child: + _selectedEmoji == null + ? SvgPicture.asset( Assets.svg.plus, - color: Theme.of(context) - .extension< - StackColors>()! - .textWhite, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textWhite, width: 12, height: 12, ) - : SvgPicture.asset( + : SvgPicture.asset( Assets.svg.thickX, - color: Theme.of(context) - .extension< - StackColors>()! - .textWhite, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textWhite, width: 8, height: 8, ), - ), ), ), - ], - ), + ), + ], ), ), - const SizedBox(height: 8), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: - Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, - controller: nameController, - focusNode: nameFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter contact name", - nameFocusNode, - context, - ).copyWith( - suffixIcon: ref - .read( - contactNameIsNotEmptyStateProvider - .state, - ) - .state - ? Padding( + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: nameController, + focusNode: nameFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter contact name", + nameFocusNode, + context, + ).copyWith( + suffixIcon: + ref + .read( + contactNameIsNotEmptyStateProvider + .state, + ) + .state + ? Padding( padding: const EdgeInsets.only( right: 0, ), @@ -573,34 +585,29 @@ class _AddAddressBookEntryViewState ), ), ) - : null, - ), - onChanged: (newValue) { - ref - .read( - contactNameIsNotEmptyStateProvider - .state, - ) - .state = newValue.isNotEmpty; - }, + : null, ), + onChanged: (newValue) { + ref + .read( + contactNameIsNotEmptyStateProvider + .state, + ) + .state = newValue.isNotEmpty; + }, ), - ], - ), + ), + ], + ), const SizedBox(height: 8), - if (forms.length <= 1) - const SizedBox( - height: 8, - ), + if (forms.length <= 1) const SizedBox(height: 8), if (forms.length <= 1) forms[0], if (forms.length > 1) for (int i = 0; i < forms.length; i++) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -617,15 +624,11 @@ class _AddAddressBookEntryViewState ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), forms[i], ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), CustomTextButton( onTap: () { _addForm(); @@ -644,9 +647,7 @@ class _AddAddressBookEntryViewState // style: STextStyles.largeMedium14(context), // ), // ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), const Spacer(), Row( children: [ @@ -668,18 +669,17 @@ class _AddAddressBookEntryViewState }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: Builder( builder: (context) { - final bool nameExists = ref - .watch( - contactNameIsNotEmptyStateProvider - .state, - ) - .state; + final bool nameExists = + ref + .watch( + contactNameIsNotEmptyStateProvider + .state, + ) + .state; final bool validForms = ref.watch( validContactStateProvider( @@ -697,55 +697,62 @@ class _AddAddressBookEntryViewState buttonHeight: isDesktop ? ButtonHeight.m : null, enabled: shouldEnableSave, - onPressed: shouldEnableSave - ? () async { - if (FocusScope.of(context) - .hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75, - ), - ); - } - final List - entries = []; - for (int i = 0; + onPressed: + shouldEnableSave + ? () async { + if (FocusScope.of( + context, + ).hasFocus) { + FocusScope.of( + context, + ).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75, + ), + ); + } + final List + entries = []; + for ( + int i = 0; i < forms.length; - i++) { - entries.add( - ref - .read( - addressEntryDataProvider( - forms[i].id, - ), - ) - .buildAddressEntry(), - ); - } - final ContactEntry contact = - ContactEntry( - emojiChar: _selectedEmoji?.char, - name: nameController.text, - addresses: entries, - isFavorite: _isFavorite, - customId: const Uuid().v1(), - ); + i++ + ) { + entries.add( + ref + .read( + addressEntryDataProvider( + forms[i].id, + ), + ) + .buildAddressEntry(), + ); + } + final ContactEntry contact = + ContactEntry( + emojiChar: + _selectedEmoji?.char, + name: nameController.text, + addresses: entries, + isFavorite: _isFavorite, + customId: const Uuid().v1(), + ); - if (await ref - .read( - addressBookServiceProvider, - ) - .addContact(contact)) { - if (mounted) { - Navigator.of(context).pop(); + if (await ref + .read( + addressBookServiceProvider, + ) + .addContact(contact)) { + if (mounted) { + Navigator.of(context).pop(); + } + // TODO show success notification + } else { + // TODO show error notification } - // TODO show success notification - } else { - // TODO show error notification } - } - : null, + : null, ); }, ), diff --git a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart index fc59eca68..374752024 100644 --- a/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/add_new_contact_address_view.dart @@ -18,7 +18,6 @@ import '../../../providers/ui/address_book_providers/address_entry_data_provider import '../../../providers/ui/address_book_providers/valid_contact_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -33,7 +32,6 @@ class AddNewContactAddressView extends ConsumerStatefulWidget { const AddNewContactAddressView({ super.key, required this.contactId, - this.barcodeScanner = const BarcodeScannerWrapper(), this.clipboard = const ClipboardWrapper(), }); @@ -41,7 +39,6 @@ class AddNewContactAddressView extends ConsumerStatefulWidget { final String contactId; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -53,13 +50,12 @@ class _AddNewContactAddressViewState extends ConsumerState { late final String contactId; - late final BarcodeScannerInterface barcodeScanner; late final ClipboardInterface clipboard; @override void initState() { contactId = widget.contactId; - barcodeScanner = widget.barcodeScanner; + clipboard = widget.clipboard; super.initState(); @@ -68,61 +64,67 @@ class _AddNewContactAddressViewState @override Widget build(BuildContext context) { final contact = ref.watch( - addressBookServiceProvider - .select((value) => value.getContactById(contactId)), + addressBookServiceProvider.select( + (value) => value.getContactById(contactId), + ), ); final isDesktop = Util.isDesktop; return ConditionalParent( condition: !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: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Add new address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + 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: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + title: Text( + "Add new address", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - ), - ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, ), - ); - }, + ), + ), ), - ), - ), child: Column( children: [ Row( @@ -132,31 +134,28 @@ class _AddNewContactAddressViewState width: 48, decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension()! - .textFieldActiveBG, + color: + Theme.of( + context, + ).extension()!.textFieldActiveBG, ), child: Center( - child: contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), + child: + contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), if (isDesktop) - Text( - contact.name, - style: STextStyles.pageTitleH2(context), - ), + Text(contact.name, style: STextStyles.pageTitleH2(context)), if (!isDesktop) Expanded( child: FittedBox( @@ -169,17 +168,9 @@ class _AddNewContactAddressViewState ), ], ), - const SizedBox( - height: 16, - ), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), + NewContactAddressEntryForm(id: 0, clipboard: clipboard), + const SizedBox(height: 16), const Spacer(), Row( children: [ @@ -200,9 +191,7 @@ class _AddNewContactAddressViewState }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Save", @@ -222,8 +211,9 @@ class _AddNewContactAddressViewState ref.read(addressEntryDataProvider(0)).buildAddressEntry(), ); - final ContactEntry editedContact = - contact.copyWith(addresses: entries); + final ContactEntry editedContact = contact.copyWith( + addresses: entries, + ); if (await ref .read(addressBookServiceProvider) diff --git a/lib/pages/address_book_views/subviews/address_book_filter_view.dart b/lib/pages/address_book_views/subviews/address_book_filter_view.dart index 9feaa0a37..47d14c8bb 100644 --- a/lib/pages/address_book_views/subviews/address_book_filter_view.dart +++ b/lib/pages/address_book_views/subviews/address_book_filter_view.dart @@ -42,9 +42,7 @@ class _AddressBookFilterViewState extends ConsumerState { @override void initState() { final coins = [...AppConfig.coins]; - coins.removeWhere( - (e) => e is Firo && e.network.isTestNet, - ); + coins.removeWhere((e) => e is Firo && e.network.isTestNet); final showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; @@ -81,45 +79,43 @@ class _AddressBookFilterViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Text( - "Only selected cryptocurrency addresses will be displayed.", - style: STextStyles.itemSubtitle(context), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "Only selected cryptocurrency addresses will be displayed.", + style: STextStyles.itemSubtitle(context), + ), ), - ), - const SizedBox( - height: 12, - ), - Text( - "Select cryptocurrency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - child, - ], + const SizedBox(height: 12), + Text( + "Select cryptocurrency", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 12), + child, + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), @@ -157,8 +153,9 @@ class _AddressBookFilterViewState extends ConsumerState { child: Column( children: [ Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32), + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), child: child, ), ], @@ -170,8 +167,10 @@ class _AddressBookFilterViewState extends ConsumerState { ), ), Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32, vertical: 32), + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 32, + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -234,8 +233,9 @@ class _AddressBookFilterViewState extends ConsumerState { child: Checkbox( value: ref .watch( - addressBookFilterProvider - .select((value) => value.coins), + addressBookFilterProvider.select( + (value) => value.coins, + ), ) .contains(coin), onChanged: (value) { @@ -254,9 +254,7 @@ class _AddressBookFilterViewState extends ConsumerState { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -264,9 +262,7 @@ class _AddressBookFilterViewState extends ConsumerState { coin.prettyName, style: STextStyles.largeMedium14(context), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( coin.ticker, style: STextStyles.itemSubtitle(context), diff --git a/lib/pages/address_book_views/subviews/contact_details_view.dart b/lib/pages/address_book_views/subviews/contact_details_view.dart index b343c3092..0ee0e6b93 100644 --- a/lib/pages/address_book_views/subviews/contact_details_view.dart +++ b/lib/pages/address_book_views/subviews/contact_details_view.dart @@ -62,24 +62,26 @@ class _ContactDetailsViewState extends ConsumerState { List> _cachedTransactions = []; Future>> - _filteredTransactionsByContact() async { - final contact = - ref.read(addressBookServiceProvider).getContactById(_contactId); + _filteredTransactionsByContact() async { + final contact = ref + .read(addressBookServiceProvider) + .getContactById(_contactId); // TODO: optimise - final transactions = await ref - .read(mainDBProvider) - .isar - .transactions - .where() - .filter() - .anyOf( - contact.addresses.map((e) => e.address), - (q, String e) => q.address((q) => q.valueEqualTo(e)), - ) - .sortByTimestampDesc() - .findAll(); + final transactions = + await ref + .read(mainDBProvider) + .isar + .transactions + .where() + .filter() + .anyOf( + contact.addresses.map((e) => e.address), + (q, String e) => q.address((q) => q.valueEqualTo(e)), + ) + .sortByTimestampDesc() + .findAll(); final List> result = []; @@ -111,8 +113,9 @@ class _ContactDetailsViewState extends ConsumerState { debugPrint("BUILD: $runtimeType"); final _contact = ref.watch( - addressBookServiceProvider - .select((value) => value.getContactById(_contactId)), + addressBookServiceProvider.select( + (value) => value.getContactById(_contactId), + ), ); return Background( @@ -130,11 +133,7 @@ class _ContactDetailsViewState extends ConsumerState { ), actions: [ Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), + padding: const EdgeInsets.only(top: 10, bottom: 10, right: 10), child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( @@ -144,20 +143,23 @@ class _ContactDetailsViewState extends ConsumerState { color: Theme.of(context).extension()!.background, icon: SvgPicture.asset( Assets.svg.star, - color: _contact.isFavorite - ? Theme.of(context) - .extension()! - .favoriteStarActive - : Theme.of(context) - .extension()! - .favoriteStarInactive, + color: + _contact.isFavorite + ? Theme.of( + context, + ).extension()!.favoriteStarActive + : Theme.of( + context, + ).extension()!.favoriteStarInactive, width: 20, height: 20, ), onPressed: () { final bool isFavorite = _contact.isFavorite; - ref.read(addressBookServiceProvider).editContact( + ref + .read(addressBookServiceProvider) + .editContact( _contact.copyWith(isFavorite: !isFavorite), ); }, @@ -165,11 +167,7 @@ class _ContactDetailsViewState extends ConsumerState { ), ), Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), + padding: const EdgeInsets.only(top: 10, bottom: 10, right: 10), child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( @@ -179,9 +177,10 @@ class _ContactDetailsViewState extends ConsumerState { color: Theme.of(context).extension()!.background, icon: SvgPicture.asset( Assets.svg.trash, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -190,43 +189,44 @@ class _ContactDetailsViewState extends ConsumerState { context: context, useSafeArea: true, barrierDismissible: true, - builder: (_) => StackDialog( - title: "Delete ${_contact.name}?", - message: "Contact will be deleted permanently!", - leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Cancel", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Delete", - style: STextStyles.button(context), + builder: + (_) => StackDialog( + title: "Delete ${_contact.name}?", + message: "Contact will be deleted permanently!", + leftButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Delete", + style: STextStyles.button(context), + ), + onPressed: () { + ref + .read(addressBookServiceProvider) + .removeContact(_contact.customId); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.success, + message: "${_contact.name} deleted", + context: context, + ); + }, + ), ), - onPressed: () { - ref - .read(addressBookServiceProvider) - .removeContact(_contact.customId); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.success, - message: "${_contact.name} deleted", - context: context, - ); - }, - ), - ), ); }, ), @@ -234,308 +234,292 @@ class _ContactDetailsViewState extends ConsumerState { ), ], ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 12, - ), - Row( - children: [ - Container( - height: 48, - width: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension()! - .textFieldActiveBG, - ), - child: Center( - child: _contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - _contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Row( + children: [ + Container( + height: 48, + width: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: + Theme.of( + context, + ).extension()!.textFieldActiveBG, + ), + child: Center( + child: + _contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + _contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), + ), ), - ), - const SizedBox( - width: 16, - ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - _contact.name, - textAlign: TextAlign.left, - style: STextStyles.pageTitleH2(context), + const SizedBox(width: 16), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + _contact.name, + textAlign: TextAlign.left, + style: STextStyles.pageTitleH2(context), + ), ), - ), - const Spacer(), - TextButton( - onPressed: () { - Navigator.of(context).pushNamed( - EditContactNameEmojiView.routeName, - arguments: _contact.customId, - ); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context)! - .copyWith( - minimumSize: MaterialStateProperty.all( - const Size(46, 32), + const Spacer(), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed( + EditContactNameEmojiView.routeName, + arguments: _contact.customId, + ); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context)! + .copyWith( + minimumSize: MaterialStateProperty.all( + const Size(46, 32), + ), ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), + const SizedBox(width: 4), + Text( + "Edit", + style: STextStyles.buttonSmall(context), + ), + ], ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.buttonSmall(context), - ), - ], ), ), - ), - ], - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Addresses", - style: STextStyles.itemSubtitle(context), - ), - CustomTextButton( - text: "Add new", - onTap: () { - Navigator.of(context).pushNamed( - AddNewContactAddressView.routeName, - arguments: _contact.customId, - ); - }, - ), - ], - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( + ], + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ..._contact.addressesSorted.map( - (e) => Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.file( - File( - ref.watch(coinIconProvider(e.coin)), + Text( + "Addresses", + style: STextStyles.itemSubtitle(context), + ), + CustomTextButton( + text: "Add new", + onTap: () { + Navigator.of(context).pushNamed( + AddNewContactAddressView.routeName, + arguments: _contact.customId, + ); + }, + ), + ], + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._contact.addressesSorted.map( + (e) => Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.file( + File(ref.watch(coinIconProvider(e.coin))), + height: 24, ), - height: 24, - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "${e.label} (${e.coin.ticker})", - style: - STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 2, - ), - FittedBox( - fit: BoxFit.scaleDown, - child: Text( - e.address, - style: - STextStyles.itemSubtitle(context) - .copyWith( - fontSize: 8, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "${e.label} (${e.coin.ticker})", + style: STextStyles.itemSubtitle12( + context, ), ), - ), - ], + const SizedBox(height: 2), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + e.address, + style: STextStyles.itemSubtitle( + context, + ).copyWith(fontSize: 8), + ), + ), + ], + ), ), - ), - GestureDetector( - onTap: () { - ref - .read(addressEntryDataProvider(0)) - .address = e.address; - ref - .read(addressEntryDataProvider(0)) - .addressLabel = e.label; - ref.read(addressEntryDataProvider(0)).coin = - e.coin; + GestureDetector( + onTap: () { + ref + .read(addressEntryDataProvider(0)) + .address = e.address; + ref + .read(addressEntryDataProvider(0)) + .addressLabel = e.label; + ref + .read(addressEntryDataProvider(0)) + .coin = e.coin; - Navigator.of(context).pushNamed( - EditContactAddressView.routeName, - arguments: Tuple2(_contact.customId, e), - ); - }, - child: RoundedContainer( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.pencil, - width: 14, - height: 14, - color: Theme.of(context) - .extension()! - .accentColorDark, + Navigator.of(context).pushNamed( + EditContactAddressView.routeName, + arguments: Tuple2(_contact.customId, e), + ); + }, + child: RoundedContainer( + color: + Theme.of(context) + .extension()! + .textFieldDefaultBG, + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.pencil, + width: 14, + height: 14, + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), - ), - const SizedBox( - width: 4, - ), - GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: e.address), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: RoundedContainer( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.copy, - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, + const SizedBox(width: 4), + GestureDetector( + onTap: () { + clipboard.setData( + ClipboardData(text: e.address), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: RoundedContainer( + color: + Theme.of(context) + .extension()! + .textFieldDefaultBG, + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + Assets.svg.copy, + width: 16, + height: 16, + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), - ), - const SizedBox( - height: 24, - ), - Text( - "Transaction history", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - FutureBuilder( - future: _filteredTransactionsByContact(), - builder: ( - _, - AsyncSnapshot>> snapshot, - ) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - _cachedTransactions = snapshot.data!; + const SizedBox(height: 24), + Text( + "Transaction history", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox(height: 12), + FutureBuilder( + future: _filteredTransactionsByContact(), + builder: ( + _, + AsyncSnapshot>> + snapshot, + ) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _cachedTransactions = snapshot.data!; - if (_cachedTransactions.isNotEmpty) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cachedTransactions.map( - (e) => TransactionCard( - key: Key( - "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey", + if (_cachedTransactions.isNotEmpty) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey", + ), + transaction: e.item2, + walletId: e.item1, ), - transaction: e.item2, - walletId: e.item1, ), + ], + ), + ); + } else { + return RoundedWhiteContainer( + child: Center( + child: Text( + "No transactions found", + style: STextStyles.itemSubtitle(context), ), - ], - ), - ); - } else { - return RoundedWhiteContainer( - child: Center( - child: Text( - "No transactions found", - style: STextStyles.itemSubtitle(context), ), - ), - ); - } - } else { - // TODO: proper loading animation - if (_cachedTransactions.isEmpty) { - return const LoadingIndicator(); + ); + } } else { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ..._cachedTransactions.map( - (e) => TransactionCard( - key: Key( - "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey", + // TODO: proper loading animation + if (_cachedTransactions.isEmpty) { + return const LoadingIndicator(); + } else { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ..._cachedTransactions.map( + (e) => TransactionCard( + key: Key( + "contactDetailsTransaction_${e.item1}_${e.item2.txid}_cardKey", + ), + transaction: e.item2, + walletId: e.item1, ), - transaction: e.item2, - walletId: e.item1, ), - ), - ], - ), - ); + ], + ), + ); + } } - } - }, - ), - const SizedBox( - height: 16, - ), - ], + }, + ), + const SizedBox(height: 16), + ], + ), ), ), ), diff --git a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart index 1d5ebd21c..652ff94e3 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_address_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_address_view.dart @@ -18,7 +18,6 @@ import '../../../providers/ui/address_book_providers/address_entry_data_provider import '../../../providers/ui/address_book_providers/valid_contact_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -34,7 +33,7 @@ class EditContactAddressView extends ConsumerStatefulWidget { super.key, required this.contactId, required this.addressEntry, - this.barcodeScanner = const BarcodeScannerWrapper(), + this.clipboard = const ClipboardWrapper(), }); @@ -43,7 +42,6 @@ class EditContactAddressView extends ConsumerStatefulWidget { final String contactId; final ContactAddressEntry addressEntry; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -56,15 +54,12 @@ class _EditContactAddressViewState late final String contactId; late final ContactAddressEntry addressEntry; - late final BarcodeScannerInterface barcodeScanner; late final ClipboardInterface clipboard; Future save(ContactEntry contact) async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); + await Future.delayed(const Duration(milliseconds: 75)); } final List entries = contact.addresses.toList(); @@ -99,7 +94,6 @@ class _EditContactAddressViewState void initState() { contactId = widget.contactId; addressEntry = widget.addressEntry; - barcodeScanner = widget.barcodeScanner; clipboard = widget.clipboard; super.initState(); @@ -108,61 +102,67 @@ class _EditContactAddressViewState @override Widget build(BuildContext context) { final contact = ref.watch( - addressBookServiceProvider - .select((value) => value.getContactById(contactId)), + addressBookServiceProvider.select( + (value) => value.getContactById(contactId), + ), ); final bool isDesktop = Util.isDesktop; return ConditionalParent( condition: !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: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Edit address", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + 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: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + title: Text( + "Edit address", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - ), - ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, ), - ); - }, + ), + ), ), - ), - ), child: Column( children: [ Row( @@ -172,31 +172,28 @@ class _EditContactAddressViewState width: 48, decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), - color: Theme.of(context) - .extension()! - .textFieldActiveBG, + color: + Theme.of( + context, + ).extension()!.textFieldActiveBG, ), child: Center( - child: contact.emojiChar == null - ? SvgPicture.asset( - Assets.svg.user, - height: 24, - width: 24, - ) - : Text( - contact.emojiChar!, - style: STextStyles.pageTitleH1(context), - ), + child: + contact.emojiChar == null + ? SvgPicture.asset( + Assets.svg.user, + height: 24, + width: 24, + ) + : Text( + contact.emojiChar!, + style: STextStyles.pageTitleH1(context), + ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), if (isDesktop) - Text( - contact.name, - style: STextStyles.pageTitleH2(context), - ), + Text(contact.name, style: STextStyles.pageTitleH2(context)), if (!isDesktop) Expanded( child: FittedBox( @@ -209,23 +206,14 @@ class _EditContactAddressViewState ), ], ), - const SizedBox( - height: 16, - ), - NewContactAddressEntryForm( - id: 0, - barcodeScanner: barcodeScanner, - clipboard: clipboard, - ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 16), + NewContactAddressEntryForm(id: 0, clipboard: clipboard), + const SizedBox(height: 24), ConditionalParent( condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), child: GestureDetector( onTap: () async { // delete address @@ -240,11 +228,13 @@ class _EditContactAddressViewState //Deleting an entry directly from _addresses gives error // "Cannot remove from a fixed-length list", so we remove the // entry from a copy - final tempAddresses = - List.from(_addresses); + final tempAddresses = List.from( + _addresses, + ); tempAddresses.remove(entry); - final ContactEntry editedContact = - contact.copyWith(addresses: tempAddresses); + final ContactEntry editedContact = contact.copyWith( + addresses: tempAddresses, + ); if (await ref .read(addressBookServiceProvider) .editContact(editedContact)) { @@ -254,16 +244,11 @@ class _EditContactAddressViewState // TODO show error notification } }, - child: Text( - "Delete address", - style: STextStyles.link(context), - ), + child: Text("Delete address", style: STextStyles.link(context)), ), ), const Spacer(), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( @@ -283,9 +268,7 @@ class _EditContactAddressViewState }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Save", diff --git a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart index a552aca29..412da64d6 100644 --- a/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart +++ b/lib/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart @@ -33,10 +33,7 @@ import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; class EditContactNameEmojiView extends ConsumerStatefulWidget { - const EditContactNameEmojiView({ - super.key, - required this.contactId, - }); + const EditContactNameEmojiView({super.key, required this.contactId}); static const String routeName = "/editContactNameEmoji"; @@ -63,8 +60,9 @@ class _EditContactNameEmojiViewState nameFocusNode = FocusNode(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final contact = - ref.read(addressBookServiceProvider).getContactById(contactId); + final contact = ref + .read(addressBookServiceProvider) + .getContactById(contactId); nameController.text = contact.name; setState(() { @@ -84,8 +82,9 @@ class _EditContactNameEmojiViewState @override Widget build(BuildContext context) { final contact = ref.watch( - addressBookServiceProvider - .select((value) => value.getContactById(contactId)), + addressBookServiceProvider.select( + (value) => value.getContactById(contactId), + ), ); final isDesktop = Util.isDesktop; @@ -93,53 +92,58 @@ class _EditContactNameEmojiViewState return ConditionalParent( condition: !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: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Edit contact", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + 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: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + title: Text( + "Edit contact", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - ), - ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, ), - ); - }, + ), + ), ), - ), - ), child: Column( children: [ Row( @@ -208,23 +212,26 @@ class _EditContactNameEmojiViewState width: emojiSize, decoration: BoxDecoration( borderRadius: BorderRadius.circular(emojiSize / 2), - color: Theme.of(context) - .extension()! - .textFieldActiveBG, + color: + Theme.of( + context, + ).extension()!.textFieldActiveBG, ), child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.user, - height: emojiSize / 2, - width: emojiSize / 2, - ) - : Text( - _selectedEmoji!.char, - style: isDesktop - ? STextStyles.desktopH3(context) - : STextStyles.pageTitleH1(context), - ), + child: + _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.user, + height: emojiSize / 2, + width: emojiSize / 2, + ) + : Text( + _selectedEmoji!.char, + style: + isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH1(context), + ), ), ), Align( @@ -234,28 +241,32 @@ class _EditContactNameEmojiViewState width: 14, decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), child: Center( - child: _selectedEmoji == null - ? SvgPicture.asset( - Assets.svg.plus, - color: Theme.of(context) - .extension()! - .textWhite, - width: 12, - height: 12, - ) - : SvgPicture.asset( - Assets.svg.thickX, - color: Theme.of(context) - .extension()! - .textWhite, - width: 8, - height: 8, - ), + child: + _selectedEmoji == null + ? SvgPicture.asset( + Assets.svg.plus, + color: + Theme.of( + context, + ).extension()!.textWhite, + width: 12, + height: 12, + ) + : SvgPicture.asset( + Assets.svg.thickX, + color: + Theme.of( + context, + ).extension()!.textWhite, + width: 8, + height: 8, + ), ), ), ), @@ -263,10 +274,7 @@ class _EditContactNameEmojiViewState ), ), ), - if (isDesktop) - const SizedBox( - width: 8, - ), + if (isDesktop) const SizedBox(width: 8), if (isDesktop) Expanded( child: ClipRRect( @@ -285,35 +293,33 @@ class _EditContactNameEmojiViewState nameFocusNode, context, ).copyWith( - suffixIcon: nameController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - nameController.text = ""; - }); - }, - ), - ], + suffixIcon: + nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), ], ), - if (!isDesktop) - const SizedBox( - height: 8, - ), + if (!isDesktop) const SizedBox(height: 8), if (!isDesktop) ClipRRect( borderRadius: BorderRadius.circular( @@ -331,32 +337,31 @@ class _EditContactNameEmojiViewState nameFocusNode, context, ).copyWith( - suffixIcon: nameController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - nameController.text = ""; - }); - }, - ), - ], + suffixIcon: + nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + nameController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), const Spacer(), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( @@ -376,9 +381,7 @@ class _EditContactNameEmojiViewState }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Save", @@ -398,9 +401,9 @@ class _EditContactNameEmojiViewState _selectedEmoji == null ? null : _selectedEmoji!.char, ); unawaited( - ref.read(addressBookServiceProvider).editContact( - editedContact, - ), + ref + .read(addressBookServiceProvider) + .editContact(editedContact), ); if (mounted) { Navigator.of(context).pop(); diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index f1f399f13..b6dcd7bde 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -42,13 +42,11 @@ class NewContactAddressEntryForm extends ConsumerStatefulWidget { const NewContactAddressEntryForm({ super.key, required this.id, - required this.barcodeScanner, required this.clipboard, }); final int id; - final BarcodeScannerInterface barcodeScanner; final ClipboardInterface clipboard; @override @@ -72,7 +70,7 @@ class _NewContactAddressEntryFormState // .read(shouldShowLockscreenOnResumeStateProvider // .state) // .state = false; - final qrResult = await widget.barcodeScanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), @@ -102,9 +100,10 @@ class _NewContactAddressEntryFormState // now check for non standard encoded basic address } else if (ref.read(addressEntryDataProvider(widget.id)).coin != null) { - if (ref.read(addressEntryDataProvider(widget.id)).coin!.validateAddress( - qrResult.rawContent, - )) { + if (ref + .read(addressEntryDataProvider(widget.id)) + .coin! + .validateAddress(qrResult.rawContent)) { addressController.text = qrResult.rawContent; ref.read(addressEntryDataProvider(widget.id)).address = qrResult.rawContent; @@ -115,16 +114,39 @@ class _NewContactAddressEntryFormState // .read(shouldShowLockscreenOnResumeStateProvider // .state) // .state = true; - Logging.instance.w("Failed to get camera permissions to scan address qr code: ", error: e, stackTrace: s); + + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions to scan address qr code: ", + error: e, + stackTrace: s, + ); + } } } @override void initState() { - addressLabelController = TextEditingController() - ..text = ref.read(addressEntryDataProvider(widget.id)).addressLabel ?? ""; - addressController = TextEditingController() - ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; + addressLabelController = + TextEditingController() + ..text = + ref.read(addressEntryDataProvider(widget.id)).addressLabel ?? ""; + addressController = + TextEditingController() + ..text = ref.read(addressEntryDataProvider(widget.id)).address ?? ""; addressLabelFocusNode = FocusNode(); addressFocusNode = FocusNode(); coins = [...AppConfig.coins]; @@ -153,18 +175,17 @@ class _NewContactAddressEntryFormState final isDesktop = Util.isDesktop; if (isDesktop) { coins = [...AppConfig.coins]; - coins.removeWhere( - (e) => e is Firo && e.network.isTestNet, - ); + coins.removeWhere((e) => e is Firo && e.network.isTestNet); final showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; if (showTestNet) { coins = coins.toList(); } else { - coins = coins - .where((e) => e.network != CryptoCurrencyNetwork.test) - .toList(); + coins = + coins + .where((e) => e.network != CryptoCurrencyNetwork.test) + .toList(); } } @@ -181,24 +202,23 @@ class _NewContactAddressEntryFormState offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 4, - ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), ), isExpanded: true, value: ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin), + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), ), onChanged: (value) { if (value is CryptoCurrency) { @@ -222,23 +242,20 @@ class _NewContactAddressEntryFormState child: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), height: 24, width: 24, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( coin.prettyName, - style: - STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -285,54 +302,54 @@ class _NewContactAddressEntryFormState mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin), + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), ) == null ? Text( - "Select cryptocurrency", - style: STextStyles.fieldLabel(context), - ) + "Select cryptocurrency", + style: STextStyles.fieldLabel(context), + ) : Row( - children: [ - SvgPicture.file( - File( - ref.watch( - coinIconProvider( - ref.watch( - addressEntryDataProvider(widget.id) - .select( - (value) => value.coin, - ), - )!, - ), + children: [ + SvgPicture.file( + File( + ref.watch( + coinIconProvider( + ref.watch( + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), + )!, ), ), - height: 20, - width: 20, - ), - const SizedBox( - width: 12, ), - Text( - ref - .watch( - addressEntryDataProvider(widget.id) - .select((value) => value.coin), - )! - .prettyName, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), + height: 20, + width: 20, + ), + const SizedBox(width: 12), + Text( + ref + .watch( + addressEntryDataProvider( + widget.id, + ).select((value) => value.coin), + )! + .prettyName, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), if (!isDesktop) SvgPicture.asset( Assets.svg.chevronDown, width: 8, height: 4, - color: Theme.of(context) - .extension()! - .textSubtitle2, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, ), ], ), @@ -341,10 +358,7 @@ class _NewContactAddressEntryFormState ), ), ), - if (!AppConfig.isSingleCoinApp) - const SizedBox( - height: 8, - ), + if (!AppConfig.isSingleCoinApp) const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -361,25 +375,26 @@ class _NewContactAddressEntryFormState context, ).copyWith( labelStyle: isDesktop ? STextStyles.fieldLabel(context) : null, - suffixIcon: addressLabelController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - addressLabelController.text = ""; - }); - }, - ), - ], + suffixIcon: + addressLabelController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + addressLabelController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), onChanged: (newValue) { ref.read(addressEntryDataProvider(widget.id)).addressLabel = @@ -388,9 +403,7 @@ class _NewContactAddressEntryFormState }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -410,8 +423,9 @@ class _NewContactAddressEntryFormState child: Row( children: [ if (ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.address), + addressEntryDataProvider( + widget.id, + ).select((value) => value.address), ) != null) TextFieldIconButton( @@ -425,8 +439,9 @@ class _NewContactAddressEntryFormState child: const XIcon(), ), if (ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.address), + addressEntryDataProvider( + widget.id, + ).select((value) => value.address), ) == null) TextFieldIconButton( @@ -438,8 +453,10 @@ class _NewContactAddressEntryFormState if (data?.text != null && data!.text!.isNotEmpty) { String content = data.text!.trim(); if (content.contains("\n")) { - content = - content.substring(0, content.indexOf("\n")); + content = content.substring( + 0, + content.indexOf("\n"), + ); } addressController.text = content; ref @@ -451,8 +468,9 @@ class _NewContactAddressEntryFormState ), if (!Util.isDesktop && ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.address), + addressEntryDataProvider( + widget.id, + ).select((value) => value.address), ) == null) TextFieldIconButton( @@ -460,9 +478,7 @@ class _NewContactAddressEntryFormState onTap: _onQrTapped, child: const QrCodeIcon(), ), - const SizedBox( - width: 8, - ), + const SizedBox(width: 8), ], ), ), @@ -485,21 +501,18 @@ class _NewContactAddressEntryFormState ), ), if (!ref.watch( - addressEntryDataProvider(widget.id) - .select((value) => value.isValidAddress), + addressEntryDataProvider( + widget.id, + ).select((value) => value.isValidAddress), ) && addressController.text.isNotEmpty) Row( children: [ - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( "Invalid address", textAlign: TextAlign.left, diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index d68a02e4f..64a7ba40f 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -16,12 +16,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; +import 'package:isar/isar.dart'; import '../../app_config.dart'; import '../../models/buy/response_objects/crypto.dart'; import '../../models/buy/response_objects/fiat.dart'; import '../../models/buy/response_objects/quote.dart'; import '../../models/contact_address_entry.dart'; +import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import '../../providers/providers.dart'; @@ -33,10 +35,12 @@ import '../../utilities/assets.dart'; import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; +import '../../utilities/enums/derive_path_type_enum.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; +import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/custom_loading_overlay.dart'; @@ -64,13 +68,11 @@ class BuyForm extends ConsumerStatefulWidget { this.coin, this.tokenContract, this.clipboard = const ClipboardWrapper(), - this.scanner = const BarcodeScannerWrapper(), }); final CryptoCurrency? coin; final ClipboardInterface clipboard; - final BarcodeScannerInterface scanner; final EthContract? tokenContract; @override @@ -81,7 +83,6 @@ class _BuyFormState extends ConsumerState { late final CryptoCurrency? coin; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late final TextEditingController _receiveAddressController; late final TextEditingController _buyAmountController; @@ -162,13 +163,14 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading currency data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: + (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading currency data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); await _loadSimplexCryptos(); @@ -202,66 +204,62 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( + final result = + isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a crypto to buy", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Choose a crypto to buy", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: Theme.of(context) - .extension()! - .background, - child: CryptoSelectionView( - coins: coins, + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: CryptoSelectionView(coins: coins), ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => CryptoSelectionView( - coins: coins, + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CryptoSelectionView(coins: coins), ), - ), - ); + ); if (mounted && result is Crypto) { onSelected(result); @@ -274,13 +272,14 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading currency data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: + (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading currency data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); await _loadSimplexFiats(); @@ -334,66 +333,62 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( + final result = + isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a fiat with which to pay", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Choose a fiat with which to pay", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: Theme.of(context) - .extension()! - .background, - child: FiatSelectionView( - fiats: fiats, + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: FiatSelectionView(fiats: fiats), ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => FiatSelectionView( - fiats: fiats, + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => FiatSelectionView(fiats: fiats), ), - ), - ); + ); if (mounted && result is Fiat) { onSelected(result); @@ -411,25 +406,28 @@ class _BuyFormState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Loading quote data", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: + (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading quote data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); quote = SimplexQuote( crypto: selectedCrypto!, fiat: selectedFiat!, - youPayFiatPrice: buyWithFiat - ? Decimal.parse(_buyAmountController.text) - : Decimal.parse("100"), // dummy value - youReceiveCryptoAmount: buyWithFiat - ? Decimal.parse("0.000420282") // dummy value - : Decimal.parse(_buyAmountController.text), // Ternary for this + youPayFiatPrice: + buyWithFiat + ? Decimal.parse(_buyAmountController.text) + : Decimal.parse("100"), // dummy value + youReceiveCryptoAmount: + buyWithFiat + ? Decimal.parse("0.000420282") // dummy value + : Decimal.parse(_buyAmountController.text), // Ternary for this id: "id", // anything; we get an ID back receivingAddress: _receiveAddressController.text, buyWithFiat: buyWithFiat, @@ -469,16 +467,12 @@ class _BuyFormState extends ConsumerState { "Simplex API unresponsive", style: STextStyles.desktopH3(context), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Text( "Simplex API unresponsive, please try again later", style: STextStyles.smallMed14(context), ), - const SizedBox( - height: 56, - ), + const SizedBox(height: 56), Row( children: [ const Spacer(), @@ -506,9 +500,10 @@ class _BuyFormState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -558,16 +553,12 @@ class _BuyFormState extends ConsumerState { "Simplex API error", style: STextStyles.desktopH3(context), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Text( quoteResponse.exception!.errorMessage, style: STextStyles.smallMed14(context), ), - const SizedBox( - height: 56, - ), + const SizedBox(height: 56), Row( children: [ const Spacer(), @@ -596,9 +587,10 @@ class _BuyFormState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -622,11 +614,9 @@ class _BuyFormState extends ConsumerState { } else { Logging.instance.d("_loadQuote: $response"); return BuyResponse( - exception: response.exception ?? - BuyException( - response.toString(), - BuyExceptionType.generic, - ), + exception: + response.exception ?? + BuyException(response.toString(), BuyExceptionType.generic), ); } } @@ -638,66 +628,62 @@ class _BuyFormState extends ConsumerState { _fiatFocusNode.unfocus(); _cryptoFocusNode.unfocus(); - final result = isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( + final result = + isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Preview quote", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Preview quote", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: Theme.of(context) - .extension()! - .background, - child: BuyQuotePreviewView( - quote: quote, + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: BuyQuotePreviewView(quote: quote), ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => BuyQuotePreviewView( - quote: quote, + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BuyQuotePreviewView(quote: quote), ), - ), - ); + ); if (mounted && result is SimplexQuote) { onSelected(result); @@ -708,12 +694,10 @@ class _BuyFormState extends ConsumerState { try { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); + await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); @@ -743,13 +727,26 @@ class _BuyFormState extends ConsumerState { }); } } on PlatformException catch (e, s) { - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.e( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in $runtimeType: ", + error: e, + stackTrace: s, + ); + } } } @@ -759,7 +756,6 @@ class _BuyFormState extends ConsumerState { _buyAmountController = TextEditingController(); clipboard = widget.clipboard; - scanner = widget.scanner; coins = ref.read(simplexProvider).supportedCryptos; fiats = ref.read(simplexProvider).supportedFiats; @@ -776,8 +772,10 @@ class _BuyFormState extends ConsumerState { ); // TODO enum this or something // TODO set defaults better; should probably explicitly enumerate the coins & fiats used and pull the specific ones we need rather than generating them as defaults here - selectedFiat = - Fiat.fromJson({'ticker': 'USD', 'name': 'United States Dollar'}); + selectedFiat = Fiat.fromJson({ + 'ticker': 'USD', + 'name': 'United States Dollar', + }); selectedCrypto = Crypto.fromJson({ 'ticker': widget.coin?.ticker ?? 'BTC', 'name': widget.coin?.prettyName ?? 'Bitcoin', @@ -815,24 +813,21 @@ class _BuyFormState extends ConsumerState { return ConditionalParent( condition: isDesktop, - builder: (child) => SizedBox( - width: 458, - child: child, - ), + builder: (child) => SizedBox(width: 458, child: child), child: ConditionalParent( condition: !isDesktop, - builder: (child) => LayoutBuilder( - builder: (context, constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, - ), + builder: + (child) => LayoutBuilder( + builder: + (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), + ), ), - ), - ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -843,9 +838,7 @@ class _BuyFormState extends ConsumerState { color: Theme.of(context).extension()!.textDark3, ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovering1 = true), @@ -855,16 +848,19 @@ class _BuyFormState extends ConsumerState { selectCrypto(); }, child: RoundedContainer( - padding: - const EdgeInsets.symmetric(vertical: 6, horizontal: 2), - color: _hovering1 - ? Theme.of(context) - .extension()! - .currencyListItemBG - .withOpacity(_hovering1 ? 0.3 : 0) - : Theme.of(context) - .extension()! - .textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 2, + ), + color: + _hovering1 + ? Theme.of(context) + .extension()! + .currencyListItemBG + .withOpacity(_hovering1 ? 0.3 : 0) + : Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Padding( padding: const EdgeInsets.all(12), child: Row( @@ -873,9 +869,7 @@ class _BuyFormState extends ConsumerState { ticker: selectedCrypto?.ticker ?? "BTC", size: 20, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Expanded( child: Text( selectedCrypto?.ticker ?? "ERR", @@ -884,9 +878,10 @@ class _BuyFormState extends ConsumerState { ), SvgPicture.asset( Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .buttonTextSecondaryDisabled, + color: + Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, width: 10, height: 5, ), @@ -896,9 +891,7 @@ class _BuyFormState extends ConsumerState { ), ), ), - SizedBox( - height: isDesktop ? 20 : 12, - ), + SizedBox(height: isDesktop ? 20 : 12), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -912,9 +905,7 @@ class _BuyFormState extends ConsumerState { ), ], ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) => setState(() => _hovering2 = true), @@ -924,16 +915,19 @@ class _BuyFormState extends ConsumerState { selectFiat(); }, child: RoundedContainer( - padding: - const EdgeInsets.symmetric(vertical: 3, horizontal: 2), - color: _hovering2 - ? Theme.of(context) - .extension()! - .currencyListItemBG - .withOpacity(_hovering2 ? 0.3 : 0) - : Theme.of(context) - .extension()! - .textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + vertical: 3, + horizontal: 2, + ), + color: + _hovering2 + ? Theme.of(context) + .extension()! + .currencyListItemBG + .withOpacity(_hovering2 ? 0.3 : 0) + : Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Padding( padding: const EdgeInsets.only( left: 12.0, @@ -949,9 +943,10 @@ class _BuyFormState extends ConsumerState { horizontal: 6, ), decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .currencyListItemBG, + color: + Theme.of( + context, + ).extension()!.currencyListItemBG, borderRadius: BorderRadius.circular(4), ), child: Text( @@ -960,22 +955,19 @@ class _BuyFormState extends ConsumerState { ), textAlign: TextAlign.center, style: STextStyles.smallMed12(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), - const SizedBox( - width: 8, - ), + const SizedBox(width: 8), Text( selectedFiat?.ticker ?? 'ERR', style: STextStyles.largeMedium14(context), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: Text( selectedFiat?.name ?? 'Error', @@ -984,9 +976,10 @@ class _BuyFormState extends ConsumerState { ), SvgPicture.asset( Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .buttonTextSecondaryDisabled, + color: + Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, width: 10, height: 5, ), @@ -996,9 +989,7 @@ class _BuyFormState extends ConsumerState { ), ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1021,9 +1012,7 @@ class _BuyFormState extends ConsumerState { ), ], ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -1037,12 +1026,13 @@ class _BuyFormState extends ConsumerState { // ? _BuyFormState.minFiat.toStringAsFixed(2) ?? '50.00' // : _BuyFormState.minCrypto.toStringAsFixed(8), focusNode: _buyAmountFocusNode, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.left, // inputFormatters: [NumericalRangeFormatter()], onChanged: (_) { @@ -1060,9 +1050,10 @@ class _BuyFormState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -1073,33 +1064,34 @@ class _BuyFormState extends ConsumerState { const SizedBox(width: 2), buyWithFiat ? Container( - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 6, - ), - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .currencyListItemBG, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - format.simpleCurrencySymbol( - selectedFiat?.ticker.toUpperCase() ?? "ERR", - ), - textAlign: TextAlign.center, - style: - STextStyles.smallMed12(context).copyWith( - color: Theme.of(context) + padding: const EdgeInsets.symmetric( + vertical: 3, + horizontal: 6, + ), + decoration: BoxDecoration( + color: + Theme.of(context) .extension()! - .accentColorDark, - ), + .currencyListItemBG, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + format.simpleCurrencySymbol( + selectedFiat?.ticker.toUpperCase() ?? "ERR", + ), + textAlign: TextAlign.center, + style: STextStyles.smallMed12(context).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), - ) - : CoinIconForTicker( - ticker: selectedCrypto?.ticker ?? "BTC", - size: 20, ), + ) + : CoinIconForTicker( + ticker: selectedCrypto?.ticker ?? "BTC", + size: 20, + ), SizedBox( width: buyWithFiat ? 8 : 10, ), // maybe make isDesktop-aware? @@ -1108,9 +1100,10 @@ class _BuyFormState extends ConsumerState { ? selectedFiat?.ticker ?? "ERR" : selectedCrypto?.ticker ?? "ERR", style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ], @@ -1125,65 +1118,63 @@ class _BuyFormState extends ConsumerState { children: [ _buyAmountController.text.isNotEmpty ? TextFieldIconButton( - key: const Key( - "buyViewClearAmountFieldButtonKey", - ), - onTap: () { - // if (_BuyFormState.buyWithFiat) { - // _buyAmountController.text = _BuyFormState - // .minFiat - // .toStringAsFixed(2); - // } else { - // if (selectedCrypto?.ticker == - // _BuyFormState.boundedCryptoTicker) { - // _buyAmountController.text = _BuyFormState - // .minCrypto - // .toStringAsFixed(8); - // } - // } - _buyAmountController.text = ""; - validateAmount(); - }, - child: const XIcon(), - ) + key: const Key( + "buyViewClearAmountFieldButtonKey", + ), + onTap: () { + // if (_BuyFormState.buyWithFiat) { + // _buyAmountController.text = _BuyFormState + // .minFiat + // .toStringAsFixed(2); + // } else { + // if (selectedCrypto?.ticker == + // _BuyFormState.boundedCryptoTicker) { + // _buyAmountController.text = _BuyFormState + // .minCrypto + // .toStringAsFixed(8); + // } + // } + _buyAmountController.text = ""; + validateAmount(); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "buyViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); + key: const Key( + "buyViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); - final amountString = - Decimal.tryParse(data?.text ?? ""); - if (amountString != null) { - _buyAmountController.text = - amountString.toString(); + final amountString = Decimal.tryParse( + data?.text ?? "", + ); + if (amountString != null) { + _buyAmountController.text = + amountString.toString(); - validateAmount(); - } - }, - child: _buyAmountController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + validateAmount(); + } + }, + child: + _buyAmountController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), ], ), ), ), ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), if (_amountOutOfRangeErrorString.isNotEmpty) Text( _amountOutOfRangeErrorString, style: STextStyles.errorSmall(context), ), - SizedBox( - height: isDesktop ? 20 : 12, - ), + SizedBox(height: isDesktop ? 20 : 12), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1205,41 +1196,62 @@ class _BuyFormState extends ConsumerState { ); Navigator.of(context) .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) + ChooseFromStackView.routeName, + arguments: coin, + ) .then((value) async { - if (value is String) { - final wallet = ref.read(pWallets).getWallet(value); - - // _toController.text = manager.walletName; - // model.recipientAddress = - // await manager.currentReceivingAddress; - _receiveAddressController.text = - (await wallet.getCurrentReceivingAddress())! - .value; - - setState(() { - _addressToggleFlag = - _receiveAddressController.text.isNotEmpty; + if (value is String) { + final wallet = ref + .read(pWallets) + .getWallet(value); + + // _toController.text = manager.walletName; + // model.recipientAddress = + // await manager.currentReceivingAddress; + + final address = + await wallet.getCurrentReceivingAddress(); + + if (address!.type == AddressType.p2tr && + wallet is Bip39HDWallet) { + // lets assume any wallet that has taproot also has segwit. WCGW + final address = + await ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(wallet.walletId) + .filter() + .typeEqualTo(AddressType.p2wpkh) + .sortByDerivationIndexDesc() + .findFirst() ?? + await wallet.generateNextReceivingAddress( + derivePathType: DerivePathType.bip84, + ); + + _receiveAddressController.text = + address.value; + } else { + _receiveAddressController.text = + address.value; + } + + setState(() { + _addressToggleFlag = + _receiveAddressController.text.isNotEmpty; + }); + validateAmount(); + } }); - validateAmount(); - } - }); } catch (e, s) { - Logging.instance.w( - "", - error: e, - stackTrace: s, - ); + Logging.instance.w("", error: e, stackTrace: s); } }, ), ], ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1289,105 +1301,115 @@ class _BuyFormState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _receiveAddressController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _receiveAddressController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _addressToggleFlag ? TextFieldIconButton( - key: const Key( - "buyViewClearAddressFieldButtonKey", - ), - onTap: () { - _receiveAddressController.text = ""; - _address = ""; - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) + key: const Key( + "buyViewClearAddressFieldButtonKey", + ), + onTap: () { + _receiveAddressController.text = ""; + _address = ""; + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "buyViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .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"), - ); - } - - _receiveAddressController.text = content; - _address = content; - - setState(() { - _addressToggleFlag = - _receiveAddressController - .text.isNotEmpty; - }); - } - }, - child: _receiveAddressController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + key: const Key( + "buyViewPasteAddressFieldButtonKey", ), + onTap: () async { + final ClipboardData? data = await clipboard + .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"), + ); + } + + _receiveAddressController.text = content; + _address = content; + + setState(() { + _addressToggleFlag = + _receiveAddressController + .text + .isNotEmpty; + }); + } + }, + child: + _receiveAddressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_receiveAddressController.text.isEmpty && AppConfig.isStackCoin(selectedCrypto?.ticker) && isDesktop) TextFieldIconButton( key: const Key("buyViewAddressBookButtonKey"), onTap: () async { - final entry = - await showDialog( + final entry = await showDialog< + ContactAddressEntry? + >( context: context, - builder: (context) => DesktopDialog( - maxWidth: 696, - maxHeight: 600, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Address book", - style: STextStyles.desktopH3( - context, + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: + STextStyles.desktopH3( + context, + ), + ), ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: AppConfig.coins + .firstWhere( + (e) => + e.ticker + .toLowerCase() == + selectedCrypto!.ticker + .toString() + .toLowerCase(), + ), ), ), - const DesktopDialogCloseButton(), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: AppConfig.coins.firstWhere( - (e) => - e.ticker.toLowerCase() == - selectedCrypto!.ticker - .toString() - .toLowerCase(), - ), - ), - ), - ], - ), - ), + ), ); if (entry != null) { @@ -1408,10 +1430,10 @@ class _BuyFormState extends ConsumerState { TextFieldIconButton( key: const Key("buyViewAddressBookButtonKey"), onTap: () { - Navigator.of(context, rootNavigator: isDesktop) - .pushNamed( - AddressBookView.routeName, - ); + Navigator.of( + context, + rootNavigator: isDesktop, + ).pushNamed(AddressBookView.routeName); }, child: const AddressBookIcon(), ), @@ -1429,20 +1451,17 @@ class _BuyFormState extends ConsumerState { ), ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), if (_receivingAddressValidationErrorString.isNotEmpty) Text( _receivingAddressValidationErrorString, style: STextStyles.errorSmall(context), ), - SizedBox( - height: isDesktop ? 20 : 12, - ), + SizedBox(height: isDesktop ? 20 : 12), PrimaryButton( buttonHeight: isDesktop ? ButtonHeight.l : null, - enabled: _addressToggleFlag && + enabled: + _addressToggleFlag && _amountOutOfRangeErrorString.isEmpty && _buyAmountController.text.isNotEmpty, onPressed: () { diff --git a/lib/pages/buy_view/buy_order_details.dart b/lib/pages/buy_view/buy_order_details.dart index 7021309a2..30b6060d8 100644 --- a/lib/pages/buy_view/buy_order_details.dart +++ b/lib/pages/buy_view/buy_order_details.dart @@ -29,10 +29,7 @@ import '../../widgets/desktop/primary_button.dart'; import '../../widgets/rounded_white_container.dart'; class BuyOrderDetailsView extends ConsumerStatefulWidget { - const BuyOrderDetailsView({ - super.key, - required this.order, - }); + const BuyOrderDetailsView({super.key, required this.order}); final SimplexOrder order; @@ -74,29 +71,31 @@ Provider: Simplex style: STextStyles.navBarTitle(context), ), ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); @@ -104,21 +103,13 @@ Provider: Simplex child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Simplex order", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), + Text("Simplex order", style: STextStyles.pageTitleH1(context)), + const SizedBox(height: 16), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Purchase ID", - style: STextStyles.label(context), - ), + Text("Purchase ID", style: STextStyles.label(context)), Text( widget.order.paymentId, style: STextStyles.label(context).copyWith( @@ -128,17 +119,12 @@ Provider: Simplex ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "User ID", - style: STextStyles.label(context), - ), + Text("User ID", style: STextStyles.label(context)), Text( widget.order.userId, style: STextStyles.label(context).copyWith( @@ -148,17 +134,12 @@ Provider: Simplex ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Quote ID", - style: STextStyles.label(context), - ), + Text("Quote ID", style: STextStyles.label(context)), Text( widget.order.quote.id, style: STextStyles.label(context).copyWith( @@ -168,17 +149,12 @@ Provider: Simplex ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Quoted cost", - style: STextStyles.label(context), - ), + Text("Quoted cost", style: STextStyles.label(context)), Text( "${widget.order.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.order.quote.fiat.ticker.toUpperCase()}", style: STextStyles.label(context).copyWith( @@ -188,9 +164,7 @@ Provider: Simplex ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), // RoundedWhiteContainer( // child: Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -215,10 +189,7 @@ Provider: Simplex child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Quoted amount", - style: STextStyles.label(context), - ), + Text("Quoted amount", style: STextStyles.label(context)), Text( "${widget.order.quote.youReceiveCryptoAmount} ${widget.order.quote.crypto.ticker.toUpperCase()}", style: STextStyles.label(context).copyWith( @@ -228,9 +199,7 @@ Provider: Simplex ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -248,32 +217,23 @@ Provider: Simplex ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Provider", - style: STextStyles.label(context), - ), + Text("Provider", style: STextStyles.label(context)), SizedBox( width: 64, height: 32, child: SvgPicture.asset( - Assets.buy.simplexLogo( - ref.watch(themeProvider).brightness, - ), + Assets.buy.simplexLogo(ref.watch(themeProvider).brightness), ), ), ], ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -307,18 +267,15 @@ Provider: Simplex Assets.svg.copy, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - const SizedBox( - width: 10, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), + const SizedBox(width: 10), Text( "Copy to clipboard", - style: STextStyles.desktopButtonSecondaryEnabled( - context, - ), + style: STextStyles.desktopButtonSecondaryEnabled(context), ), ], ), diff --git a/lib/pages/buy_view/buy_quote_preview.dart b/lib/pages/buy_view/buy_quote_preview.dart index 7d736e3a6..8137bce71 100644 --- a/lib/pages/buy_view/buy_quote_preview.dart +++ b/lib/pages/buy_view/buy_quote_preview.dart @@ -14,8 +14,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; + import '../../models/buy/response_objects/quote.dart'; -import 'sub_widgets/buy_warning_popup.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; import '../../utilities/assets.dart'; @@ -26,12 +26,10 @@ import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/rounded_white_container.dart'; +import 'sub_widgets/buy_warning_popup.dart'; class BuyQuotePreviewView extends ConsumerStatefulWidget { - const BuyQuotePreviewView({ - super.key, - required this.quote, - }); + const BuyQuotePreviewView({super.key, required this.quote}); final SimplexQuote quote; @@ -48,9 +46,7 @@ class _BuyQuotePreviewViewState extends ConsumerState { Future _buyWarning() async { await showDialog( context: context, - builder: (context) => BuyWarningPopup( - quote: widget.quote, - ), + builder: (context) => BuyWarningPopup(quote: widget.quote), ); } @@ -76,29 +72,31 @@ class _BuyQuotePreviewViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); @@ -110,17 +108,12 @@ class _BuyQuotePreviewViewState extends ConsumerState { "Buy ${widget.quote.crypto.ticker.toUpperCase()}", style: STextStyles.pageTitleH1(context), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "You pay", - style: STextStyles.label(context), - ), + Text("You pay", style: STextStyles.label(context)), Text( "${format.simpleCurrencySymbol(widget.quote.fiat.ticker.toUpperCase())}${widget.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.quote.fiat.ticker.toUpperCase()}", style: STextStyles.label(context).copyWith( @@ -130,9 +123,7 @@ class _BuyQuotePreviewViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), // RoundedWhiteContainer( // child: Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -157,10 +148,7 @@ class _BuyQuotePreviewViewState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "You receive", - style: STextStyles.label(context), - ), + Text("You receive", style: STextStyles.label(context)), Text( "${widget.quote.youReceiveCryptoAmount} ${widget.quote.crypto.ticker.toUpperCase()}", style: STextStyles.label(context).copyWith( @@ -170,9 +158,7 @@ class _BuyQuotePreviewViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -190,17 +176,12 @@ class _BuyQuotePreviewViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Quote ID", - style: STextStyles.label(context), - ), + Text("Quote ID", style: STextStyles.label(context)), Text( widget.quote.id, style: STextStyles.label(context).copyWith( @@ -210,37 +191,25 @@ class _BuyQuotePreviewViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Provider", - style: STextStyles.label(context), - ), + Text("Provider", style: STextStyles.label(context)), SizedBox( width: 64, height: 32, child: SvgPicture.asset( - Assets.buy.simplexLogo( - ref.watch(themeProvider).brightness, - ), + Assets.buy.simplexLogo(ref.watch(themeProvider).brightness), ), ), ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), const Spacer(), - PrimaryButton( - label: "Buy", - onPressed: _buyWarning, - ), + PrimaryButton(label: "Buy", onPressed: _buyWarning), ], ), ); diff --git a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart index 432a3435a..089b492af 100644 --- a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart +++ b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart @@ -32,10 +32,7 @@ import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; class CryptoSelectionView extends ConsumerStatefulWidget { - const CryptoSelectionView({ - super.key, - required this.coins, - }); + const CryptoSelectionView({super.key, required this.coins}); final List coins; @@ -122,11 +119,11 @@ class _CryptoSelectionViewState extends ConsumerState { style: STextStyles.pageTitleH2(context), ), ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, ), - child: child, ), ), ); @@ -135,10 +132,7 @@ class _CryptoSelectionViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, children: [ - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -168,39 +162,33 @@ class _CryptoSelectionViewState extends ConsumerState { height: 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 = ""; - }); - filter(""); - }, - ), - ], + 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 = ""; + }); + filter(""); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), - const SizedBox( - height: 10, - ), - Text( - "All coins", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 10), + Text("All coins", style: STextStyles.smallMed12(context)), + const SizedBox(height: 12), Flexible( child: RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -226,9 +214,7 @@ class _CryptoSelectionViewState extends ConsumerState { ticker: _coins[index].ticker, ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -237,16 +223,16 @@ class _CryptoSelectionViewState extends ConsumerState { _coins[index].name, style: STextStyles.largeMedium14(context), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( _coins[index].ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + style: STextStyles.smallMed12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle1, ), ), ], @@ -297,9 +283,7 @@ class CoinIconForTicker extends ConsumerWidget { try { final coin = AppConfig.getCryptoCurrencyForTicker(ticker)!; return SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: size, height: size, ); diff --git a/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart b/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart index 4fee72444..05bb3a6df 100644 --- a/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart +++ b/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart @@ -28,10 +28,7 @@ import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; class FiatSelectionView extends StatefulWidget { - const FiatSelectionView({ - super.key, - required this.fiats, - }); + const FiatSelectionView({super.key, required this.fiats}); final List fiats; @@ -122,11 +119,11 @@ class _FiatSelectionViewState extends State { style: STextStyles.pageTitleH2(context), ), ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, ), - child: child, ), ), ); @@ -135,10 +132,7 @@ class _FiatSelectionViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, children: [ - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -168,39 +162,33 @@ class _FiatSelectionViewState extends State { height: 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 = ""; - }); - filter(""); - }, - ), - ], + 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 = ""; + }); + filter(""); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), - const SizedBox( - height: 10, - ), - Text( - "All currencies", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 10), + Text("All currencies", style: STextStyles.smallMed12(context)), + const SizedBox(height: 12), Flexible( child: SingleChildScrollView( child: RoundedWhiteContainer( @@ -212,95 +200,92 @@ class _FiatSelectionViewState extends State { }, defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - ..._fiats.map( - (e) { - return TableRow( - children: [ - TableCell( - verticalAlignment: - TableCellVerticalAlignment.fill, - child: GestureDetector( - onTap: () => Navigator.of(context).pop(e), - child: Container( - color: Colors.transparent, - padding: const EdgeInsets.only(left: 12), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Container( - padding: const EdgeInsets.all(7.5), - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .currencyListItemBG, - borderRadius: - BorderRadius.circular(4), + ..._fiats.map((e) { + return TableRow( + children: [ + TableCell( + verticalAlignment: TableCellVerticalAlignment.fill, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(e), + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.only(left: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(7.5), + decoration: BoxDecoration( + color: + Theme.of(context) + .extension()! + .currencyListItemBG, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + format.simpleCurrencySymbol( + e.ticker.toUpperCase(), ), - child: Text( - format.simpleCurrencySymbol( - e.ticker.toUpperCase(), - ), - style: STextStyles.subtitle(context) - .apply( - fontSizeFactor: (1 / - format - .simpleCurrencySymbol( - e.ticker.toUpperCase(), - ) - .length * // Couldn't get pow() working here - format - .simpleCurrencySymbol( - e.ticker.toUpperCase(), - ) - .length), - ), - textAlign: TextAlign.center, + style: STextStyles.subtitle( + context, + ).apply( + fontSizeFactor: + (1 / + format + .simpleCurrencySymbol( + e.ticker.toUpperCase(), + ) + .length * // Couldn't get pow() working here + format + .simpleCurrencySymbol( + e.ticker.toUpperCase(), + ) + .length), ), + textAlign: TextAlign.center, ), - ], - ), + ), + ], ), ), ), - GestureDetector( - onTap: () => Navigator.of(context).pop(e), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - e.name, - style: - STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, - ), - Text( - e.ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), + ), + GestureDetector( + onTap: () => Navigator.of(context).pop(e), + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + e.name, + style: STextStyles.largeMedium14(context), + ), + const SizedBox(height: 2), + Text( + e.ticker.toUpperCase(), + style: STextStyles.smallMed12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle1, ), - ], - ), + ), + ], ), ), ), - ], - ); - }, - ), + ), + ], + ); + }), ], ), diff --git a/lib/pages/cashfusion/cashfusion_view.dart b/lib/pages/cashfusion/cashfusion_view.dart index 4579dbc52..53da25374 100644 --- a/lib/pages/cashfusion/cashfusion_view.dart +++ b/lib/pages/cashfusion/cashfusion_view.dart @@ -37,10 +37,7 @@ import 'fusion_progress_view.dart'; import 'fusion_rounds_selection_sheet.dart'; class CashFusionView extends ConsumerStatefulWidget { - const CashFusionView({ - super.key, - required this.walletId, - }); + const CashFusionView({super.key, required this.walletId}); static const routeName = "/cashFusionView"; @@ -74,15 +71,16 @@ class _CashFusionViewState extends ConsumerState { ); } catch (e) { if (!e.toString().contains( - "FusionProgressUIState was already set for ${widget.walletId}", - )) { + "FusionProgressUIState was already set for ${widget.walletId}", + )) { rethrow; } } - final int rounds = _option == FusionOption.continuous - ? 0 - : int.parse(fusionRoundController.text); + final int rounds = + _option == FusionOption.continuous + ? 0 + : int.parse(fusionRoundController.text); final newInfo = FusionInfo( host: serverController.text, @@ -94,16 +92,11 @@ class _CashFusionViewState extends ConsumerState { // update user prefs (persistent) ref.read(prefsChangeNotifierProvider).setFusionServerInfo(coin, newInfo); - unawaited( - fusionWallet.fuse( - fusionInfo: newInfo, - ), - ); + unawaited(fusionWallet.fuse(fusionInfo: newInfo)); - await Navigator.of(context).pushNamed( - FusionProgressView.routeName, - arguments: widget.walletId, - ); + await Navigator.of( + context, + ).pushNamed(FusionProgressView.routeName, arguments: widget.walletId); } @override @@ -118,8 +111,9 @@ class _CashFusionViewState extends ConsumerState { coin = ref.read(pWalletCoin(widget.walletId)); - final info = - ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin); + final info = ref + .read(prefsChangeNotifierProvider) + .getFusionServerInfo(coin); serverController.text = info.host; portController.text = info.port.toString(); @@ -156,10 +150,7 @@ class _CashFusionViewState extends ConsumerState { appBar: AppBar( automaticallyImplyLeading: false, leading: const AppBarBackButton(), - title: Text( - "Fusion", - style: STextStyles.navBarTitle(context), - ), + title: Text("Fusion", style: STextStyles.navBarTitle(context)), titleSpacing: 0, actions: [ AspectRatio( @@ -170,9 +161,10 @@ class _CashFusionViewState extends ConsumerState { Assets.svg.circleQuestion, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: () async { //' TODO show about? @@ -181,225 +173,89 @@ class _CashFusionViewState extends ConsumerState { ), ], ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RoundedWhiteContainer( - child: Text( - "Fusion helps anonymize your coins by mixing them.", - style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RoundedWhiteContainer( + child: Text( + "Fusion helps anonymize your coins by mixing them.", + style: STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), ), ), - ), - const SizedBox( - height: 16, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Server settings", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Server settings", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), + ), + CustomTextButton( + text: "Default", + onTap: () { + final def = + kFusionServerInfoDefaults[coin]!; + serverController.text = def.host; + portController.text = def.port.toString(); + fusionRoundController.text = + def.rounds.toString(); + _option = FusionOption.continuous; + setState(() { + _enableSSLCheckbox = def.ssl; + }); + }, ), + ], + ), + const SizedBox(height: 12), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - CustomTextButton( - text: "Default", - onTap: () { - final def = kFusionServerInfoDefaults[coin]!; - serverController.text = def.host; - portController.text = def.port.toString(); - fusionRoundController.text = - def.rounds.toString(); - _option = FusionOption.continuous; + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: serverController, + focusNode: serverFocusNode, + onChanged: (value) { setState(() { - _enableSSLCheckbox = def.ssl; + _enableStartButton = + value.isNotEmpty && + portController.text.isNotEmpty && + fusionRoundController.text.isNotEmpty; }); }, - ), - ], - ), - const SizedBox( - height: 12, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: false, - enableSuggestions: false, - controller: serverController, - focusNode: serverFocusNode, - onChanged: (value) { - setState(() { - _enableStartButton = value.isNotEmpty && - portController.text.isNotEmpty && - fusionRoundController.text.isNotEmpty; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Server", - serverFocusNode, - context, - desktopMed: true, - ), - ), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: false, - enableSuggestions: false, - controller: portController, - focusNode: portFocusNode, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - keyboardType: TextInputType.number, - onChanged: (value) { - setState(() { - _enableStartButton = value.isNotEmpty && - serverController.text.isNotEmpty && - fusionRoundController.text.isNotEmpty; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Port", - portFocusNode, - context, - ), - ), - ), - const SizedBox( - height: 10, - ), - GestureDetector( - onTap: () { - setState(() { - _enableSSLCheckbox = !_enableSSLCheckbox; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: Checkbox( - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - value: _enableSSLCheckbox, - onChanged: (newValue) { - setState( - () { - _enableSSLCheckbox = - !_enableSSLCheckbox; - }, - ); - }, - ), - ), - const SizedBox( - width: 12, - ), - Text( - "Use SSL", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - ), - const SizedBox( - height: 16, - ), - Text( - "Rounds of fusion", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - ), - const SizedBox( - height: 12, - ), - RoundedContainer( - onPressed: () async { - final option = - await showModalBottomSheet( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Server", + serverFocusNode, + context, + desktopMed: true, ), - builder: (_) { - return FusionRoundCountSelectSheet( - currentOption: _option, - ); - }, - ); - if (option != null) { - setState(() { - _option = option; - }); - } - }, - color: Theme.of(context) - .extension()! - .textFieldActiveBG, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - _option.name.capitalize(), - style: STextStyles.w500_12(context), - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ], ), ), - ), - if (_option == FusionOption.custom) - const SizedBox( - height: 10, - ), - if (_option == FusionOption.custom) + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -407,45 +263,176 @@ class _CashFusionViewState extends ConsumerState { child: TextField( autocorrect: false, enableSuggestions: false, - controller: fusionRoundController, - focusNode: fusionRoundFocusNode, + controller: portController, + focusNode: portFocusNode, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], keyboardType: TextInputType.number, onChanged: (value) { setState(() { - _enableStartButton = value.isNotEmpty && + _enableStartButton = + value.isNotEmpty && serverController.text.isNotEmpty && - portController.text.isNotEmpty; + fusionRoundController.text.isNotEmpty; }); }, style: STextStyles.field(context), decoration: standardInputDecoration( - "Number of fusions", - fusionRoundFocusNode, + "Port", + portFocusNode, context, - ).copyWith( - labelText: "Enter number of fusions..", ), ), ), - const SizedBox( - height: 16, - ), - const Spacer(), - PrimaryButton( - label: "Start", - enabled: _enableStartButton, - onPressed: _startFusion, - ), - ], + const SizedBox(height: 10), + GestureDetector( + onTap: () { + setState(() { + _enableSSLCheckbox = !_enableSSLCheckbox; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Checkbox( + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _enableSSLCheckbox, + onChanged: (newValue) { + setState(() { + _enableSSLCheckbox = + !_enableSSLCheckbox; + }); + }, + ), + ), + const SizedBox(width: 12), + Text( + "Use SSL", + style: STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Text( + "Rounds of fusion", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), + ), + const SizedBox(height: 12), + RoundedContainer( + onPressed: () async { + final option = + await showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) { + return FusionRoundCountSelectSheet( + currentOption: _option, + ); + }, + ); + if (option != null) { + setState(() { + _option = option; + }); + } + }, + color: + Theme.of( + context, + ).extension()!.textFieldActiveBG, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + _option.name.capitalize(), + style: STextStyles.w500_12(context), + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + color: + Theme.of(context) + .extension()! + .textSubtitle1, + ), + ], + ), + ), + ), + if (_option == FusionOption.custom) + const SizedBox(height: 10), + if (_option == FusionOption.custom) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: fusionRoundController, + focusNode: fusionRoundFocusNode, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + keyboardType: TextInputType.number, + onChanged: (value) { + setState(() { + _enableStartButton = + value.isNotEmpty && + serverController.text.isNotEmpty && + portController.text.isNotEmpty; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Number of fusions", + fusionRoundFocusNode, + context, + ).copyWith( + labelText: "Enter number of fusions..", + ), + ), + ), + const SizedBox(height: 16), + const Spacer(), + PrimaryButton( + label: "Start", + enabled: _enableStartButton, + onPressed: _startFusion, + ), + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/cashfusion/fusion_progress_view.dart b/lib/pages/cashfusion/fusion_progress_view.dart index 022fa0861..021a8ef69 100644 --- a/lib/pages/cashfusion/fusion_progress_view.dart +++ b/lib/pages/cashfusion/fusion_progress_view.dart @@ -33,10 +33,7 @@ import '../../widgets/rounded_container.dart'; import '../../widgets/stack_dialog.dart'; class FusionProgressView extends ConsumerStatefulWidget { - const FusionProgressView({ - super.key, - required this.walletId, - }); + const FusionProgressView({super.key, required this.walletId}); static const routeName = "/cashFusionProgressView"; @@ -52,23 +49,24 @@ class _FusionProgressViewState extends ConsumerState { final shouldCancel = await showDialog( context: context, barrierDismissible: false, - builder: (_) => StackDialog( - title: "Cancel fusion?", - leftButton: SecondaryButton( - label: "No", - buttonHeight: null, - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: PrimaryButton( - label: "Yes", - buttonHeight: null, - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ), + builder: + (_) => StackDialog( + title: "Cancel fusion?", + leftButton: SecondaryButton( + label: "No", + buttonHeight: null, + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: PrimaryButton( + label: "Yes", + buttonHeight: null, + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), ); if (shouldCancel == true && mounted) { @@ -113,9 +111,10 @@ class _FusionProgressViewState extends ConsumerState { final bool _failed = ref.watch(fusionProgressUIStateProvider(widget.walletId)).failed; - final int _fusionRoundsCompleted = ref - .watch(fusionProgressUIStateProvider(widget.walletId)) - .fusionRoundsCompleted; + final int _fusionRoundsCompleted = + ref + .watch(fusionProgressUIStateProvider(widget.walletId)) + .fusionRoundsCompleted; WakelockPlus.enable(); @@ -124,28 +123,28 @@ class _FusionProgressViewState extends ConsumerState { return await _requestAndProcessCancel(); }, child: Background( - child: SafeArea( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - automaticallyImplyLeading: false, - leading: AppBarBackButton( - onPressed: () async { - if (await _requestAndProcessCancel()) { - if (mounted) { - Navigator.of(context).pop(); - } + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: AppBarBackButton( + onPressed: () async { + if (await _requestAndProcessCancel()) { + if (mounted) { + Navigator.of(context).pop(); } - }, - ), - title: Text( - "Fusion progress", - style: STextStyles.navBarTitle(context), - ), - titleSpacing: 0, + } + }, ), - body: LayoutBuilder( + title: Text( + "Fusion progress", + style: STextStyles.navBarTitle(context), + ), + titleSpacing: 0, + ), + body: SafeArea( + child: LayoutBuilder( builder: (builderContext, constraints) { return SingleChildScrollView( child: ConstrainedBox( @@ -160,64 +159,57 @@ class _FusionProgressViewState extends ConsumerState { children: [ if (_fusionRoundsCompleted == 0) RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackError, + color: + Theme.of(context) + .extension()! + .snackBarBackError, child: Text( "Do not close this window. If you exit, " "the process will be canceled.", - style: - STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .snackBarTextError, + style: STextStyles.smallMed14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .snackBarTextError, ), textAlign: TextAlign.center, ), ), if (_fusionRoundsCompleted > 0) RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackInfo, + color: + Theme.of(context) + .extension()! + .snackBarBackInfo, child: Text( "Fusion rounds completed: $_fusionRoundsCompleted", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .snackBarTextInfo, + color: + Theme.of(context) + .extension()! + .snackBarTextInfo, ), textAlign: TextAlign.center, ), ), - const SizedBox( - height: 20, - ), - FusionProgress( - walletId: widget.walletId, - ), + const SizedBox(height: 20), + FusionProgress(walletId: widget.walletId), const Spacer(), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), if (_succeeded) PrimaryButton( label: "Fuse again", onPressed: _fuseAgain, ), - if (_succeeded) - const SizedBox( - height: 16, - ), + if (_succeeded) const SizedBox(height: 16), if (_failed) PrimaryButton( label: "Try again", onPressed: _fuseAgain, ), - if (_failed) - const SizedBox( - height: 16, - ), + if (_failed) const SizedBox(height: 16), SecondaryButton( label: "Cancel", onPressed: () async { @@ -247,8 +239,9 @@ class _FusionProgressViewState extends ConsumerState { final fusionWallet = ref.read(pWallets).getWallet(widget.walletId) as CashFusionInterface; - final fusionInfo = - ref.read(prefsChangeNotifierProvider).getFusionServerInfo(coin); + final fusionInfo = ref + .read(prefsChangeNotifierProvider) + .getFusionServerInfo(coin); try { fusionWallet.uiState = ref.read( @@ -256,8 +249,8 @@ class _FusionProgressViewState extends ConsumerState { ); } catch (e) { if (!e.toString().contains( - "FusionProgressUIState was already set for ${widget.walletId}", - )) { + "FusionProgressUIState was already set for ${widget.walletId}", + )) { rethrow; } } diff --git a/lib/pages/churning/churning_progress_view.dart b/lib/pages/churning/churning_progress_view.dart index 55319738a..a214640c6 100644 --- a/lib/pages/churning/churning_progress_view.dart +++ b/lib/pages/churning/churning_progress_view.dart @@ -19,10 +19,7 @@ import '../../widgets/stack_dialog.dart'; import 'churn_error_dialog.dart'; class ChurningProgressView extends ConsumerStatefulWidget { - const ChurningProgressView({ - super.key, - required this.walletId, - }); + const ChurningProgressView({super.key, required this.walletId}); static const routeName = "/churningProgressView"; @@ -37,23 +34,24 @@ class _ChurningProgressViewState extends ConsumerState { final shouldCancel = await showDialog( context: context, barrierDismissible: false, - builder: (_) => StackDialog( - title: "Cancel churning?", - leftButton: SecondaryButton( - label: "No", - buttonHeight: null, - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: PrimaryButton( - label: "Yes", - buttonHeight: null, - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ), + builder: + (_) => StackDialog( + title: "Cancel churning?", + leftButton: SecondaryButton( + label: "No", + buttonHeight: null, + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: PrimaryButton( + label: "Yes", + buttonHeight: null, + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), ); if (shouldCancel == true && mounted) { @@ -102,10 +100,11 @@ class _ChurningProgressViewState extends ConsumerState { if (context.mounted) { showDialog( context: context, - builder: (context) => ChurnErrorDialog( - error: n.toString(), - walletId: widget.walletId, - ), + builder: + (context) => ChurnErrorDialog( + error: n.toString(), + walletId: widget.walletId, + ), ); } } @@ -117,28 +116,28 @@ class _ChurningProgressViewState extends ConsumerState { return await _requestAndProcessCancel(); }, child: Background( - child: SafeArea( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - automaticallyImplyLeading: false, - leading: AppBarBackButton( - onPressed: () async { - if (await _requestAndProcessCancel()) { - if (context.mounted) { - Navigator.of(context).pop(); - } + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: AppBarBackButton( + onPressed: () async { + if (await _requestAndProcessCancel()) { + if (context.mounted) { + Navigator.of(context).pop(); } - }, - ), - title: Text( - "Churning progress", - style: STextStyles.navBarTitle(context), - ), - titleSpacing: 0, + } + }, + ), + title: Text( + "Churning progress", + style: STextStyles.navBarTitle(context), ), - body: LayoutBuilder( + titleSpacing: 0, + ), + body: SafeArea( + child: LayoutBuilder( builder: (builderContext, constraints) { return SingleChildScrollView( child: ConstrainedBox( @@ -153,91 +152,85 @@ class _ChurningProgressViewState extends ConsumerState { children: [ if (_roundsCompleted == 0) RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackError, + color: + Theme.of(context) + .extension()! + .snackBarBackError, child: Text( "Do not close this window. If you exit, " "the process will be canceled.", - style: - STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .snackBarTextError, + style: STextStyles.smallMed14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .snackBarTextError, ), textAlign: TextAlign.center, ), ), if (_roundsCompleted > 0) RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackInfo, + color: + Theme.of(context) + .extension()! + .snackBarBackInfo, child: Text( "Churning rounds completed: $_roundsCompleted", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .snackBarTextInfo, + color: + Theme.of(context) + .extension()! + .snackBarTextInfo, ), textAlign: TextAlign.center, ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), const MoneroChanDance(), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), ProgressItem( iconAsset: Assets.svg.alertCircle, - label: "Waiting for balance to unlock ${ref.watch( - pChurningService(widget.walletId) - .select((s) => s.confirmsInfo), - ) ?? ""}", + label: + "Waiting for balance to unlock ${ref.watch(pChurningService(widget.walletId).select((s) => s.confirmsInfo)) ?? ""}", status: ref.watch( - pChurningService(widget.walletId) - .select((s) => s.waitingForUnlockedBalance), + pChurningService( + widget.walletId, + ).select((s) => s.waitingForUnlockedBalance), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), ProgressItem( iconAsset: Assets.svg.churn, label: "Creating churn transaction", status: ref.watch( - pChurningService(widget.walletId) - .select((s) => s.makingChurnTransaction), + pChurningService( + widget.walletId, + ).select((s) => s.makingChurnTransaction), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), ProgressItem( iconAsset: Assets.svg.checkCircle, label: "Complete", status: ref.watch( - pChurningService(widget.walletId) - .select((s) => s.completedStatus), + pChurningService( + widget.walletId, + ).select((s) => s.completedStatus), ), ), const Spacer(), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), if (_succeeded) PrimaryButton( label: "Churn again", - onPressed: ref - .read(pChurningService(widget.walletId)) - .churn, - ), - if (_succeeded) - const SizedBox( - height: 16, + onPressed: + ref + .read(pChurningService(widget.walletId)) + .churn, ), + if (_succeeded) const SizedBox(height: 16), SecondaryButton( label: "Cancel", onPressed: () async { diff --git a/lib/pages/churning/churning_view.dart b/lib/pages/churning/churning_view.dart index d59042e5e..6b23ed8f7 100644 --- a/lib/pages/churning/churning_view.dart +++ b/lib/pages/churning/churning_view.dart @@ -23,10 +23,7 @@ import 'churning_progress_view.dart'; import 'churning_rounds_selection_sheet.dart'; class ChurningView extends ConsumerStatefulWidget { - const ChurningView({ - super.key, - required this.walletId, - }); + const ChurningView({super.key, required this.walletId}); static const routeName = "/churnView"; @@ -47,16 +44,16 @@ class _ChurnViewState extends ConsumerState { Future _startChurn() async { final churningService = ref.read(pChurningService(widget.walletId)); - final int rounds = _option == ChurnOption.continuous - ? 0 - : int.parse(churningRoundController.text); + final int rounds = + _option == ChurnOption.continuous + ? 0 + : int.parse(churningRoundController.text); churningService.rounds = rounds; - await Navigator.of(context).pushNamed( - ChurningProgressView.routeName, - arguments: widget.walletId, - ); + await Navigator.of( + context, + ).pushNamed(ChurningProgressView.routeName, arguments: widget.walletId); } @override @@ -86,61 +83,58 @@ class _ChurnViewState extends ConsumerState { @override Widget build(BuildContext context) { return Background( - child: SafeArea( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - automaticallyImplyLeading: false, - leading: const AppBarBackButton(), - title: Text( - "Churn", - style: STextStyles.navBarTitle(context), - ), - titleSpacing: 0, - actions: [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - size: 36, - icon: SvgPicture.asset( - Assets.svg.circleQuestion, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: () async { - await showDialog( - context: context, - builder: (context) => const StackOkDialog( - title: "What is churning?", - message: "Churning in a Monero wallet involves" - " sending Monero to oneself in multiple" - " transactions, which can enhance privacy" - " by making it harder for observers to " - "link your transactions. This process" - " re-mixes the funds within the network," - " helping obscure transaction history. " - "Churning is optional and mainly beneficial" - " in scenarios where maximum privacy is" - " desired or if you received the Monero from" - " a source from which you'd like to disassociate.", - ), - ); - }, + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: const AppBarBackButton(), + title: Text("Churn", style: STextStyles.navBarTitle(context)), + titleSpacing: 0, + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), + onPressed: () async { + await showDialog( + context: context, + builder: + (context) => const StackOkDialog( + title: "What is churning?", + message: + "Churning in a Monero wallet involves" + " sending Monero to oneself in multiple" + " transactions, which can enhance privacy" + " by making it harder for observers to " + "link your transactions. This process" + " re-mixes the funds within the network," + " helping obscure transaction history. " + "Churning is optional and mainly beneficial" + " in scenarios where maximum privacy is" + " desired or if you received the Monero from" + " a source from which you'd like to disassociate.", + ), + ); + }, ), - ], - ), - body: LayoutBuilder( + ), + ], + ), + body: SafeArea( + child: LayoutBuilder( builder: (builderContext, constraints) { return SingleChildScrollView( child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), + constraints: BoxConstraints(minHeight: constraints.maxHeight), child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.all(16), @@ -151,55 +145,52 @@ class _ChurnViewState extends ConsumerState { child: Text( "Churning helps anonymize your coins by mixing them.", style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ), - const SizedBox( - height: 16, - ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), + const SizedBox(height: 16), Text( "Configuration", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedContainer( onPressed: () async { final option = await showModalBottomSheet( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) { - return ChurnRoundCountSelectSheet( - currentOption: _option, + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) { + return ChurnRoundCountSelectSheet( + currentOption: _option, + ); + }, ); - }, - ); if (option != null) { setState(() { _option = option; }); } }, - color: Theme.of(context) - .extension()! - .textFieldActiveBG, + color: + Theme.of( + context, + ).extension()!.textFieldActiveBG, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( @@ -213,18 +204,17 @@ class _ChurnViewState extends ConsumerState { SvgPicture.asset( Assets.svg.chevronDown, width: 12, - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of(context) + .extension()! + .textSubtitle1, ), ], ), ), ), if (_option == ChurnOption.custom) - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), if (_option == ChurnOption.custom) ClipRRect( borderRadius: BorderRadius.circular( @@ -254,23 +244,20 @@ class _ChurnViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), CheckboxTextButton( label: "Pause on errors", - initialValue: !ref - .read(pChurningService(widget.walletId)) - .ignoreErrors, + initialValue: + !ref + .read(pChurningService(widget.walletId)) + .ignoreErrors, onChanged: (value) { ref .read(pChurningService(widget.walletId)) .ignoreErrors = !value; }, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), const Spacer(), PrimaryButton( label: "Start", diff --git a/lib/pages/exchange_view/choose_from_stack_view.dart b/lib/pages/exchange_view/choose_from_stack_view.dart index 73d79f647..d4cc11dac 100644 --- a/lib/pages/exchange_view/choose_from_stack_view.dart +++ b/lib/pages/exchange_view/choose_from_stack_view.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/constants.dart'; @@ -23,10 +24,7 @@ import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart'; import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; class ChooseFromStackView extends ConsumerStatefulWidget { - const ChooseFromStackView({ - super.key, - required this.coin, - }); + const ChooseFromStackView({super.key, required this.coin}); final CryptoCurrency coin; @@ -48,12 +46,13 @@ class _ChooseFromStackViewState extends ConsumerState { @override Widget build(BuildContext context) { - final walletIds = ref - .watch(pWallets) - .wallets - .where((e) => e.info.coin == coin) - .map((e) => e.walletId) - .toList(); + final walletIds = + ref + .watch(pWallets) + .wallets + .where((e) => e.info.coin == coin) + .map((e) => e.walletId) + .toList(); return Background( child: Scaffold( @@ -65,80 +64,84 @@ class _ChooseFromStackViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: walletIds.isEmpty - ? Column( - children: [ - RoundedWhiteContainer( - child: Center( - child: Text( - "No ${coin.ticker.toUpperCase()} wallets", - style: STextStyles.itemSubtitle(context), - ), - ), - ), - ], - ) - : ListView.builder( - itemCount: walletIds.length, - itemBuilder: (context, index) { - final walletId = walletIds[index]; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension()! - .highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: + walletIds.isEmpty + ? Column( + children: [ + RoundedWhiteContainer( + child: Center( + child: Text( + "No ${coin.ticker.toUpperCase()} wallets", + style: STextStyles.itemSubtitle(context), + ), ), ), - padding: const EdgeInsets.all(0), - // color: Theme.of(context).extension()!.popupBG, - elevation: 0, - onPressed: () async { - if (mounted) { - Navigator.of(context).pop(walletId); - } - }, - child: RoundedWhiteContainer( - // color: Colors.transparent, - child: Row( - children: [ - WalletInfoCoinIcon(coin: coin), - const SizedBox( - width: 12, + ], + ) + : ListView.builder( + itemCount: walletIds.length, + itemBuilder: (context, index) { + final walletId = walletIds[index]; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: RawMaterialButton( + splashColor: + Theme.of( + context, + ).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ref.watch(pWalletName(walletId)), - style: STextStyles.titleBold12(context), - overflow: TextOverflow.ellipsis, + ), + padding: const EdgeInsets.all(0), + // color: Theme.of(context).extension()!.popupBG, + elevation: 0, + onPressed: () async { + if (mounted) { + Navigator.of(context).pop(walletId); + } + }, + child: RoundedWhiteContainer( + // color: Colors.transparent, + child: Row( + children: [ + WalletInfoCoinIcon(coin: coin), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ref.watch(pWalletName(walletId)), + style: STextStyles.titleBold12( + context, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + WalletInfoRowBalance( + walletId: walletIds[index], + ), + ], ), - const SizedBox( - height: 2, - ), - WalletInfoRowBalance( - walletId: walletIds[index], - ), - ], - ), + ), + ], ), - ], + ), ), - ), - ), - ); - }, - ), + ); + }, + ), + ), ), ), ); diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 999fca764..69e77f13c 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -17,8 +17,8 @@ import 'package:uuid/uuid.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/trade_wallet_lookup.dart'; +import '../../notifications/show_flush_bar.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; -import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; import '../../themes/stack_colors.dart'; @@ -98,11 +98,7 @@ class _ConfirmChangeNowSendViewState ), ); - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); late String txid; Future txidFuture; @@ -118,10 +114,7 @@ class _ConfirmChangeNowSendViewState unawaited(wallet.refresh()); - final results = await Future.wait([ - txidFuture, - time, - ]); + final results = await Future.wait([txidFuture, time]); sendProgressController.triggerSuccess?.call(); await Future.delayed(const Duration(seconds: 5)); @@ -129,15 +122,15 @@ class _ConfirmChangeNowSendViewState txid = (results.first as TxData).txid!; // save note - await ref.read(mainDBProvider).putTransactionNote( - TransactionNote( - walletId: walletId, - txid: txid, - value: note, - ), + await ref + .read(mainDBProvider) + .putTransactionNote( + TransactionNote(walletId: walletId, txid: txid, value: note), ); - await ref.read(tradeSentFromStackLookupProvider).save( + await ref + .read(tradeSentFromStackLookupProvider) + .save( tradeWalletLookup: TradeWalletLookup( uuid: const Uuid().v1(), txid: txid, @@ -162,7 +155,11 @@ class _ConfirmChangeNowSendViewState Navigator.of(context).popUntil(ModalRoute.withName(routeOnSuccessName)); } } catch (e, s) { - Logging.instance.e("Broadcast transaction failed: ", error: e, stackTrace: s); + Logging.instance.e( + "Broadcast transaction failed: ", + error: e, + stackTrace: s, + ); // pop sending dialog Navigator.of(context).pop(); @@ -182,9 +179,10 @@ class _ConfirmChangeNowSendViewState child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), ), onPressed: () { @@ -205,53 +203,61 @@ class _ConfirmChangeNowSendViewState if (Util.isDesktop) { unlocked = await showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.end, + builder: + (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - DesktopDialogCloseButton(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend( - coin: coin, - ), - ), - ], - ), - ), + ), ); } else { unlocked = await Navigator.push( context, RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", - ), + builder: + (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), settings: const RouteSettings(name: "/confirmsendlockscreen"), ), ); } - if (unlocked is bool && unlocked && mounted) { - await _attemptSend(context); + if (unlocked is bool && mounted) { + if (unlocked) { + await _attemptSend(context); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase", + context: context, + ), + ); + } } } @@ -289,234 +295,226 @@ class _ConfirmChangeNowSendViewState style: STextStyles.navBarTitle(context), ), ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); }, child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - children: [ - Row( + builder: + (child) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( children: [ - const SizedBox( - width: 6, - ), - const AppBarBackButton( - isCompact: true, - iconSize: 23, - ), - const SizedBox( - width: 12, - ), - Text( - "Confirm ${ref.watch(pWalletCoin(walletId)).ticker} transaction", - style: STextStyles.desktopH3(context), + Row( + children: [ + const SizedBox(width: 6), + const AppBarBackButton(isCompact: true, iconSize: 23), + const SizedBox(width: 12), + Text( + "Confirm ${ref.watch(pWalletCoin(walletId)).ticker} transaction", + style: STextStyles.desktopH3(context), + ), + ], ), - ], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Column( - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - borderColor: Theme.of(context) - .extension()! - .background, - child: child, + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, ), - const SizedBox( - height: 16, - ), - Row( + child: Column( children: [ - Text( - "Transaction fee", - style: - STextStyles.desktopTextExtraExtraSmall(context), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: child, ), - ], - ), - const SizedBox( - height: 10, - ), - RoundedContainer( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - ref - .watch( - pAmountFormatter( - ref.watch(pWalletCoin(walletId)), - ), - ) - .format(widget.txData.fee!), - style: - STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - ], - ), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackSuccess, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Total amount", - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, + const SizedBox(height: 16), + Row( + children: [ + Text( + "Transaction fee", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), - ), - Builder( - builder: (context) { - final coin = ref.read(pWalletCoin(walletId)); - final fee = widget.txData.fee!; - final amount = widget.txData.amountWithoutChange!; - final total = amount + fee; - - return Text( - ref.watch(pAmountFormatter(coin)).format(total), - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, + ], + ), + const SizedBox(height: 10), + RoundedContainer( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + ref + .watch( + pAmountFormatter( + ref.watch(pWalletCoin(walletId)), + ), + ) + .format(widget.txData.fee!), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), - textAlign: TextAlign.right, - ); - }, - ), - ], - ), - ), - const SizedBox( - height: 16, - ), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: Navigator.of(context).pop, + ), + ], ), ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Send", - buttonHeight: isDesktop ? ButtonHeight.l : null, - onPressed: _confirmSend, + const SizedBox(height: 16), + RoundedContainer( + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total amount", + style: STextStyles.titleBold12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + ), + Builder( + builder: (context) { + final coin = ref.read(pWalletCoin(walletId)); + final fee = widget.txData.fee!; + final amount = + widget.txData.amountWithoutChange!; + final total = amount + fee; + + return Text( + ref + .watch(pAmountFormatter(coin)) + .format(total), + style: STextStyles.itemSubtitle12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ); + }, + ), + ], ), ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: _confirmSend, + ), + ), + ], + ), ], ), - ], - ), + ), + ], ), - ], - ), - ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ConditionalParent( condition: isDesktop, - builder: (child) => Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.background, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + builder: + (child) => Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.background, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row(children: [child]), ), ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - child, - ], - ), - ), - ), child: Text( "Send ${ref.watch(pWalletCoin(walletId)).ticker}", - style: isDesktop - ? STextStyles.desktopTextMedium(context) - : STextStyles.pageTitleH1(context), + style: + isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.pageTitleH1(context), ), ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Send from", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Send from", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), Text( ref.watch(pWalletName(walletId)), style: STextStyles.itemSubtitle12(context), @@ -526,13 +524,10 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -541,9 +536,7 @@ class _ConfirmChangeNowSendViewState "${trade.exchangeName} address", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle12(context), @@ -553,68 +546,72 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), + Text("Amount", style: STextStyles.smallMed12(context)), ConditionalParent( condition: isDesktop, - builder: (child) => Row( - children: [ - child, - Builder( - builder: (context) { - final coin = ref.watch(pWalletCoin(walletId)); - final price = ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin)), - ); - final amountWithoutChange = - widget.txData.amountWithoutChange!; - final value = - (price.item1 * amountWithoutChange.decimal) - .toAmount(fractionDigits: 2); - final currency = ref.watch( - prefsChangeNotifierProvider - .select((value) => value.currency), - ); - final locale = ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), - ); - - return Text( - " | ${value.fiatString(locale: locale)} $currency", - style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - ); - }, + builder: + (child) => Row( + children: [ + child, + Builder( + builder: (context) { + final coin = ref.watch(pWalletCoin(walletId)); + final price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ); + final String extra; + if (price == null) { + extra = ""; + } else { + final amountWithoutChange = + widget.txData.amountWithoutChange!; + final value = (price.value * + amountWithoutChange.decimal) + .toAmount(fractionDigits: 2); + final currency = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + ); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); + + extra = + " | ${value.fiatString(locale: locale)} $currency"; + } + + return Text( + extra, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle2, + ), + ); + }, + ), + ], ), - ], - ), child: Text( ref .watch( - pAmountFormatter( - ref.watch(pWalletCoin(walletId)), - ), + pAmountFormatter(ref.watch(pWalletCoin(walletId))), ) .format((widget.txData.amountWithoutChange!)), style: STextStyles.itemSubtitle12(context), @@ -626,13 +623,10 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -646,9 +640,7 @@ class _ConfirmChangeNowSendViewState .watch( pAmountFormatter(ref.read(pWalletCoin(walletId))), ) - .format( - widget.txData.fee!, - ), + .format(widget.txData.fee!), style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, ), @@ -657,24 +649,16 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Note", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), Text( widget.txData.note ?? "", style: STextStyles.itemSubtitle12(context), @@ -684,21 +668,15 @@ class _ConfirmChangeNowSendViewState ), isDesktop ? Container( - color: - Theme.of(context).extension()!.background, - height: 1, - ) - : const SizedBox( - height: 12, - ), + color: Theme.of(context).extension()!.background, + height: 1, + ) + : const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Trade ID", - style: STextStyles.smallMed12(context), - ), + Text("Trade ID", style: STextStyles.smallMed12(context)), Text( trade.tradeId, style: STextStyles.itemSubtitle12(context), @@ -707,24 +685,23 @@ class _ConfirmChangeNowSendViewState ], ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop) RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackSuccess, + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Total amount", style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, + color: + Theme.of( + context, + ).extension()!.textConfirmTotalAmount, ), ), Builder( @@ -737,9 +714,10 @@ class _ConfirmChangeNowSendViewState return Text( ref.watch(pAmountFormatter(coin)).format(total), style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, ), textAlign: TextAlign.right, ); @@ -748,10 +726,7 @@ class _ConfirmChangeNowSendViewState ], ), ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), if (!isDesktop) const Spacer(), if (!isDesktop) PrimaryButton( diff --git a/lib/pages/exchange_view/edit_trade_note_view.dart b/lib/pages/exchange_view/edit_trade_note_view.dart index db918be65..1406d87e3 100644 --- a/lib/pages/exchange_view/edit_trade_note_view.dart +++ b/lib/pages/exchange_view/edit_trade_note_view.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../providers/exchange/trade_note_service_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/constants.dart'; @@ -79,86 +80,94 @@ class _EditNoteViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _noteController, - style: STextStyles.field(context), - focusNode: noteFieldFocusNode, - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Note", - noteFieldFocusNode, - 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 = ""; - }); - }, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: _noteController, + style: STextStyles.field(context), + focusNode: noteFieldFocusNode, + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Note", + noteFieldFocusNode, + 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, + ), + ) + : null, + ), ), ), - ), - const Spacer(), - TextButton( - onPressed: () async { - await ref.read(tradeNoteServiceProvider).set( - tradeId: widget.tradeId, - note: _noteController.text, - ); - if (mounted) { - Navigator.of(context).pop(); - } - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Save", - style: STextStyles.button(context), + const Spacer(), + TextButton( + onPressed: () async { + await ref + .read(tradeNoteServiceProvider) + .set( + tradeId: widget.tradeId, + note: _noteController.text, + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Save", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart index e3ae98acd..edfe6ba19 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart @@ -13,33 +13,29 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; +import 'package:tuple/tuple.dart'; import '../../../app_config.dart'; -import '../../../exceptions/exchange/unsupported_currency_exception.dart'; +import '../../../models/exchange/aggregate_currency.dart'; import '../../../models/isar/exchange_cache/currency.dart'; import '../../../models/isar/exchange_cache/pair.dart'; -import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/exchange_data_loading_service.dart'; -import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; -import '../../../services/exchange/nanswap/nanswap_exchange.dart'; -import '../../../services/exchange/trocador/trocador_exchange.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/constants.dart'; +import '../../../utilities/logger.dart'; import '../../../utilities/prefs.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../widgets/background.dart'; +import '../../../widgets/coin_ticker_tag.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_loading_overlay.dart'; -import '../../../widgets/desktop/primary_button.dart'; -import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/icon_widgets/x_icon.dart'; import '../../../widgets/loading_indicator.dart'; import '../../../widgets/rounded_white_container.dart'; -import '../../../widgets/stack_dialog.dart'; import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; import '../../buy_view/sub_widgets/crypto_selection_view.dart'; @@ -47,14 +43,12 @@ import '../../buy_view/sub_widgets/crypto_selection_view.dart'; class ExchangeCurrencySelectionView extends StatefulWidget { const ExchangeCurrencySelectionView({ super.key, - required this.willChangeTicker, - required this.pairedTicker, + required this.pairedCurrency, required this.isFixedRate, required this.willChangeIsSend, }); - final String? willChangeTicker; - final String? pairedTicker; + final AggregateCurrency? pairedCurrency; final bool isFixedRate; final bool willChangeIsSend; @@ -69,31 +63,29 @@ class _ExchangeCurrencySelectionViewState final _searchFocusNode = FocusNode(); final isDesktop = Util.isDesktop; - List _currencies = []; + List _currencies = []; bool _loaded = false; String _searchString = ""; - Future _showUpdatingCurrencies({ - required Future whileFuture, - }) async { + Future _showUpdatingCurrencies({required Future whileFuture}) async { unawaited( showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Loading currencies", - eventBus: null, + builder: + (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Loading currencies", + eventBus: null, + ), + ), ), - ), - ), ), ); @@ -106,141 +98,115 @@ class _ExchangeCurrencySelectionViewState return result; } - Future> _loadCurrencies() async { - if (widget.pairedTicker == null) { - return await _getCurrencies(); - } + Future> _loadCurrencies() async { await ExchangeDataLoadingService.instance.initDB(); - final List currencies = await ExchangeDataLoadingService - .instance.isar.currencies - .where() - .filter() - .exchangeNameEqualTo(MajesticBankExchange.exchangeName) - .or() - .exchangeNameStartsWith(TrocadorExchange.exchangeName) - .or() - .exchangeNameStartsWith(NanswapExchange.exchangeName) - .findAll(); - - final cn = await ChangeNowExchange.instance.getPairedCurrencies( - widget.pairedTicker!, - widget.isFixedRate, - ); - - if (cn.value == null) { - if (cn.exception is UnsupportedCurrencyException) { - return _getDistinctCurrenciesFrom(currencies); - } - - if (mounted) { - await showDialog( - context: context, - builder: (context) => StackDialog( - title: "Exchange Error", - message: "Failed to load currency data: ${cn.exception}", - leftButton: SecondaryButton( - label: "Ok", - onPressed: Navigator.of(context, rootNavigator: isDesktop).pop, - ), - rightButton: PrimaryButton( - label: "Retry", - onPressed: () async { - Navigator.of(context, rootNavigator: isDesktop).pop(); - _currencies = await _showUpdatingCurrencies( - whileFuture: _loadCurrencies(), - ); - setState(() {}); - }, - ), - ), - ); - } - } else { - currencies.addAll(cn.value!); - } - - return _getDistinctCurrenciesFrom(currencies); - } - - Future> _getCurrencies() async { - await ExchangeDataLoadingService.instance.initDB(); - final currencies = await ExchangeDataLoadingService.instance.isar.currencies - .where() - .filter() - .isFiatEqualTo(false) - .and() - .group( - (q) => widget.isFixedRate - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .sortByIsStackCoin() - .thenByName() - .findAll(); + final isar = await ExchangeDataLoadingService.instance.isar; + final currencies = + await isar.currencies + .where() + .filter() + .isFiatEqualTo(false) + .and() + .group( + (q) => + widget.isFixedRate + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .sortByIsStackCoin() + .thenByName() + .findAll(); // If using Tor, filter exchanges which do not support Tor. if (Prefs.instance.useTor) { if (Exchange.exchangeNamesWithTorSupport.isNotEmpty) { currencies.removeWhere( - (element) => !Exchange.exchangeNamesWithTorSupport - .contains(element.exchangeName), + (element) => + !Exchange.exchangeNamesWithTorSupport.contains( + element.exchangeName, + ), ); } } - return _getDistinctCurrenciesFrom(currencies); + return await _getDistinctCurrenciesFrom(currencies); } - List _getDistinctCurrenciesFrom(List currencies) { - final List distinctCurrencies = []; + Future> _getDistinctCurrenciesFrom( + List currencies, + ) async { + final Map> groups = {}; + for (final currency in currencies) { - if (!distinctCurrencies.any( - (e) => e.ticker.toLowerCase() == currency.ticker.toLowerCase(), - )) { - distinctCurrencies.add(currency); - } + final key = '${currency.ticker.toLowerCase()}|${currency.getFuzzyNet()}'; + + groups.putIfAbsent(key, () => []).add(currency); } - return distinctCurrencies; - } - List filter(String text) { - if (widget.pairedTicker == null) { - if (text.isEmpty) { - return _currencies; - } + final Set results = {}; - return _currencies - .where( - (e) => - e.name.toLowerCase().contains(text.toLowerCase()) || - e.ticker.toLowerCase().contains(text.toLowerCase()), - ) - .toList(); - } else { - if (text.isEmpty) { - return _currencies + for (final group in groups.values) { + final items = group + .map((e) => Tuple2(e.exchangeName, e)) + .toList(growable: false); + + results.add(AggregateCurrency(exchangeCurrencyPairs: items)); + } + + if (widget.pairedCurrency != null) { + results.remove(widget.pairedCurrency); + } + + final walletCoins = + results .where( - (e) => - e.ticker.toLowerCase() != widget.pairedTicker!.toLowerCase(), + (currency) => + AppConfig.coins + .where( + (coin) => + coin.ticker.toLowerCase() == + currency.ticker.toLowerCase() && + currency.fuzzyNet == coin.ticker.toLowerCase(), + ) + .isNotEmpty, ) .toList(); - } - return _currencies - .where( - (e) => - e.ticker.toLowerCase() != widget.pairedTicker!.toLowerCase() && - (e.name.toLowerCase().contains(text.toLowerCase()) || - e.ticker.toLowerCase().contains(text.toLowerCase())), - ) - .toList(); + final list = results.toList(); + + // sort alphabetically by name + list.sort((a, b) => a.name.compareTo(b.name)); + + // reverse sort walletCoins to prepare for next step + walletCoins.sort((a, b) => b.name.compareTo(a.name)); + + // insert wallet coins at beginning + for (final c in walletCoins) { + list.remove(c); + list.insert(0, c); } + + return list; + } + + List filter(String text) { + if (text.isEmpty) { + return _currencies.toList(); + } + + return _currencies + .where( + (e) => + e.name.toLowerCase().contains(text.toLowerCase()) || + e.ticker.toLowerCase().contains(text.toLowerCase()), + ) + .toList(); } @override @@ -262,8 +228,9 @@ class _ExchangeCurrencySelectionViewState if (!_loaded) { _loaded = true; WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - _currencies = - await _showUpdatingCurrencies(whileFuture: _loadCurrencies()); + _currencies = await _showUpdatingCurrencies( + whileFuture: _loadCurrencies(), + ); setState(() {}); }); } @@ -284,7 +251,7 @@ class _ExchangeCurrencySelectionViewState const Duration(milliseconds: 50), ); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -295,197 +262,200 @@ class _ExchangeCurrencySelectionViewState ), ), body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(horizontal: 16), child: child, ), ), ); }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, - children: [ - if (!isDesktop) - const SizedBox( - height: 16, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autofocus: isDesktop, - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (value) => setState(() => _searchString = value), - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) const SizedBox(height: 16), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (value) => setState(() => _searchString = value), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; - }); - }, + 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, + ), + ) + : null, + ), ), ), - ), - const SizedBox( - height: 20, - ), - Flexible( - child: Builder( - builder: (context) { - final coins = AppConfig.coins.where( - (e) => - e.ticker.toLowerCase() != - widget.pairedTicker?.toLowerCase(), - ); - - final items = filter(_searchString); - - final walletCoins = items - .where( - (currency) => coins - .where( - (coin) => - coin.ticker.toLowerCase() == - currency.ticker.toLowerCase(), - ) - .isNotEmpty, - ) - .toList(); - - // sort alphabetically by name - items.sort((a, b) => a.name.compareTo(b.name)); - - // reverse sort walletCoins to prepare for next step - walletCoins.sort((a, b) => b.name.compareTo(a.name)); - - // insert wallet coins at beginning - for (final c in walletCoins) { - items.remove(c); - items.insert(0, c); - } - - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: ListView.builder( - shrinkWrap: true, - primary: isDesktop ? false : null, - itemCount: items.length, - itemBuilder: (builderContext, index) { - final bool hasImageUrl = - items[index].image.startsWith("http"); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(items[index]); - }, - child: RoundedWhiteContainer( - child: Row( - children: [ - SizedBox( - width: 24, - height: 24, - child: - AppConfig.isStackCoin(items[index].ticker) - ? CoinIconForTicker( + const SizedBox(height: 20), + Flexible( + child: Builder( + builder: (context) { + final items = filter(_searchString); + + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: items.length, + itemBuilder: (builderContext, index) { + final image = items[index].image; + final hasImageUrl = image.startsWith("http"); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + final selected = items[index]; + Logging.instance.d("swap selected: $selected"); + Navigator.of(context).pop(selected); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: + AppConfig.isStackCoin( + items[index].ticker, + ) + ? CoinIconForTicker( ticker: items[index].ticker, size: 24, ) - // ? getIconForTicker( - // items[index].ticker, - // size: 24, - // ) - : hasImageUrl - ? SvgPicture.network( - items[index].image, - width: 24, - height: 24, - placeholderBuilder: (_) => - const LoadingIndicator(), - ) - : const SizedBox( - width: 24, - height: 24, + : hasImageUrl + ? _NetImage( + url: image, + key: ValueKey( + image + items[index].fuzzyNet, + ), + ) + : const SizedBox( + width: 24, + height: 24, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + items[index].name, + style: STextStyles.largeMedium14( + context, + ), + ), + if (items[index].ticker + .toLowerCase() != + items[index].fuzzyNet + .toLowerCase()) + Padding( + padding: const EdgeInsets.only( + left: 12, ), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - items[index].name, - style: - STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, - ), - Text( - items[index].ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + child: CoinTickerTag( + ticker: + items[index].fuzzyNet + .toUpperCase(), + ), + ), + ], + ), + const SizedBox(height: 2), + Text( + items[index].ticker.toUpperCase(), + style: STextStyles.smallMed12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle1, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, - ), - ); - }, + ); + }, + ), + ); + }, + ), ), - ), - ], + ], + ), ), ); } } + +class _NetImage extends StatelessWidget { + const _NetImage({super.key, required this.url}); + + final String url; + + @override + Widget build(BuildContext context) { + if (url.endsWith(".svg")) { + return SvgPicture.network( + key: key, + url, + width: 24, + height: 24, + placeholderBuilder: (_) => const LoadingIndicator(), + ); + } else { + return Image.network(url, width: 24, height: 24, key: key); + } + } +} diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 111d38448..675cd30cd 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -15,7 +15,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; @@ -24,7 +23,6 @@ import '../../models/exchange/incomplete_exchange.dart'; import '../../models/exchange/response_objects/estimate.dart'; import '../../models/exchange/response_objects/range.dart'; import '../../models/isar/exchange_cache/currency.dart'; -import '../../models/isar/exchange_cache/pair.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; import '../../providers/providers.dart'; @@ -32,7 +30,6 @@ import '../../services/exchange/change_now/change_now_exchange.dart'; import '../../services/exchange/exchange.dart'; import '../../services/exchange/exchange_data_loading_service.dart'; import '../../services/exchange/exchange_response.dart'; -import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; import '../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../services/exchange/trocador/trocador_exchange.dart'; import '../../themes/stack_colors.dart'; @@ -40,6 +37,7 @@ import '../../utilities/amount/amount_unit.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/exchange_rate_type_enum.dart'; +import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; @@ -49,6 +47,7 @@ import '../../widgets/desktop/desktop_dialog.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/desktop/primary_button.dart'; import '../../widgets/desktop/secondary_button.dart'; +import '../../widgets/dialogs/basic_dialog.dart'; import '../../widgets/rounded_container.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; @@ -60,12 +59,7 @@ import 'sub_widgets/exchange_provider_options.dart'; import 'sub_widgets/rate_type_toggle.dart'; class ExchangeForm extends ConsumerStatefulWidget { - const ExchangeForm({ - super.key, - this.walletId, - this.coin, - this.contract, - }); + const ExchangeForm({super.key, this.walletId, this.coin, this.contract}); final String? walletId; final CryptoCurrency? coin; @@ -85,7 +79,6 @@ class _ExchangeFormState extends ConsumerState { return Exchange.exchangesWithTorSupport; } else { return [ - MajesticBankExchange.instance, ChangeNowExchange.instance, TrocadorExchange.instance, NanswapExchange.instance, @@ -111,19 +104,19 @@ class _ExchangeFormState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Updating exchange rate", - eventBus: null, + builder: + (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Updating exchange rate", + eventBus: null, + ), + ), ), - ), - ), ), ); @@ -183,39 +176,6 @@ class _ExchangeFormState extends ConsumerState { ?.decimal; } - Future _getAggregateCurrency(Currency currency) async { - final rateType = ref.read(efRateTypeProvider); - final currencies = await ExchangeDataLoadingService.instance.isar.currencies - .filter() - .group( - (q) => rateType == ExchangeRateType.fixed - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .and() - .tickerEqualTo( - currency.ticker, - caseSensitive: false, - ) - .and() - .tokenContractEqualTo(currency.tokenContract) - .findAll(); - - final items = [Tuple2(currency.exchangeName, currency)]; - - for (final currency in currencies) { - items.add(Tuple2(currency.exchangeName, currency)); - } - - return AggregateCurrency(exchangeCurrencyPairs: items); - } - void selectSendCurrency() async { final type = ref.read(efRateTypeProvider); final fromTicker = ref.read(efCurrencyPairProvider).send?.ticker ?? ""; @@ -233,21 +193,15 @@ class _ExchangeFormState extends ConsumerState { } final selectedCurrency = await _showCurrencySelectionSheet( - willChange: ref.read(efCurrencyPairProvider).send?.ticker, willChangeIsSend: true, - paired: ref.read(efCurrencyPairProvider).receive?.ticker, + paired: ref.read(efCurrencyPairProvider).receive, isFixedRate: type == ExchangeRateType.fixed, ); if (selectedCurrency != null) { - await showUpdatingExchangeRate( - whileFuture: _getAggregateCurrency(selectedCurrency).then( - (aggregateSelected) => ref.read(efCurrencyPairProvider).setSend( - aggregateSelected, - notifyListeners: true, - ), - ), - ); + ref + .read(efCurrencyPairProvider) + .setSend(selectedCurrency, notifyListeners: true); } } @@ -260,21 +214,15 @@ class _ExchangeFormState extends ConsumerState { } final selectedCurrency = await _showCurrencySelectionSheet( - willChange: ref.read(efCurrencyPairProvider).receive?.ticker, willChangeIsSend: false, - paired: ref.read(efCurrencyPairProvider).send?.ticker, + paired: ref.read(efCurrencyPairProvider).send, isFixedRate: ref.read(efRateTypeProvider) == ExchangeRateType.fixed, ); if (selectedCurrency != null) { - await showUpdatingExchangeRate( - whileFuture: _getAggregateCurrency(selectedCurrency).then( - (aggregateSelected) => ref.read(efCurrencyPairProvider).setReceive( - aggregateSelected, - notifyListeners: true, - ), - ), - ); + ref + .read(efCurrencyPairProvider) + .setReceive(selectedCurrency, notifyListeners: true); } } @@ -284,20 +232,20 @@ class _ExchangeFormState extends ConsumerState { _receiveFocusNode.unfocus(); final temp = ref.read(efCurrencyPairProvider).send; - ref.read(efCurrencyPairProvider).setSend( + ref + .read(efCurrencyPairProvider) + .setSend( ref.read(efCurrencyPairProvider).receive, notifyListeners: true, ); - ref.read(efCurrencyPairProvider).setReceive( - temp, - notifyListeners: true, - ); + ref.read(efCurrencyPairProvider).setReceive(temp, notifyListeners: true); // final reversed = ref.read(efReversedProvider); final amount = ref.read(efSendAmountProvider); - ref.read(efSendAmountProvider.notifier).state = - ref.read(efReceiveAmountProvider); + ref.read(efSendAmountProvider.notifier).state = ref.read( + efReceiveAmountProvider, + ); ref.read(efReceiveAmountProvider.notifier).state = amount; @@ -306,83 +254,81 @@ class _ExchangeFormState extends ConsumerState { _swapLock = false; } - Future _showCurrencySelectionSheet({ - required String? willChange, - required String? paired, + Future _showCurrencySelectionSheet({ + required AggregateCurrency? paired, required bool isFixedRate, required bool willChangeIsSend, }) async { _sendFocusNode.unfocus(); _receiveFocusNode.unfocus(); - final result = isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( + final result = + isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Choose a coin to exchange", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: Theme.of(context) - .extension()! - .background, - child: ExchangeCurrencySelectionView( - willChangeTicker: willChange, - pairedTicker: paired, - isFixedRate: isFixedRate, - willChangeIsSend: willChangeIsSend, + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: ExchangeCurrencySelectionView( + pairedCurrency: paired, + isFixedRate: isFixedRate, + willChangeIsSend: willChangeIsSend, + ), ), ), - ), - ], + ], + ), ), ), + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: + (_) => ExchangeCurrencySelectionView( + pairedCurrency: paired, + isFixedRate: isFixedRate, + willChangeIsSend: willChangeIsSend, ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ExchangeCurrencySelectionView( - willChangeTicker: willChange, - pairedTicker: paired, - isFixedRate: isFixedRate, - willChangeIsSend: willChangeIsSend, ), - ), - ); + ); - if (mounted && result is Currency) { + if (mounted && result is AggregateCurrency) { return result; } else { return null; @@ -397,22 +343,97 @@ class _ExchangeFormState extends ConsumerState { update(); } + Future _checkTrocadorWarning(Currency from, Currency to) async { + final Set warnings = {}; + + final firoWarning = + TrocadorExchange.checkFiro(from) ?? TrocadorExchange.checkFiro(to); + final ltcWarning = + TrocadorExchange.checkLtc(from) ?? TrocadorExchange.checkLtc(to); + + if (firoWarning != null) warnings.add(firoWarning); + if (ltcWarning != null) warnings.add(ltcWarning); + + if (warnings.isNotEmpty) { + final title = warnings.map((e) => e.message).join(" and "); + final message = warnings.map((e) => e.messageDetail).join(" "); + + final result = await showDialog( + context: context, + builder: (context) { + return BasicDialog( + title: title, + message: message, + canPopWithBackButton: true, + flex: true, + desktopHeight: 400, + leftButton: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () => Navigator.of(context).pop(false), + ), + rightButton: PrimaryButton( + label: "Continue", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () => Navigator.of(context).pop(true), + ), + ); + }, + ); + + return result == true; + } else { + return true; + } + } + void onExchangePressed() async { + final exchangeName = ref.read(efExchangeProvider).name; + + final fromCurrency = ref + .read(efCurrencyPairProvider) + .send + ?.forExchange(exchangeName); + final toCurrency = ref + .read(efCurrencyPairProvider) + .receive + ?.forExchange(exchangeName); + + if (fromCurrency == null || toCurrency == null) { + await showDialog( + context: context, + builder: + (context) => const StackOkDialog( + title: "Missing currency!", + message: "This should not happen. Please contact support", + ), + ); + + return; + } + + if (exchangeName == TrocadorExchange.exchangeName) { + final canContinue = await _checkTrocadorWarning(fromCurrency, toCurrency); + if (!canContinue) return; + } + final rateType = ref.read(efRateTypeProvider); - final fromTicker = ref.read(efCurrencyPairProvider).send?.ticker ?? ""; - final toTicker = ref.read(efCurrencyPairProvider).receive?.ticker ?? ""; final estimate = ref.read(efEstimateProvider)!; final sendAmount = ref.read(efSendAmountProvider)!; - if (rateType == ExchangeRateType.fixed && toTicker.toUpperCase() == "WOW") { - await showDialog( - context: context, - builder: (context) => const StackOkDialog( - title: "WOW error", - message: - "Wownero is temporarily disabled as a receiving currency for fixed rate trades due to network issues", - ), - ); + if (rateType == ExchangeRateType.fixed && + toCurrency.ticker.toUpperCase() == "WOW") { + if (mounted) { + await showDialog( + context: context, + builder: + (context) => const StackOkDialog( + title: "WOW error", + message: + "Wownero is temporarily disabled as a receiving currency for fixed rate trades due to network issues", + ), + ); + } return; } @@ -421,14 +442,15 @@ class _ExchangeFormState extends ConsumerState { final amountToSend = estimate.reversed ? estimate.estimatedAmount : sendAmount; - final amountToReceive = estimate.reversed - ? ref.read(efReceiveAmountProvider)! - : estimate.estimatedAmount; + final amountToReceive = + estimate.reversed + ? ref.read(efReceiveAmountProvider)! + : estimate.estimatedAmount; switch (rateType) { case ExchangeRateType.estimated: rate = - "1 ${fromTicker.toUpperCase()} ~${(amountToReceive / sendAmount).toDecimal(scaleOnInfinitePrecision: 8).toStringAsFixed(8)} ${toTicker.toUpperCase()}"; + "1 ${fromCurrency.ticker.toUpperCase()} ~${(amountToReceive / sendAmount).toDecimal(scaleOnInfinitePrecision: 8).toStringAsFixed(8)} ${toCurrency.ticker.toUpperCase()}"; break; case ExchangeRateType.fixed: bool? shouldCancel; @@ -466,32 +488,30 @@ class _ExchangeFormState extends ConsumerState { "Do you want to attempt trade anyways?", style: STextStyles.desktopTextSmall(context), ), - const Spacer( - flex: 2, - ), + const Spacer(flex: 2), Row( children: [ Expanded( child: SecondaryButton( label: "Cancel", buttonHeight: ButtonHeight.l, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(true), + onPressed: + () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Attempt", buttonHeight: ButtonHeight.l, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(false), + onPressed: + () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), ), ), ], @@ -521,10 +541,7 @@ class _ExchangeFormState extends ConsumerState { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - child: Text( - "Attempt", - style: STextStyles.button(context), - ), + child: Text("Attempt", style: STextStyles.button(context)), onPressed: () { // continue and try to attempt trade Navigator.of(context).pop(false); @@ -540,15 +557,13 @@ class _ExchangeFormState extends ConsumerState { return; } rate = - "1 ${fromTicker.toUpperCase()} ~${(amountToReceive / amountToSend).toDecimal( - scaleOnInfinitePrecision: 12, - ).toStringAsFixed(8)} ${toTicker.toUpperCase()}"; + "1 ${fromCurrency.ticker.toUpperCase()} ~${(amountToReceive / amountToSend).toDecimal(scaleOnInfinitePrecision: 12).toStringAsFixed(8)} ${toCurrency.ticker.toUpperCase()}"; break; } final model = IncompleteExchangeModel( - sendTicker: fromTicker.toUpperCase(), - receiveTicker: toTicker.toUpperCase(), + sendCurrency: fromCurrency, + receiveCurrency: toCurrency, rateInfo: rate, sendAmount: amountToSend, receiveAmount: amountToReceive, @@ -560,8 +575,10 @@ class _ExchangeFormState extends ConsumerState { if (mounted) { if (walletInitiated) { - ref.read(exchangeSendFromWalletIdStateProvider.state).state = - Tuple2(walletId!, coin!); + ref.read(exchangeSendFromWalletIdStateProvider.state).state = Tuple2( + walletId!, + coin!, + ); if (isDesktop) { ref.read(ssss.state).state = model; await showDialog( @@ -571,18 +588,15 @@ class _ExchangeFormState extends ConsumerState { return const DesktopDialog( maxWidth: 720, maxHeight: double.infinity, - child: StepScaffold( - initialStep: 2, - ), + child: StepScaffold(initialStep: 2), ); }, ); } else { unawaited( - Navigator.of(context).pushNamed( - Step2View.routeName, - arguments: model, - ), + Navigator.of( + context, + ).pushNamed(Step2View.routeName, arguments: model), ); } } else { @@ -597,18 +611,15 @@ class _ExchangeFormState extends ConsumerState { return const DesktopDialog( maxWidth: 720, maxHeight: double.infinity, - child: StepScaffold( - initialStep: 1, - ), + child: StepScaffold(initialStep: 1), ); }, ); } else { unawaited( - Navigator.of(context).pushNamed( - Step1View.routeName, - arguments: model, - ), + Navigator.of( + context, + ).pushNamed(Step1View.routeName, arguments: model), ); } } @@ -620,9 +631,10 @@ class _ExchangeFormState extends ConsumerState { return false; } - final String? ticker = isSend - ? ref.read(efCurrencyPairProvider).send?.ticker - : ref.read(efCurrencyPairProvider).receive?.ticker; + final String? ticker = + isSend + ? ref.read(efCurrencyPairProvider).send?.ticker + : ref.read(efCurrencyPairProvider).receive?.ticker; if (ticker == null) { return false; @@ -640,9 +652,10 @@ class _ExchangeFormState extends ConsumerState { } final reversed = ref.read(efReversedProvider); - final amount = reversed - ? ref.read(efReceiveAmountProvider) - : ref.read(efSendAmountProvider); + final amount = + reversed + ? ref.read(efReceiveAmountProvider) + : ref.read(efSendAmountProvider); final pair = ref.read(efCurrencyPairProvider); if (amount == null || @@ -654,7 +667,7 @@ class _ExchangeFormState extends ConsumerState { } final rateType = ref.read(efRateTypeProvider); final Map>, Range?>> - results = {}; + results = {}; for (final exchange in usableExchanges) { final sendCurrency = pair.send?.forExchange(exchange.name); @@ -663,26 +676,29 @@ class _ExchangeFormState extends ConsumerState { if (sendCurrency != null && receiveCurrency != null) { final rangeResponse = await exchange.getRange( reversed ? receiveCurrency.ticker : sendCurrency.ticker, + reversed ? receiveCurrency.network : sendCurrency.network, reversed ? sendCurrency.ticker : receiveCurrency.ticker, + reversed ? sendCurrency.network : receiveCurrency.network, rateType == ExchangeRateType.fixed, ); + Logging.instance.d( + "${exchange.name}: fixedRate=$rateType, RANGE=$rangeResponse", + ); + final estimateResponse = await exchange.getEstimates( sendCurrency.ticker, + sendCurrency.network, receiveCurrency.ticker, + receiveCurrency.network, amount, rateType == ExchangeRateType.fixed, reversed, ); - results.addAll( - { - exchange.name: Tuple2( - estimateResponse, - rangeResponse.value, - ), - }, - ); + results.addAll({ + exchange.name: Tuple2(estimateResponse, rangeResponse.value), + }); } } @@ -743,8 +759,7 @@ class _ExchangeFormState extends ConsumerState { } }); _receiveFocusNode.addListener(() { - if (_receiveFocusNode.hasFocus && - ref.read(efExchangeProvider).name != ChangeNowExchange.exchangeName) { + if (_receiveFocusNode.hasFocus) { final reversed = ref.read(efReversedProvider); WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(efReversedProvider.notifier).state = true; @@ -767,18 +782,18 @@ class _ExchangeFormState extends ConsumerState { .setReceive(null, notifyListeners: true); ExchangeDataLoadingService.instance .getAggregateCurrency( - widget.contract == null ? coin!.ticker : widget.contract!.symbol, - ExchangeRateType.estimated, - widget.contract == null ? null : widget.contract!.address, - ) + widget.contract == null ? coin!.ticker : widget.contract!.symbol, + coin!.ticker.toLowerCase(), + ExchangeRateType.estimated, + widget.contract?.address, + ) .then((value) { - if (value != null) { - ref.read(efCurrencyPairProvider).setSend( - value, - notifyListeners: true, - ); - } - }); + if (value != null) { + ref + .read(efCurrencyPairProvider) + .setSend(value, notifyListeners: true); + } + }); }); } else { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { @@ -816,9 +831,7 @@ class _ExchangeFormState extends ConsumerState { if (_sendFocusNode.hasFocus) { _sendController.selection = TextSelection.fromPosition( - TextPosition( - offset: _sendController.text.length, - ), + TextPosition(offset: _sendController.text.length), ); } } @@ -835,9 +848,7 @@ class _ExchangeFormState extends ConsumerState { if (_receiveFocusNode.hasFocus) { _receiveController.selection = TextSelection.fromPosition( - TextPosition( - offset: _receiveController.text.length, - ), + TextPosition(offset: _receiveController.text.length), ); } } @@ -868,13 +879,13 @@ class _ExchangeFormState extends ConsumerState { color: Theme.of(context).extension()!.textDark3, ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), ExchangeTextField( - key: Key("exchangeTextFieldKeyFor_" - "${Theme.of(context).extension()!.themeId}" - "${ref.watch(efCurrencyPairProvider.select((value) => value.send?.ticker))}"), + key: Key( + "exchangeTextFieldKeyFor_" + "${Theme.of(context).extension()!.themeId}" + "${ref.watch(efCurrencyPairProvider.select((value) => value.send?.ticker))}", + ), controller: _sendController, focusNode: _sendFocusNode, textStyle: STextStyles.smallMed14(context).copyWith( @@ -893,15 +904,12 @@ class _ExchangeFormState extends ConsumerState { onChanged: sendFieldOnChanged, onButtonTap: selectSendCurrency, isWalletCoin: isWalletCoin(coin, true), - currency: - ref.watch(efCurrencyPairProvider.select((value) => value.send)), - ), - SizedBox( - height: isDesktop ? 10 : 4, - ), - SizedBox( - height: isDesktop ? 10 : 4, + currency: ref.watch( + efCurrencyPairProvider.select((value) => value.send), + ), ), + SizedBox(height: isDesktop ? 10 : 4), + SizedBox(height: isDesktop ? 10 : 4), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -914,20 +922,23 @@ class _ExchangeFormState extends ConsumerState { ), ConditionalParent( condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), child: Semantics( label: "Swap Button. Reverse The Exchange Currencies.", excludeSemantics: true, child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(6) - : const EdgeInsets.all(2), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, + padding: + isDesktop + ? const EdgeInsets.all(6) + : const EdgeInsets.all(2), + color: + Theme.of( + context, + ).extension()!.buttonBackSecondary, radiusMultiplier: 0.75, child: GestureDetector( onTap: () async { @@ -939,9 +950,10 @@ class _ExchangeFormState extends ConsumerState { Assets.svg.swap, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -950,9 +962,7 @@ class _ExchangeFormState extends ConsumerState { ), ], ), - SizedBox( - height: isDesktop ? 10 : 7, - ), + SizedBox(height: isDesktop ? 10 : 7), ExchangeTextField( key: Key( "exchangeTextFieldKeyFor1_${Theme.of(context).extension()!.themeId}", @@ -967,52 +977,42 @@ class _ExchangeFormState extends ConsumerState { borderRadius: Constants.size.circularBorderRadius, background: Theme.of(context).extension()!.textFieldDefaultBG, - onTap: rateType == ExchangeRateType.estimated && - ref.watch(efExchangeProvider).name == - ChangeNowExchange.exchangeName - ? null - : () { - if (_sendController.text == "-") { - _sendController.text = ""; - } - }, + onTap: + rateType == ExchangeRateType.estimated + ? null + : () { + if (_sendController.text == "-") { + _sendController.text = ""; + } + }, onChanged: receiveFieldOnChanged, onButtonTap: selectReceiveCurrency, isWalletCoin: isWalletCoin(coin, true), - currency: ref - .watch(efCurrencyPairProvider.select((value) => value.receive)), - readOnly: rateType == ExchangeRateType.estimated && - ref.watch(efExchangeProvider).name == - ChangeNowExchange.exchangeName, - ), - SizedBox( - height: isDesktop ? 20 : 12, + currency: ref.watch( + efCurrencyPairProvider.select((value) => value.receive), + ), + readOnly: rateType == ExchangeRateType.estimated, ), + SizedBox(height: isDesktop ? 20 : 12), SizedBox( height: isDesktop ? 60 : 40, - child: RateTypeToggle( - key: UniqueKey(), - onChanged: onRateTypeChanged, - ), + child: RateTypeToggle(key: UniqueKey(), onChanged: onRateTypeChanged), ), AnimatedSize( duration: const Duration(milliseconds: 300), - child: ref.watch(efSendAmountProvider) == null && - ref.watch(efReceiveAmountProvider) == null - ? const SizedBox( - height: 0, - ) - : Padding( - padding: EdgeInsets.only(top: isDesktop ? 20 : 12), - child: ExchangeProviderOptions( - fixedRate: rateType == ExchangeRateType.fixed, - reversed: ref.watch(efReversedProvider), + child: + ref.watch(efSendAmountProvider) == null && + ref.watch(efReceiveAmountProvider) == null + ? const SizedBox(height: 0) + : Padding( + padding: EdgeInsets.only(top: isDesktop ? 20 : 12), + child: ExchangeProviderOptions( + fixedRate: rateType == ExchangeRateType.fixed, + reversed: ref.watch(efReversedProvider), + ), ), - ), - ), - SizedBox( - height: isDesktop ? 20 : 12, ), + SizedBox(height: isDesktop ? 20 : 12), PrimaryButton( buttonHeight: isDesktop ? ButtonHeight.l : null, enabled: ref.watch(efCanExchangeProvider), diff --git a/lib/pages/exchange_view/exchange_step_views/step_1_view.dart b/lib/pages/exchange_view/exchange_step_views/step_1_view.dart index 8e582dab9..96c168a8d 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_1_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_1_view.dart @@ -66,159 +66,157 @@ class _Step1ViewState extends State { } }, ), - title: Text( - "Swap", - style: STextStyles.navBarTitle(context), - ), + title: Text("Swap", style: STextStyles.navBarTitle(context)), ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 0, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Confirm amount", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Network fees and other exchange charges are included in the rate.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "You send", - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension()! - .infoItemText, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow(count: 4, current: 0, width: width), + const SizedBox(height: 14), + Text( + "Confirm amount", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox(height: 8), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox(height: 24), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "You send", + style: STextStyles.itemSubtitle( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .infoItemText, + ), ), - ), - Text( - "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .infoItemText, + Text( + "${model.sendAmount.toStringAsFixed(8)} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .infoItemText, + ), ), - ), - ], + ], + ), ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "You receive", - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension()! - .infoItemText, + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "You receive", + style: STextStyles.itemSubtitle( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .infoItemText, + ), ), - ), - Text( - "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .infoItemText, + Text( + "~${model.receiveAmount.toStringAsFixed(8)} ${model.receiveTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .infoItemText, + ), ), - ), - ], + ], + ), ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - model.rateType == ExchangeRateType.estimated - ? "Estimated rate" - : "Fixed rate", - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension()! - .infoItemLabel, + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + model.rateType == ExchangeRateType.estimated + ? "Estimated rate" + : "Fixed rate", + style: STextStyles.itemSubtitle( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .infoItemLabel, + ), ), - ), - Text( - model.rateInfo, - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .infoItemText, + Text( + model.rateInfo, + style: STextStyles.itemSubtitle12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .infoItemText, + ), ), - ), - ], + ], + ), ), - ), - const SizedBox( - height: 12, - ), - const Spacer(), - TextButton( - onPressed: () { - Navigator.of(context).pushNamed( - Step2View.routeName, - arguments: model, - ); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Next", - style: STextStyles.button(context), + const SizedBox(height: 12), + const Spacer(), + TextButton( + onPressed: () { + Navigator.of(context).pushNamed( + Step2View.routeName, + arguments: model, + ); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Next", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 951c211b9..a731bf9df 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -44,14 +44,12 @@ class Step2View extends ConsumerStatefulWidget { super.key, required this.model, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), }); static const String routeName = "/exchangeStep2"; final IncompleteExchangeModel model; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; @override ConsumerState createState() => _Step2ViewState(); @@ -60,7 +58,6 @@ class Step2View extends ConsumerStatefulWidget { class _Step2ViewState extends ConsumerState { late final IncompleteExchangeModel model; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late final TextEditingController _toController; late final TextEditingController _refundController; @@ -72,7 +69,7 @@ class _Step2ViewState extends ConsumerState { void _onRefundQrTapped() async { try { - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, @@ -85,7 +82,8 @@ class _Step2ViewState extends ConsumerState { model.refundAddress = _refundController.text; setState(() { - enableNext = _toController.text.isNotEmpty && + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; }); } else { @@ -93,22 +91,38 @@ class _Step2ViewState extends ConsumerState { model.refundAddress = _refundController.text; setState(() { - enableNext = _toController.text.isNotEmpty && + enableNext = + _toController.text.isNotEmpty && _refundController.text.isNotEmpty; }); } } on PlatformException catch (e, s) { - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code in $runtimeType: ", + error: e, + stackTrace: s, + ); + } } } void _onToQrTapped() async { try { - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, @@ -121,7 +135,8 @@ class _Step2ViewState extends ConsumerState { model.recipientAddress = _toController.text; setState(() { - enableNext = _toController.text.isNotEmpty && + enableNext = + _toController.text.isNotEmpty && (_refundController.text.isNotEmpty || !ref.read(efExchangeProvider).supportsRefundAddress); }); @@ -130,17 +145,33 @@ class _Step2ViewState extends ConsumerState { model.recipientAddress = _toController.text; setState(() { - enableNext = _toController.text.isNotEmpty && + enableNext = + _toController.text.isNotEmpty && (_refundController.text.isNotEmpty || !!ref.read(efExchangeProvider).supportsRefundAddress); }); } } on PlatformException catch (e, s) { - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code in $runtimeType: ", + error: e, + stackTrace: s, + ); + } } } @@ -148,7 +179,6 @@ class _Step2ViewState extends ConsumerState { void initState() { model = widget.model; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; _toController = TextEditingController(); _refundController = TextEditingController(); @@ -165,9 +195,9 @@ class _Step2ViewState extends ConsumerState { .getWallet(tuple.item1) .getCurrentReceivingAddress() .then((value) { - _toController.text = value!.value; - model.recipientAddress = _toController.text; - }); + _toController.text = value!.value; + model.recipientAddress = _toController.text; + }); } else { if (model.sendTicker.toUpperCase() == tuple.item2.ticker.toUpperCase()) { @@ -176,9 +206,9 @@ class _Step2ViewState extends ConsumerState { .getWallet(tuple.item1) .getCurrentReceivingAddress() .then((value) { - _refundController.text = value!.value; - model.refundAddress = _refundController.text; - }); + _refundController.text = value!.value; + model.refundAddress = _refundController.text; + }); } } } @@ -216,303 +246,45 @@ class _Step2ViewState extends ConsumerState { } }, ), - title: Text( - "Swap", - style: STextStyles.navBarTitle(context), - ), + title: Text("Swap", style: STextStyles.navBarTitle(context)), ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 1, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Exchange details", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Enter your recipient and refund addresses", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recipient Wallet", - style: STextStyles.smallMed12(context), - ), - if (AppConfig.isStackCoin(model.receiveTicker)) - CustomTextButton( - text: "Choose from ${AppConfig.prefix}", - onTap: () { - try { - final coin = AppConfig.coins.firstWhere( - (e) => - e.ticker.toLowerCase() == - model.receiveTicker.toLowerCase(), - ); - - Navigator.of(context) - .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) - .then((value) async { - if (value is String) { - final wallet = ref - .read(pWallets) - .getWallet(value); - - _toController.text = wallet.info.name; - model.recipientAddress = (await wallet - .getCurrentReceivingAddress()) - ?.value ?? - wallet - .info.cachedReceivingAddress; - - setState(() { - enableNext = - _toController.text.isNotEmpty && - (_refundController - .text.isNotEmpty || - !supportsRefund); - }); - } - }); - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - } - }, - ), - ], - ), - const SizedBox( - height: 4, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - onTap: () {}, - key: const Key( - "recipientExchangeStep2ViewAddressFieldKey", - ), - controller: _toController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: [ - // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, - ), - focusNode: _toFocusNode, - style: STextStyles.field(context), - onChanged: (value) { - model.recipientAddress = _toController.text; - setState(() { - enableNext = _toController.text.isNotEmpty && - (_refundController.text.isNotEmpty || - !supportsRefund); - }); - }, - decoration: standardInputDecoration( - "Enter the ${model.receiveTicker.toUpperCase()} payout address", - _toFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: _toController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _toController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _toController.text = ""; - model.recipientAddress = - _toController.text; - - setState(() { - enableNext = _toController - .text.isNotEmpty && - (_refundController.text - .isNotEmpty || - !supportsRefund); - }); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain, - ); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = - data.text!.trim(); - - _toController.text = - content; - model.recipientAddress = - _toController.text; - - setState(() { - enableNext = _toController - .text - .isNotEmpty && - (_refundController - .text - .isNotEmpty || - !supportsRefund); - }); - } - }, - child: - _toController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_toController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey", - ), - onTap: () { - ref - .read( - exchangeFlowIsActiveStateProvider - .state, - ) - .state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then((_) { - ref - .read( - exchangeFlowIsActiveStateProvider - .state, - ) - .state = false; - - final address = ref - .read( - exchangeFromAddressBookAddressStateProvider - .state, - ) - .state; - if (address.isNotEmpty) { - _toController.text = address; - model.recipientAddress = - _toController.text; - ref - .read( - exchangeFromAddressBookAddressStateProvider - .state, - ) - .state = ""; - } - setState(() { - enableNext = _toController - .text.isNotEmpty && - (_refundController.text - .isNotEmpty || - !supportsRefund); - }); - }); - }, - child: const AddressBookIcon(), - ), - if (_toController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey", - ), - onTap: _onToQrTapped, - child: const QrCodeIcon(), - ), - ], - ), - ), - ), - ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow(count: 4, current: 1, width: width), + const SizedBox(height: 14), + Text( + "Exchange details", + style: STextStyles.pageTitleH1(context), ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Text( - "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", - style: STextStyles.label(context), + const SizedBox(height: 8), + Text( + "Enter your recipient and refund addresses", + style: STextStyles.itemSubtitle(context), ), - ), - const SizedBox( - height: 24, - ), - if (supportsRefund) + const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Refund Wallet (required)", + "Recipient Wallet", style: STextStyles.smallMed12(context), ), - if (AppConfig.isStackCoin(model.sendTicker)) + if (AppConfig.isStackCoin(model.receiveTicker)) CustomTextButton( text: "Choose from ${AppConfig.prefix}", onTap: () { @@ -520,36 +292,45 @@ class _Step2ViewState extends ConsumerState { final coin = AppConfig.coins.firstWhere( (e) => e.ticker.toLowerCase() == - model.sendTicker.toLowerCase(), + model.receiveTicker.toLowerCase(), ); Navigator.of(context) .pushNamed( - ChooseFromStackView.routeName, - arguments: coin, - ) + ChooseFromStackView.routeName, + arguments: coin, + ) .then((value) async { - if (value is String) { - final wallet = ref - .read(pWallets) - .getWallet(value); - - _refundController.text = - wallet.info.name; - model.refundAddress = (await wallet - .getCurrentReceivingAddress())! - .value; - } - setState(() { - enableNext = - _toController.text.isNotEmpty && - _refundController - .text.isNotEmpty; - }); - }); + if (value is String) { + final wallet = ref + .read(pWallets) + .getWallet(value); + + _toController.text = + wallet.info.name; + model.recipientAddress = + (await wallet + .getCurrentReceivingAddress()) + ?.value ?? + wallet + .info + .cachedReceivingAddress; + + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + (_refundController + .text + .isNotEmpty || + !supportsRefund); + }); + } + }); } catch (e, s) { - Logging.instance.i( - "$e\n$s", + Logging.instance.e( + "", error: e, stackTrace: s, ); @@ -558,20 +339,17 @@ class _Step2ViewState extends ConsumerState { ), ], ), - if (supportsRefund) - const SizedBox( - height: 4, - ), - if (supportsRefund) + const SizedBox(height: 4), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), child: TextField( + onTap: () {}, key: const Key( - "refundExchangeStep2ViewAddressFieldKey", + "recipientExchangeStep2ViewAddressFieldKey", ), - controller: _refundController, + controller: _toController, readOnly: false, autocorrect: false, enableSuggestions: false, @@ -584,19 +362,20 @@ class _Step2ViewState extends ConsumerState { paste: true, selectAll: false, ), - focusNode: _refundFocusNode, + focusNode: _toFocusNode, style: STextStyles.field(context), onChanged: (value) { - model.refundAddress = _refundController.text; + model.recipientAddress = _toController.text; setState(() { enableNext = _toController.text.isNotEmpty && - _refundController.text.isNotEmpty; + (_refundController.text.isNotEmpty || + !supportsRefund); }); }, decoration: standardInputDecoration( - "Enter ${model.sendTicker.toUpperCase()} refund address", - _refundFocusNode, + "Enter the ${model.receiveTicker.toUpperCase()} payout address", + _toFocusNode, context, ).copyWith( contentPadding: const EdgeInsets.only( @@ -606,16 +385,272 @@ class _Step2ViewState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _refundController.text.isEmpty - ? const EdgeInsets.only(right: 16) - : const EdgeInsets.only(right: 0), + padding: + _toController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _refundController.text.isNotEmpty + _toController.text.isNotEmpty ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _toController.text = ""; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + (_refundController + .text + .isNotEmpty || + !supportsRefund); + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = + data.text!.trim(); + + _toController.text = + content; + model.recipientAddress = + _toController.text; + + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + (_refundController + .text + .isNotEmpty || + !supportsRefund); + }); + } + }, + child: + _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey", + ), + onTap: () { + ref + .read( + exchangeFlowIsActiveStateProvider + .state, + ) + .state = true; + Navigator.of( + context, + ).pushNamed(AddressBookView.routeName).then(( + _, + ) { + ref + .read( + exchangeFlowIsActiveStateProvider + .state, + ) + .state = false; + + final address = + ref + .read( + exchangeFromAddressBookAddressStateProvider + .state, + ) + .state; + if (address.isNotEmpty) { + _toController.text = + address; + model.recipientAddress = + _toController.text; + ref + .read( + exchangeFromAddressBookAddressStateProvider + .state, + ) + .state = ""; + } + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + (_refundController + .text + .isNotEmpty || + !supportsRefund); + }); + }); + }, + child: const AddressBookIcon(), + ), + if (_toController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: _onToQrTapped, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox(height: 6), + RoundedWhiteContainer( + child: Text( + "This is the wallet where your ${model.receiveTicker.toUpperCase()} will be sent to.", + style: STextStyles.label(context), + ), + ), + const SizedBox(height: 24), + if (supportsRefund) + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Refund Wallet (required)", + style: STextStyles.smallMed12(context), + ), + if (AppConfig.isStackCoin(model.sendTicker)) + CustomTextButton( + text: "Choose from ${AppConfig.prefix}", + onTap: () { + try { + final coin = AppConfig.coins + .firstWhere( + (e) => + e.ticker.toLowerCase() == + model.sendTicker + .toLowerCase(), + ); + + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final wallet = ref + .read(pWallets) + .getWallet(value); + + _refundController.text = + wallet.info.name; + model.refundAddress = + (await wallet + .getCurrentReceivingAddress())! + .value; + } + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + _refundController + .text + .isNotEmpty; + }); + }); + } catch (e, s) { + Logging.instance.i( + "$e\n$s", + error: e, + stackTrace: s, + ); + } + }, + ), + ], + ), + if (supportsRefund) const SizedBox(height: 4), + if (supportsRefund) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "refundExchangeStep2ViewAddressFieldKey", + ), + controller: _refundController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: [ + // FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + focusNode: _refundFocusNode, + style: STextStyles.field(context), + onChanged: (value) { + model.refundAddress = + _refundController.text; + setState(() { + enableNext = + _toController.text.isNotEmpty && + _refundController.text.isNotEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter ${model.sendTicker.toUpperCase()} refund address", + _refundFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: + _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _refundController.text.isNotEmpty + ? TextFieldIconButton( key: const Key( "sendViewClearAddressFieldButtonKey", ), @@ -625,27 +660,30 @@ class _Step2ViewState extends ConsumerState { _refundController.text; setState(() { - enableNext = _toController + enableNext = + _toController .text .isNotEmpty && _refundController - .text.isNotEmpty; + .text + .isNotEmpty; }); }, child: const XIcon(), ) - : TextFieldIconButton( + : TextFieldIconButton( key: const Key( "sendViewPasteAddressFieldButtonKey", ), onTap: () async { final ClipboardData? data = await clipboard.getData( - Clipboard.kTextPlain, - ); + Clipboard.kTextPlain, + ); if (data?.text != null && data! - .text!.isNotEmpty) { + .text! + .isNotEmpty) { final content = data.text!.trim(); @@ -656,7 +694,8 @@ class _Step2ViewState extends ConsumerState { .text; setState(() { - enableNext = _toController + enableNext = + _toController .text .isNotEmpty && _refundController @@ -665,131 +704,139 @@ class _Step2ViewState extends ConsumerState { }); } }, - child: _refundController - .text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + child: + _refundController + .text + .isEmpty + ? const ClipboardIcon() + : const XIcon(), ), - if (_refundController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey", - ), - onTap: () { - ref - .read( - exchangeFlowIsActiveStateProvider - .state, - ) - .state = true; - Navigator.of(context) - .pushNamed( - AddressBookView.routeName, - ) - .then((_) { + if (_refundController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey", + ), + onTap: () { ref .read( exchangeFlowIsActiveStateProvider .state, ) - .state = false; - final address = ref - .read( - exchangeFromAddressBookAddressStateProvider - .state, + .state = true; + Navigator.of(context) + .pushNamed( + AddressBookView + .routeName, ) - .state; - if (address.isNotEmpty) { - _refundController.text = - address; - model.refundAddress = - _refundController.text; - } - setState(() { - enableNext = _toController - .text.isNotEmpty && - _refundController - .text.isNotEmpty; - }); - }); - }, - child: const AddressBookIcon(), - ), - if (_refundController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey", + .then((_) { + ref + .read( + exchangeFlowIsActiveStateProvider + .state, + ) + .state = false; + final address = + ref + .read( + exchangeFromAddressBookAddressStateProvider + .state, + ) + .state; + if (address + .isNotEmpty) { + _refundController + .text = address; + model.refundAddress = + _refundController + .text; + } + setState(() { + enableNext = + _toController + .text + .isNotEmpty && + _refundController + .text + .isNotEmpty; + }); + }); + }, + child: const AddressBookIcon(), ), - onTap: _onRefundQrTapped, - child: const QrCodeIcon(), - ), - ], + if (_refundController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: _onRefundQrTapped, + child: const QrCodeIcon(), + ), + ], + ), ), ), ), ), ), - ), - if (supportsRefund) - const SizedBox( - height: 6, - ), - if (supportsRefund) - RoundedWhiteContainer( - child: Text( - "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", - style: STextStyles.label(context), + if (supportsRefund) const SizedBox(height: 6), + if (supportsRefund) + RoundedWhiteContainer( + child: Text( + "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", + style: STextStyles.label(context), + ), ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Back", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + const SizedBox(height: 16), + const Spacer(), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context, + ), + child: Text( + "Back", + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, + ), ), ), ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Next", - enabled: enableNext, - onPressed: () { - Navigator.of(context).pushNamed( - Step3View.routeName, - arguments: model, - ); - }, + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Next", + enabled: enableNext, + onPressed: () { + Navigator.of(context).pushNamed( + Step3View.routeName, + arguments: model, + ); + }, + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart index 2cd014fc3..4f0b352c3 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart @@ -74,307 +74,301 @@ class _Step3ViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 75)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, ), - title: Text( - "Swap", - style: STextStyles.navBarTitle(context), - ), + title: Text("Swap", style: STextStyles.navBarTitle(context)), ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 2, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Confirm exchange details", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 24, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "You send", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "You receive", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - "${model.receiveAmount.toString()} ${model.receiveTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow(count: 4, current: 2, width: width), + const SizedBox(height: 14), + Text( + "Confirm exchange details", + style: STextStyles.pageTitleH1(context), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "Estimated rate", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Text( - model.rateInfo, - style: STextStyles.itemSubtitle12(context), - ), - ], + const SizedBox(height: 24), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "You send", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Recipient ${model.receiveTicker.toUpperCase()} address", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - Text( - model.recipientAddress!, - style: STextStyles.itemSubtitle12(context), - ), - ], + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "You receive", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + "${model.receiveAmount.toString()} ${model.receiveTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), ), - ), - if (supportsRefund) - const SizedBox( - height: 8, + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Estimated rate", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + model.rateInfo, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), ), - if (supportsRefund) + const SizedBox(height: 8), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Refund ${model.sendTicker.toUpperCase()} address", + "Recipient ${model.receiveTicker.toUpperCase()} address", style: STextStyles.itemSubtitle(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( - model.refundAddress!, + model.recipientAddress!, style: STextStyles.itemSubtitle12(context), ), ], ), ), - const SizedBox( - height: 8, - ), - const Spacer(), - Row( - children: [ - Expanded( - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Back", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + if (supportsRefund) const SizedBox(height: 8), + if (supportsRefund) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Refund ${model.sendTicker.toUpperCase()} address", + style: STextStyles.itemSubtitle(context), ), - ), + const SizedBox(height: 4), + Text( + model.refundAddress!, + style: STextStyles.itemSubtitle12( + context, + ), + ), + ], ), ), - const SizedBox( - width: 16, - ), - Expanded( - child: TextButton( - onPressed: () async { - unawaited( - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Creating a trade", - eventBus: null, - ), - ), + const SizedBox(height: 8), + const Spacer(), + Row( + children: [ + Expanded( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context, ), + child: Text( + "Back", + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), - ); + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextButton( + onPressed: () async { + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: + (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.6), + child: + const CustomLoadingOverlay( + message: + "Creating a trade", + eventBus: null, + ), + ), + ), + ), + ); - final ExchangeResponse response = - await ref - .read(efExchangeProvider) - .createTrade( - from: model.sendTicker, - to: model.receiveTicker, - fixedRate: model.rateType != - ExchangeRateType.estimated, - amount: model.reversed - ? model.receiveAmount - : model.sendAmount, - addressTo: - model.recipientAddress!, - extraId: null, - addressRefund: supportsRefund - ? model.refundAddress! - : "", - refundExtraId: "", - estimate: model.estimate, - reversed: model.reversed, - ); + final ExchangeResponse + response = await ref + .read(efExchangeProvider) + .createTrade( + from: model.sendTicker, + fromNetwork: + model.sendCurrency.network, + to: model.receiveTicker, + toNetwork: + model.receiveCurrency.network, + fixedRate: + model.rateType != + ExchangeRateType.estimated, + amount: + model.reversed + ? model.receiveAmount + : model.sendAmount, + addressTo: model.recipientAddress!, + extraId: null, + addressRefund: + supportsRefund + ? model.refundAddress! + : "", + refundExtraId: "", + estimate: model.estimate, + reversed: model.reversed, + ); - if (response.value == null) { - if (context.mounted) { - Navigator.of(context).pop(); + if (response.value == null) { + if (context.mounted) { + Navigator.of(context).pop(); - // TODO: better errors - String? message; - if (response.exception != null) { - message = - response.exception!.toString(); - if (message.startsWith( - "FormatException:", - ) && - message.contains("")) { + // TODO: better errors + String? message; + if (response.exception != null) { message = - "${ref.read(efExchangeProvider).name} server error"; + response.exception!.toString(); + if (message.startsWith( + "FormatException:", + ) && + message.contains("")) { + message = + "${ref.read(efExchangeProvider).name} server error"; + } } - } - unawaited( - showDialog( - context: context, - barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to create trade", - message: message ?? "", + unawaited( + showDialog( + context: context, + barrierDismissible: true, + builder: + (_) => StackDialog( + title: + "Failed to create trade", + message: message ?? "", + ), ), - ), - ); + ); + } + return; } - return; - } - - // save trade to hive - await ref.read(tradesServiceProvider).add( - trade: response.value!, - shouldNotifyListeners: true, - ); - String status = response.value!.status; + // save trade to hive + await ref + .read(tradesServiceProvider) + .add( + trade: response.value!, + shouldNotifyListeners: true, + ); - model.trade = response.value!; + String status = response.value!.status; - // extra info if status is waiting - if (status == "Waiting") { - status += " for deposit"; - } + model.trade = response.value!; - if (mounted) { - Navigator.of(context).pop(); - } + // extra info if status is waiting + if (status == "Waiting") { + status += " for deposit"; + } - unawaited( - NotificationApi.showNotification( - changeNowId: model.trade!.tradeId, - title: status, - body: - "Trade ID ${model.trade!.tradeId}", - walletId: "", - iconAssetName: Assets.svg.arrowRotate, - date: model.trade!.timestamp, - shouldWatchForUpdates: true, - coinName: "coinName", - ), - ); + if (mounted) { + Navigator.of(context).pop(); + } - if (mounted) { unawaited( - Navigator.of(context).pushNamed( - Step4View.routeName, - arguments: model, + NotificationApi.showNotification( + changeNowId: model.trade!.tradeId, + title: status, + body: + "Trade ID ${model.trade!.tradeId}", + walletId: "", + iconAssetName: Assets.svg.arrowRotate, + date: model.trade!.timestamp, + shouldWatchForUpdates: true, + coinName: "coinName", ), ); - } - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Next", - style: STextStyles.button(context), + + if (context.mounted) { + unawaited( + Navigator.of(context).pushNamed( + Step4View.routeName, + arguments: model, + ), + ); + } + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Next", + style: STextStyles.button(context), + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); 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 7b867aab3..973298bd6 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 @@ -21,6 +21,7 @@ import '../../../models/exchange/incomplete_exchange.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../providers/providers.dart'; import '../../../route_generator.dart'; +import '../../../services/wallets.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount_formatter.dart'; @@ -34,6 +35,8 @@ import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/models/tx_data.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../widgets/background.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; @@ -65,6 +68,7 @@ class Step4View extends ConsumerStatefulWidget { } class _Step4ViewState extends ConsumerState { + late final bool isWalletCoinAndCanSend; late final IncompleteExchangeModel model; late final ClipboardInterface clipboard; @@ -72,13 +76,20 @@ class _Step4ViewState extends ConsumerState { Timer? _statusTimer; - bool _isWalletCoinAndHasWallet(String ticker, WidgetRef ref) { + bool isWalletCoinAndCanSendWithoutWalletOpened( + String ticker, + Wallets walletsInstance, + ) { try { final coin = AppConfig.getCryptoCurrencyForTicker(ticker); - return ref - .read(pWallets) - .wallets - .where((e) => e.info.coin == coin) + return walletsInstance.wallets + .where( + (e) => + e.info.coin == coin && + (e is! ExternalWallet || + e is MwebInterface), // ltc mweb is external but swaps + // should not use mweb, hence the odd logic check here + ) .isNotEmpty; } catch (_) { return false; @@ -86,8 +97,9 @@ class _Step4ViewState extends ConsumerState { } Future _updateStatus() async { - final statusResponse = - await ref.read(efExchangeProvider).updateTrade(model.trade!); + final statusResponse = await ref + .read(efExchangeProvider) + .updateTrade(model.trade!); String status = "Waiting"; if (statusResponse.value != null) { status = statusResponse.value!.status; @@ -110,6 +122,11 @@ class _Step4ViewState extends ConsumerState { model = widget.model; clipboard = widget.clipboard; + isWalletCoinAndCanSend = isWalletCoinAndCanSendWithoutWalletOpened( + model.trade!.payInCurrency, + ref.read(pWallets), + ); + _statusTimer = Timer.periodic(const Duration(seconds: 60), (_) { _updateStatus(); }); @@ -149,46 +166,34 @@ class _Step4ViewState extends ConsumerState { Theme.of(context).extension()!.backgroundAppBar, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius * 3, - ), + top: Radius.circular(Constants.size.circularBorderRadius * 3), ), ), builder: (context) { return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), Text( "Select Firo balance", style: STextStyles.pageTitleH2(context), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), SecondaryButton( label: "${ref.watch(pAmountFormatter(coin)).format(balancePrivate.spendable)} (private)", onPressed: () => Navigator.of(context).pop(false), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), SecondaryButton( label: "${ref.watch(pAmountFormatter(coin)).format(balancePublic.spendable)} (public)", onPressed: () => Navigator.of(context).pop(true), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), ], ), ); @@ -237,55 +242,46 @@ class _Step4ViewState extends ConsumerState { ), ); - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); Future txDataFuture; + final recipient = TxRecipient( + address: address, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(address)!, + ); + if (wallet is FiroWallet && !firoPublicSend) { txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], - note: "${model.trade!.payInCurrency.toUpperCase()}/" + recipients: [recipient], + note: + "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", ), ); } else { - final memo = wallet.info.coin is Stellar - ? model.trade!.payInExtraId.isNotEmpty - ? model.trade!.payInExtraId - : null - : null; + final memo = + wallet.info.coin is Stellar + ? model.trade!.payInExtraId.isNotEmpty + ? model.trade!.payInExtraId + : null + : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], + recipients: [recipient], memo: memo, feeRateType: FeeRateType.average, - note: "${model.trade!.payInCurrency.toUpperCase()}/" + note: + "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", ), ); } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); final txData = results.first as TxData; @@ -301,13 +297,14 @@ class _Step4ViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmChangeNowSendView( - txData: txData, - walletId: tuple.item1, - routeOnSuccessName: HomeView.routeName, - trade: model.trade!, - shouldSendPublicFiroFunds: firoPublicSend, - ), + builder: + (_) => ConfirmChangeNowSendView( + txData: txData, + walletId: tuple.item1, + routeOnSuccessName: HomeView.routeName, + trade: model.trade!, + shouldSendPublicFiroFunds: firoPublicSend, + ), settings: const RouteSettings( name: ConfirmChangeNowSendView.routeName, ), @@ -338,9 +335,10 @@ class _Step4ViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), ), onPressed: () { @@ -357,8 +355,6 @@ class _Step4ViewState extends ConsumerState { @override Widget build(BuildContext context) { - final bool isWalletCoin = - _isWalletCoinAndHasWallet(model.trade!.payInCurrency, ref); return WillPopScope( onWillPop: () async { await _close(); @@ -379,214 +375,146 @@ class _Step4ViewState extends ConsumerState { Assets.svg.x, width: 24, height: 24, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: _close, ), ), - title: Text( - "Swap", - style: STextStyles.navBarTitle(context), - ), + title: Text("Swap", style: STextStyles.navBarTitle(context)), ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 3, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Send ${model.sendTicker.toUpperCase()} to the address below", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), - RoundedContainer( - color: Theme.of(context) - .extension()! - .warningBackground, - child: RichText( - text: TextSpan( - text: - "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", - style: STextStyles.label700(context).copyWith( - color: Theme.of(context) + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow(count: 4, current: 3, width: width), + const SizedBox(height: 14), + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox(height: 8), + Text( + "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox(height: 12), + RoundedContainer( + color: + Theme.of(context) .extension()! - .warningForeground, - ), - children: [ - TextSpan( - text: - "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", - style: - STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, - ), + .warningBackground, + child: RichText( + text: TextSpan( + text: + "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", + style: STextStyles.label700( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .warningForeground, ), - ], - ), - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, children: [ - Text( - "Amount", - style: - STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.sendAmount.toString(), - ); - await clipboard.setData(data); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) + TextSpan( + text: + "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", + style: STextStyles.label( + context, + ).copyWith( + color: + Theme.of(context) .extension()! - .infoItemIcons, - width: 10, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], + .warningForeground, ), ), ], ), - const SizedBox( - height: 4, - ), - Text( - "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", - style: STextStyles.itemSubtitle12(context), - ), - ], + ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Send ${model.sendTicker.toUpperCase()} to this address", - style: - STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.trade!.payInAddress, - ); - await clipboard.setData(data); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.itemSubtitle( + context, + ), + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.sendAmount.toString(), ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, - width: 10, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], + await clipboard.setData(data); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: + "Copied to clipboard", + context: context, + ), + ); + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: + Theme.of(context) + .extension< + StackColors + >()! + .infoItemIcons, + width: 10, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2( + context, + ), + ), + ], + ), ), + ], + ), + const SizedBox(height: 4), + Text( + "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", + style: STextStyles.itemSubtitle12( + context, ), - ], - ), - const SizedBox( - height: 4, - ), - Text( - model.trade!.payInAddress, - style: STextStyles.itemSubtitle12(context), - ), - ], + ), + ], + ), ), - ), - const SizedBox( - height: 6, - ), - if (model.trade!.payInExtraId.isNotEmpty) + const SizedBox(height: 8), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -596,14 +524,15 @@ class _Step4ViewState extends ConsumerState { MainAxisAlignment.spaceBetween, children: [ Text( - "Memo", - style: - STextStyles.itemSubtitle(context), + "Send ${model.sendTicker.toUpperCase()} to this address", + style: STextStyles.itemSubtitle( + context, + ), ), GestureDetector( onTap: () async { final data = ClipboardData( - text: model.trade!.payInExtraId, + text: model.trade!.payInAddress, ); await clipboard.setData(data); if (context.mounted) { @@ -621,288 +550,368 @@ class _Step4ViewState extends ConsumerState { children: [ SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of(context) + .extension< + StackColors + >()! + .infoItemIcons, width: 10, ), - const SizedBox( - width: 4, - ), + const SizedBox(width: 4), Text( "Copy", - style: - STextStyles.link2(context), + style: STextStyles.link2( + context, + ), ), ], ), ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( - model.trade!.payInExtraId, - style: - STextStyles.itemSubtitle12(context), + model.trade!.payInAddress, + style: STextStyles.itemSubtitle12( + context, + ), ), ], ), ), - if (model.trade!.payInExtraId.isNotEmpty) - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Row( - children: [ - Text( - "Trade ID", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( + const SizedBox(height: 6), + if (model.trade!.payInExtraId.isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - Text( - model.trade!.tradeId, - style: - STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Memo", + style: STextStyles.itemSubtitle( + context, + ), + ), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.trade!.payInExtraId, + ); + await clipboard.setData(data); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: + "Copied to clipboard", + context: context, + ), + ); + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: + Theme.of(context) + .extension< + StackColors + >()! + .infoItemIcons, + width: 10, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2( + context, + ), + ), + ], + ), + ), + ], ), - GestureDetector( - onTap: () async { - final data = ClipboardData( - text: model.trade!.tradeId, - ); - await clipboard.setData(data); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); - } - }, - child: SvgPicture.asset( - Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, - width: 12, + const SizedBox(height: 4), + Text( + model.trade!.payInExtraId, + style: STextStyles.itemSubtitle12( + context, ), ), ], ), - ], + ), + if (model.trade!.payInExtraId.isNotEmpty) + const SizedBox(height: 6), + RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Trade ID", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + Text( + model.trade!.tradeId, + style: STextStyles.itemSubtitle12( + context, + ), + ), + const SizedBox(width: 10), + GestureDetector( + onTap: () async { + final data = ClipboardData( + text: model.trade!.tradeId, + ); + await clipboard.setData(data); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: + "Copied to clipboard", + context: context, + ), + ); + } + }, + child: SvgPicture.asset( + Assets.svg.copy, + color: + Theme.of(context) + .extension()! + .infoItemIcons, + width: 12, + ), + ), + ], + ), + ], + ), ), - ), - const SizedBox( - height: 6, - ), - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - Text( - _statusString, - style: STextStyles.itemSubtitle(context) - .copyWith( - color: Theme.of(context) - .extension()! - .colorForStatus(_statusString), + const SizedBox(height: 6), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Status", + style: STextStyles.itemSubtitle(context), ), - ), - ], + Text( + _statusString, + style: STextStyles.itemSubtitle( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .colorForStatus(_statusString), + ), + ), + ], + ), ), - ), - const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: () { - showDialog( - context: context, - barrierDismissible: true, - builder: (_) { - return StackDialogBase( - child: Column( - children: [ - const SizedBox( - height: 8, - ), - Center( - child: Text( - "Send ${model.sendTicker} to this address", - style: STextStyles.pageTitleH2( - context, + const Spacer(), + const SizedBox(height: 12), + TextButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: true, + builder: (_) { + return StackDialogBase( + child: Column( + children: [ + const SizedBox(height: 8), + Center( + child: Text( + "Send ${model.sendTicker} to this address", + style: STextStyles.pageTitleH2( + context, + ), ), ), - ), - const SizedBox( - height: 24, - ), - Center( - child: QR( - // TODO: grab coin uri scheme from somewhere - // data: "${coin.uriScheme}:$receivingAddress", - data: model.trade!.payInAddress, - size: MediaQuery.of(context) - .size - .width / - 2, + const SizedBox(height: 24), + Center( + child: QR( + // TODO: grab coin uri scheme from somewhere + // data: "${coin.uriScheme}:$receivingAddress", + data: model.trade!.payInAddress, + size: + MediaQuery.of( + context, + ).size.width / + 2, + ), ), - ), - const SizedBox( - height: 24, - ), - Row( - children: [ - const Spacer(), - Expanded( - child: TextButton( - onPressed: () => - Navigator.of(context) - .pop(), - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle( + const SizedBox(height: 24), + Row( + children: [ + const Spacer(), + Expanded( + child: TextButton( + onPressed: + () => + Navigator.of( + context, + ).pop(), + style: Theme.of(context) + .extension< + StackColors + >()! + .getSecondaryEnabledButtonStyle( + context, + ), + child: Text( + "Cancel", + style: STextStyles.button( context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .buttonTextSecondary, ), - child: Text( - "Cancel", - style: STextStyles.button( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .buttonTextSecondary, ), ), ), - ), - ], - ), - ], - ), - ); - }, - ); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Show QR Code", - style: STextStyles.button(context), - ), - ), - if (isWalletCoin) - const SizedBox( - height: 12, - ), - if (isWalletCoin) - Builder( - builder: (context) { - String buttonTitle = - "Send from ${AppConfig.appName}"; - - final tuple = ref - .read( - exchangeSendFromWalletIdStateProvider - .state, - ) - .state; - if (tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase()) { - final walletName = ref - .read(pWallets) - .getWallet(tuple.item1) - .info - .name; - buttonTitle = "Send from $walletName"; - } - - return TextButton( - onPressed: tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase() - ? () async { - await _confirmSend(tuple); - } - : () { - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: - (BuildContext context) { - final coin = AppConfig.coins - .firstWhere( - (e) => - e.ticker - .toLowerCase() == - model.trade! - .payInCurrency - .toLowerCase(), - ); - - return SendFromView( - coin: coin, - amount: model.sendAmount - .toAmount( - fractionDigits: - coin.fractionDigits, - ), - address: model - .trade!.payInAddress, - trade: model.trade!, - ); - }, - settings: const RouteSettings( - name: SendFromView.routeName, - ), - ), - ); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle( - context, + ], + ), + ], ), - child: Text( - buttonTitle, - style: - STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - ), + ); + }, ); }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), ), - ], + if (isWalletCoinAndCanSend) + const SizedBox(height: 12), + if (isWalletCoinAndCanSend) + Builder( + builder: (context) { + String buttonTitle = + "Send from ${AppConfig.appName}"; + + final tuple = + ref + .read( + exchangeSendFromWalletIdStateProvider + .state, + ) + .state; + if (tuple != null && + model.sendTicker.toLowerCase() == + tuple.item2.ticker.toLowerCase()) { + final walletName = + ref + .read(pWallets) + .getWallet(tuple.item1) + .info + .name; + buttonTitle = "Send from $walletName"; + } + + return TextButton( + onPressed: + tuple != null && + model.sendTicker + .toLowerCase() == + tuple.item2.ticker + .toLowerCase() + ? () async { + await _confirmSend(tuple); + } + : () { + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: ( + BuildContext context, + ) { + final coin = AppConfig + .coins + .firstWhere( + (e) => + e.ticker + .toLowerCase() == + model + .trade! + .payInCurrency + .toLowerCase(), + ); + + return SendFromView( + coin: coin, + amount: model.sendAmount + .toAmount( + fractionDigits: + coin.fractionDigits, + ), + address: + model + .trade! + .payInAddress, + trade: model.trade!, + ); + }, + settings: + const RouteSettings( + name: + SendFromView + .routeName, + ), + ), + ); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context, + ), + child: Text( + buttonTitle, + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + ); + }, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 7e97017a3..fa0283ff4 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -20,6 +20,7 @@ import '../../models/exchange/response_objects/trade.dart'; import '../../pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; +import '../../services/exchange/trocador/trocador_exchange.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; @@ -91,12 +92,13 @@ class _SendFromViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final walletIds = ref - .watch(pWallets) - .wallets - .where((e) => e.info.coin == coin) - .map((e) => e.walletId) - .toList(); + final walletIds = + ref + .watch(pWallets) + .wallets + .where((e) => e.info.coin == coin) + .map((e) => e.walletId) + .toList(); final isDesktop = Util.isDesktop; @@ -113,55 +115,51 @@ class _SendFromViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Send from", - style: STextStyles.navBarTitle(context), - ), + title: Text("Send from", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, + body: SafeArea( + child: Padding(padding: const EdgeInsets.all(16), child: child), ), ), ); }, child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxHeight: double.infinity, + child: Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Send from ${AppConfig.prefix}", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: widget.shouldPopRoot, + ).pop, + ), + ], + ), Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Send from ${AppConfig.prefix}", - style: STextStyles.desktopH3(context), - ), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: widget.shouldPopRoot, - ).pop, + child: child, ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: child, - ), - ], - ), - ), + ), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -169,20 +167,17 @@ class _SendFromViewState extends ConsumerState { children: [ Text( "You need to send ${ref.watch(pAmountFormatter(coin)).format(amount)}", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ConditionalParent( condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: ListView.builder( primary: isDesktop ? false : null, shrinkWrap: isDesktop, @@ -250,14 +245,15 @@ class _SendFromCardState extends ConsumerState { builder: (context) { return ConditionalParent( condition: Util.isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 400, - maxHeight: double.infinity, - child: Padding( - padding: const EdgeInsets.all(32), - child: child, - ), - ), + builder: + (child) => DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + ), child: BuildingTransactionDialog( coin: coin, isSpark: @@ -282,31 +278,29 @@ class _SendFromCardState extends ConsumerState { await wallet.open(); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); TxData txData; Future txDataFuture; + final recipient = TxRecipient( + address: address, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(address)!, + ); + // if not firo then do normal send if (shouldSendPublicFiroFunds == null) { - final memo = coin is Stellar - ? trade.payInExtraId.isNotEmpty - ? trade.payInExtraId - : null - : null; + final memo = + coin is Stellar + ? trade.payInExtraId.isNotEmpty + ? trade.payInExtraId + : null + : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], + recipients: [recipient], memo: memo, feeRateType: FeeRateType.average, ), @@ -317,36 +311,21 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds) { txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], + recipients: [recipient], feeRateType: FeeRateType.average, ), ); } else { txDataFuture = firoWallet.prepareSendSpark( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], + recipients: [recipient], // feeRateType: FeeRateType.average, ), ); } } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; @@ -354,14 +333,12 @@ class _SendFromCardState extends ConsumerState { // pop building dialog if (mounted) { - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop(); + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); } txData = txData.copyWith( - note: "${trade.payInCurrency.toUpperCase()}/" + note: + "${trade.payInCurrency.toUpperCase()}/" "${trade.payOutCurrency.toUpperCase()} exchange", ); @@ -369,16 +346,18 @@ class _SendFromCardState extends ConsumerState { await Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmChangeNowSendView( - txData: txData, - walletId: walletId, - routeOnSuccessName: Util.isDesktop - ? DesktopExchangeView.routeName - : HomeView.routeName, - trade: trade, - shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, - fromDesktopStep4: widget.fromDesktopStep4, - ), + builder: + (_) => ConfirmChangeNowSendView( + txData: txData, + walletId: walletId, + routeOnSuccessName: + Util.isDesktop + ? DesktopExchangeView.routeName + : HomeView.routeName, + trade: trade, + shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, + fromDesktopStep4: widget.fromDesktopStep4, + ), settings: const RouteSettings( name: ConfirmChangeNowSendView.routeName, ), @@ -407,9 +386,10 @@ class _SendFromCardState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), ), onPressed: () { @@ -434,12 +414,6 @@ class _SendFromCardState extends ConsumerState { @override Widget build(BuildContext context) { - final wallet = ref.watch(pWallets).getWallet(walletId); - - final locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale), - ); - final coin = ref.watch(pWalletCoin(walletId)); final isFiro = coin is Firo; @@ -448,86 +422,161 @@ class _SendFromCardState extends ConsumerState { padding: const EdgeInsets.all(0), child: ConditionalParent( condition: isFiro, - builder: (child) => Expandable( - header: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: child, - ), - ), - body: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MaterialButton( - splashColor: - Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"), - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () async { - if (mounted) { - unawaited( - _send( - shouldSendPublicFiroFunds: false, + builder: + (child) => Expandable( + header: Container( + color: Colors.transparent, + child: Padding(padding: const EdgeInsets.all(12), child: child), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!trade.exchangeName.startsWith( + TrocadorExchange.exchangeName, + )) + MaterialButton( + splashColor: + Theme.of(context).extension()!.highlight, + key: Key( + "walletsSheetItemButtonFiroPrivateKey_$walletId", + ), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + if (mounted) { + unawaited(_send(shouldSendPublicFiroFunds: false)); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use private balance", + style: STextStyles.itemSubtitle(context), + ), + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + ref + .watch( + pWalletBalanceTertiary( + walletId, + ), + ) + .spendable, + ), + style: STextStyles.itemSubtitle(context), + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ], + ), + ), + ), + ), + MaterialButton( + splashColor: + Theme.of(context).extension()!.highlight, + key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 6, - left: 16, - right: 16, - bottom: 6, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + onPressed: () async { + if (mounted) { + unawaited(_send(shouldSendPublicFiroFunds: true)); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Use private balance", - style: STextStyles.itemSubtitle(context), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use public balance", + style: STextStyles.itemSubtitle(context), + ), + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + ref + .watch(pWalletBalance(walletId)) + .spendable, + ), + style: STextStyles.itemSubtitle(context), + ), + ], ), - Text( - ref.watch(pAmountFormatter(coin)).format( - ref - .watch(pWalletBalanceTertiary(walletId)) - .spendable, - ), - style: STextStyles.itemSubtitle(context), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: + Theme.of( + context, + ).extension()!.infoItemLabel, ), ], ), - SvgPicture.asset( - Assets.svg.chevronRight, - height: 14, - width: 7, - color: Theme.of(context) - .extension()! - .infoItemLabel, - ), - ], + ), ), ), - ), + const SizedBox(height: 6), + ], ), - MaterialButton( + ), + child: ConditionalParent( + condition: !isFiro, + builder: + (child) => MaterialButton( splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), - padding: const EdgeInsets.all(0), + key: Key("walletsSheetItemButtonKey_$walletId"), + padding: const EdgeInsets.all(8), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( @@ -536,83 +585,11 @@ class _SendFromCardState extends ConsumerState { ), onPressed: () async { if (mounted) { - unawaited( - _send( - shouldSendPublicFiroFunds: true, - ), - ); + unawaited(_send()); } }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 6, - left: 16, - right: 16, - bottom: 6, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Use public balance", - style: STextStyles.itemSubtitle(context), - ), - Text( - ref.watch(pAmountFormatter(coin)).format( - ref - .watch(pWalletBalance(walletId)) - .spendable, - ), - style: STextStyles.itemSubtitle(context), - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronRight, - height: 14, - width: 7, - color: Theme.of(context) - .extension()! - .infoItemLabel, - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 6, - ), - ], - ), - ), - child: ConditionalParent( - condition: !isFiro, - builder: (child) => MaterialButton( - splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonKey_$walletId"), - padding: const EdgeInsets.all(8), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: child, ), - ), - onPressed: () async { - if (mounted) { - unawaited( - _send(), - ); - } - }, - child: child, - ), child: Row( children: [ Container( @@ -625,19 +602,13 @@ class _SendFromCardState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(6), child: SvgPicture.file( - File( - ref.watch( - coinIconProvider(coin), - ), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -647,13 +618,12 @@ class _SendFromCardState extends ConsumerState { ref.watch(pWalletName(walletId)), style: STextStyles.titleBold12(context), ), - if (!isFiro) - const SizedBox( - height: 2, - ), + if (!isFiro) const SizedBox(height: 2), if (!isFiro) Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref.watch(pWalletBalance(walletId)).spendable, ), style: STextStyles.itemSubtitle(context), diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart index 5b11876e8..8b56ae213 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart @@ -14,10 +14,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import '../../../app_config.dart'; +import '../../../models/exchange/aggregate_currency.dart'; import '../../../models/exchange/response_objects/estimate.dart'; import '../../../providers/exchange/exchange_form_state_provider.dart'; import '../../../providers/global/locale_provider.dart'; import '../../../services/exchange/exchange.dart'; +import '../../../services/exchange/trocador/trocador_exchange.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount_formatter.dart'; @@ -30,6 +32,9 @@ import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../widgets/animated_text.dart'; import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/basic_dialog.dart'; import '../../../widgets/exchange/trocador/trocador_kyc_info_button.dart'; import '../../../widgets/exchange/trocador/trocador_rating_type_enum.dart'; @@ -54,18 +59,26 @@ class _ExchangeOptionState extends ConsumerState { @override Widget build(BuildContext context) { - final sendCurrency = - ref.watch(efCurrencyPairProvider.select((value) => value.send)); - final receivingCurrency = - ref.watch(efCurrencyPairProvider.select((value) => value.receive)); + final sendCurrency = ref.watch( + efCurrencyPairProvider.select((value) => value.send), + ); + final receivingCurrency = ref.watch( + efCurrencyPairProvider.select((value) => value.receive), + ); final reversed = ref.watch(efReversedProvider); - final amount = reversed - ? ref.watch(efReceiveAmountProvider) - : ref.watch(efSendAmountProvider); + final amount = + reversed + ? ref.watch(efReceiveAmountProvider) + : ref.watch(efSendAmountProvider); final data = ref.watch(efEstimatesListProvider(widget.exchange.name)); final estimates = data?.item1.value; + final pair = + sendCurrency != null && receivingCurrency != null + ? (from: sendCurrency, to: receivingCurrency) + : null; + return AnimatedSize( duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubicEmphasized, @@ -76,6 +89,7 @@ class _ExchangeOptionState extends ConsumerState { return _ProviderOption( exchange: widget.exchange, estimate: null, + pair: pair, rateString: "", loadingString: true, ); @@ -94,10 +108,10 @@ class _ExchangeOptionState extends ConsumerState { int decimals; try { - decimals = AppConfig.getCryptoCurrencyForTicker( - receivingCurrency.ticker, - )! - .fractionDigits; + decimals = + AppConfig.getCryptoCurrencyForTicker( + receivingCurrency.ticker, + )!.fractionDigits; } catch (_) { decimals = 8; // some reasonable alternative } @@ -123,46 +137,50 @@ class _ExchangeOptionState extends ConsumerState { final String rateString; if (coin != null) { - rateString = "1 ${sendCurrency.ticker.toUpperCase()} " + rateString = + "1 ${sendCurrency.ticker.toUpperCase()} " "~ ${ref.watch(pAmountFormatter(coin)).format(rate)}"; } else { final formatter = AmountFormatter( unit: AmountUnit.normal, locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), ), coin: Bitcoin( CryptoCurrencyNetwork.main, ), // some sane default maxDecimals: 8, // some sane default ); - rateString = "1 ${sendCurrency.ticker.toUpperCase()} " + rateString = + "1 ${sendCurrency.ticker.toUpperCase()} " "~ ${formatter.format(rate, withUnitName: false)}" " ${receivingCurrency.ticker.toUpperCase()}"; } return ConditionalParent( condition: i > 0, - builder: (child) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - isDesktop - ? Container( - height: 1, - color: Theme.of(context) - .extension()! - .background, - ) - : const SizedBox( - height: 16, - ), - child, - ], - ), + builder: + (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + isDesktop + ? Container( + height: 1, + color: + Theme.of(context) + .extension()! + .background, + ) + : const SizedBox(height: 16), + child, + ], + ), child: _ProviderOption( key: Key(widget.exchange.name + e.exchangeProvider), exchange: widget.exchange, + pair: pair, estimate: e, rateString: rateString, kycRating: e.kycRating, @@ -189,16 +207,18 @@ class _ExchangeOptionState extends ConsumerState { message ??= "Amount too large"; } } else if (data?.item1.value == null) { - final rateType = ref.watch(efRateTypeProvider) == - ExchangeRateType.estimated - ? "estimated" - : "fixed"; + final rateType = + ref.watch(efRateTypeProvider) == + ExchangeRateType.estimated + ? "estimated" + : "fixed"; message ??= "Pair unavailable on $rateType rate flow"; } return _ProviderOption( exchange: widget.exchange, estimate: null, + pair: pair, rateString: message ?? "Failed to fetch rate", rateColor: Theme.of(context).extension()!.textError, @@ -211,6 +231,7 @@ class _ExchangeOptionState extends ConsumerState { return _ProviderOption( exchange: widget.exchange, estimate: null, + pair: pair, rateString: "n/a", ); } @@ -225,6 +246,7 @@ class _ProviderOption extends ConsumerStatefulWidget { super.key, required this.exchange, required this.estimate, + required this.pair, required this.rateString, this.kycRating, this.loadingString = false, @@ -233,6 +255,7 @@ class _ProviderOption extends ConsumerStatefulWidget { final Exchange exchange; final Estimate? estimate; + final ({AggregateCurrency from, AggregateCurrency to})? pair; final String rateString; final String? kycRating; final bool loadingString; @@ -246,12 +269,59 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { final isDesktop = Util.isDesktop; late final String _id; + final Set _warnings = {}; + + bool _warningLock = false; + void _showNoSparkWarning() async { + if (_warningLock) return; + _warningLock = true; + try { + await showDialog( + context: context, + builder: (context) { + return BasicDialog( + title: _warnings.map((e) => e.message).join(" and "), + message: _warnings.map((e) => e.messageDetail).join(" "), + canPopWithBackButton: true, + flex: true, + desktopHeight: 400, + rightButton: PrimaryButton( + label: "OK", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: Navigator.of(context).pop, + ), + ); + }, + ); + } finally { + _warningLock = false; + } + } @override void initState() { + super.initState(); _id = "${widget.exchange.name} (${widget.estimate?.exchangeProvider ?? widget.exchange.name})"; - super.initState(); + + if (widget.exchange.name == TrocadorExchange.exchangeName && + widget.pair != null) { + final from = widget.pair!.from.forExchange(widget.exchange.name); + final to = widget.pair!.to.forExchange(widget.exchange.name); + + if (from != null) { + final firoWarning = TrocadorExchange.checkFiro(from); + if (firoWarning != null) _warnings.add(firoWarning); + final ltcWarning = TrocadorExchange.checkLtc(from); + if (ltcWarning != null) _warnings.add(ltcWarning); + } + if (to != null) { + final firoWarning = TrocadorExchange.checkFiro(to); + if (firoWarning != null) _warnings.add(firoWarning); + final ltcWarning = TrocadorExchange.checkLtc(to); + if (ltcWarning != null) _warnings.add(ltcWarning); + } + } } @override @@ -265,10 +335,9 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { return ConditionalParent( condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), child: GestureDetector( onTap: () { ref.read(efExchangeProvider.notifier).state = widget.exchange; @@ -289,127 +358,138 @@ class _ProviderOptionState extends ConsumerState<_ProviderOption> { child: Padding( padding: EdgeInsets.only(top: isDesktop ? 20.0 : 15.0), child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of( + context, + ).extension()!.radioButtonIconEnabled, value: _id, groupValue: groupValue, onChanged: (_) { ref.read(efExchangeProvider.notifier).state = widget.exchange; ref - .read(efExchangeProviderNameProvider.notifier) - .state = - widget.estimate?.exchangeProvider ?? - widget.exchange.name; + .read(efExchangeProviderNameProvider.notifier) + .state = widget.estimate?.exchangeProvider ?? + widget.exchange.name; }, ), ), ), - const SizedBox( - width: 14, - ), + const SizedBox(width: 14), Padding( padding: const EdgeInsets.only(top: 5.0), child: SizedBox( width: isDesktop ? 32 : 24, height: isDesktop ? 32 : 24, - child: widget.estimate?.exchangeProviderLogo != null && - widget - .estimate! - .exchangeProviderLogo! - .isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Image.network( - widget.estimate!.exchangeProviderLogo!, - loadingBuilder: ( - context, - child, - loadingProgress, - ) { - if (loadingProgress == null) { - return child; - } else { - return const Center( - child: - CircularProgressIndicator(), - ); - } - }, - errorBuilder: (context, error, stackTrace) { - return SvgPicture.asset( - Assets.exchange.getIconFor( - exchangeName: widget.exchange.name, + child: + widget.estimate?.exchangeProviderLogo != null && + widget + .estimate! + .exchangeProviderLogo! + .isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Image.network( + widget.estimate!.exchangeProviderLogo!, + loadingBuilder: ( + context, + child, + loadingProgress, + ) { + if (loadingProgress == null) { + return child; + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + errorBuilder: (context, error, stackTrace) { + return SvgPicture.asset( + Assets.exchange.getIconFor( + exchangeName: widget.exchange.name, + ), + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ); + }, + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), + ) + : SvgPicture.asset( + Assets.exchange.getIconFor( + exchangeName: widget.exchange.name, + ), + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, ), - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - ); - }, - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - ), - ) - : SvgPicture.asset( - Assets.exchange.getIconFor( - exchangeName: widget.exchange.name, - ), - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.estimate?.exchangeProvider ?? - widget.exchange.name, - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, + ConditionalParent( + condition: _warnings.isNotEmpty, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + CustomTextButton( + text: _warnings.first.value, + onTap: () { + _showNoSparkWarning(); + }, + ), + ], + ), + child: Text( + widget.estimate?.exchangeProvider ?? + widget.exchange.name, + style: STextStyles.titleBold12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark2, + ), ), ), widget.loadingString ? AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: - STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ) + stringsToLoopThrough: const [ + "Loading", + "Loading.", + "Loading..", + "Loading...", + ], + style: STextStyles.itemSubtitle12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ) : Text( - widget.rateString, - style: - STextStyles.itemSubtitle12(context).copyWith( - color: widget.rateColor ?? - Theme.of(context) - .extension()! - .textSubtitle1, - ), + widget.rateString, + style: STextStyles.itemSubtitle12(context).copyWith( + color: + widget.rateColor ?? + Theme.of( + context, + ).extension()!.textSubtitle1, ), + ), ], ), ), if (widget.kycRating != null) TrocadorKYCInfoButton( - kycType: TrocadorKYCType.fromString( - widget.kycRating!, - ), + kycType: TrocadorKYCType.fromString(widget.kycRating!), ), ], ), diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index 1ffa12275..a2ef39305 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -15,7 +15,6 @@ import '../../../models/exchange/aggregate_currency.dart'; import '../../../providers/providers.dart'; import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange.dart'; -import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; import '../../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../../services/exchange/trocador/trocador_exchange.dart'; import '../../../themes/stack_colors.dart'; @@ -70,21 +69,18 @@ class _ExchangeProviderOptionsState @override Widget build(BuildContext context) { - final sendCurrency = - ref.watch(efCurrencyPairProvider.select((value) => value.send)); - final receivingCurrency = - ref.watch(efCurrencyPairProvider.select((value) => value.receive)); + final sendCurrency = ref.watch( + efCurrencyPairProvider.select((value) => value.send), + ); + final receivingCurrency = ref.watch( + efCurrencyPairProvider.select((value) => value.receive), + ); final showChangeNow = exchangeSupported( exchangeName: ChangeNowExchange.exchangeName, sendCurrency: sendCurrency, receiveCurrency: receivingCurrency, ); - final showMajesticBank = exchangeSupported( - exchangeName: MajesticBankExchange.exchangeName, - sendCurrency: sendCurrency, - receiveCurrency: receivingCurrency, - ); final showTrocador = exchangeSupported( exchangeName: TrocadorExchange.exchangeName, sendCurrency: sendCurrency, @@ -98,9 +94,10 @@ class _ExchangeProviderOptionsState return RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), - borderColor: isDesktop - ? Theme.of(context).extension()!.background - : null, + borderColor: + isDesktop + ? Theme.of(context).extension()!.background + : null, child: Column( children: [ if (showChangeNow) @@ -109,49 +106,26 @@ class _ExchangeProviderOptionsState fixedRate: widget.fixedRate, reversed: widget.reversed, ), - if (showChangeNow && showMajesticBank) - isDesktop - ? Container( - height: 1, - color: - Theme.of(context).extension()!.background, - ) - : const SizedBox( - height: 16, - ), - if (showMajesticBank) - ExchangeOption( - exchange: MajesticBankExchange.instance, - fixedRate: widget.fixedRate, - reversed: widget.reversed, - ), - if ((showChangeNow || showMajesticBank) && showTrocador) + if (showChangeNow && showTrocador) isDesktop ? Container( - height: 1, - color: - Theme.of(context).extension()!.background, - ) - : const SizedBox( - height: 16, - ), + height: 1, + color: Theme.of(context).extension()!.background, + ) + : const SizedBox(height: 16), if (showTrocador) ExchangeOption( fixedRate: widget.fixedRate, reversed: widget.reversed, exchange: TrocadorExchange.instance, ), - if ((showChangeNow || showMajesticBank || showTrocador) && - showNanswap) + if ((showChangeNow || showTrocador) && showNanswap) isDesktop ? Container( - height: 1, - color: - Theme.of(context).extension()!.background, - ) - : const SizedBox( - height: 16, - ), + height: 1, + color: Theme.of(context).extension()!.background, + ) + : const SizedBox(height: 16), if (showNanswap) ExchangeOption( fixedRate: widget.fixedRate, diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 0416056a7..6e56e5755 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -20,7 +20,7 @@ import 'package:tuple/tuple.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../app_config.dart'; -import '../../models/exchange/change_now/exchange_transaction_status.dart'; +import '../../models/exchange/change_now/cn_exchange_transaction_status.dart'; import '../../models/isar/models/blockchain_data/transaction.dart'; import '../../models/isar/stack_theme.dart'; import '../../notifications/show_flush_bar.dart'; @@ -29,10 +29,10 @@ import '../../providers/providers.dart'; import '../../route_generator.dart'; import '../../services/exchange/change_now/change_now_exchange.dart'; import '../../services/exchange/exchange.dart'; -import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; import '../../services/exchange/nanswap/nanswap_exchange.dart'; import '../../services/exchange/simpleswap/simpleswap_exchange.dart'; import '../../services/exchange/trocador/trocador_exchange.dart'; +import '../../services/wallets.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; import '../../utilities/amount/amount.dart'; @@ -44,6 +44,8 @@ import '../../utilities/format.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; +import '../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -101,7 +103,7 @@ class _TradeDetailsViewState extends ConsumerState { .trades .firstWhere((e) => e.tradeId == tradeId); - if (mounted) { + if (mounted && trade.exchangeName != "Majestic Bank") { final exchange = Exchange.fromName(trade.exchangeName); final response = await exchange.updateTrade(trade); @@ -153,6 +155,26 @@ class _TradeDetailsViewState extends ConsumerState { } } + bool isWalletCoinAndCanSendWithoutWalletOpened( + String ticker, + Wallets walletsInstance, + ) { + try { + final coin = AppConfig.getCryptoCurrencyForTicker(ticker); + return walletsInstance.wallets + .where( + (e) => + e.info.coin == coin && + (e is! ExternalWallet || + e is MwebInterface), // ltc mweb is external but swaps + // should not use mweb, hence the odd logic check here + ) + .isNotEmpty; + } catch (_) { + return false; + } + } + @override Widget build(BuildContext context) { final bool sentFromStack = @@ -164,7 +186,8 @@ class _TradeDetailsViewState extends ConsumerState { ), ); - final bool hasTx = sentFromStack || + final bool hasTx = + sentFromStack || !(trade.status == "New" || trade.status == "new" || trade.status == "Waiting" || @@ -176,7 +199,8 @@ class _TradeDetailsViewState extends ConsumerState { trade.status == "Expired" || trade.status == "expired" || trade.status == "Failed" || - trade.status == "failed"); + trade.status == "failed" || + trade.status.toLowerCase().startsWith("waiting")); //todo: check if print needed // debugPrint("walletId: $walletId"); @@ -190,10 +214,13 @@ class _TradeDetailsViewState extends ConsumerState { final isDesktop = Util.isDesktop; - final showSendFromStackButton = !hasTx && - !["xmr", "monero", "wow", "wownero"] - .contains(trade.payInCurrency.toLowerCase()) && + final showSendFromStackButton = + !hasTx && AppConfig.isStackCoin(trade.payInCurrency) && + isWalletCoinAndCanSendWithoutWalletOpened( + trade.payInCurrency, + ref.read(pWallets), + ) && (trade.status == "New" || trade.status == "new" || trade.status == "waiting" || @@ -201,132 +228,130 @@ class _TradeDetailsViewState extends ConsumerState { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Trade details", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Trade details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), ), ), ), - ), - ), child: Padding( - padding: isDesktop - ? const EdgeInsets.only(left: 32) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(0), child: BranchedParent( condition: isDesktop, - conditionBranchBuilder: (children) => Padding( - padding: const EdgeInsets.only( - right: 20, - ), - child: Padding( - padding: const EdgeInsets.only( - right: 12, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RoundedWhiteContainer( - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, - padding: const EdgeInsets.all(0), - child: ListView( - primary: false, - shrinkWrap: true, - children: children, - ), - ), - if (showSendFromStackButton) - const SizedBox( - height: 32, - ), - if (showSendFromStackButton) - SecondaryButton( - label: "Send from ${AppConfig.prefix}", - buttonHeight: ButtonHeight.l, - onPressed: () { - CryptoCurrency coin; - try { - coin = AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; - } catch (_) { - coin = AppConfig.getCryptoCurrencyByPrettyName( - trade.payInCurrency, - ); - } - final amount = Amount.fromDecimal( - sendAmount, - fractionDigits: coin.fractionDigits, - ); - final address = trade.payInAddress; + conditionBranchBuilder: + (children) => Padding( + padding: const EdgeInsets.only(right: 20), + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RoundedWhiteContainer( + borderColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + padding: const EdgeInsets.all(0), + child: ListView( + primary: false, + shrinkWrap: true, + children: children, + ), + ), + if (showSendFromStackButton) const SizedBox(height: 32), + if (showSendFromStackButton) + SecondaryButton( + label: "Send from ${AppConfig.prefix}", + buttonHeight: ButtonHeight.l, + onPressed: () { + CryptoCurrency coin; + try { + coin = + AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; + } catch (_) { + coin = AppConfig.getCryptoCurrencyByPrettyName( + trade.payInCurrency, + ); + } + final amount = Amount.fromDecimal( + sendAmount, + fractionDigits: coin.fractionDigits, + ); + final address = trade.payInAddress; - Navigator.of(context).pushNamed( - SendFromView.routeName, - arguments: Tuple4( - coin, - amount, - address, - trade, - ), - ); - }, - ), - const SizedBox( - height: 32, + Navigator.of(context).pushNamed( + SendFromView.routeName, + arguments: Tuple4(coin, amount, address, trade), + ); + }, + ), + const SizedBox(height: 32), + ], ), - ], + ), + ), + otherBranchBuilder: + (children) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: children, ), - ), - ), - otherBranchBuilder: (children) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, - children: children, - ), children: [ RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), child: Container( - decoration: isDesktop - ? BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + decoration: + isDesktop + ? BoxDecoration( + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - ) - : null, + ) + : null, child: Padding( - padding: isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -343,9 +368,7 @@ class _TradeDetailsViewState extends ConsumerState { width: 32, height: 32, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SelectableText( "Swap service", style: STextStyles.desktopTextMedium(context), @@ -353,25 +376,24 @@ class _TradeDetailsViewState extends ConsumerState { ], ), Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, + crossAxisAlignment: + isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ SelectableText( "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", style: STextStyles.titleBold12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Builder( builder: (context) { String text; try { final coin = AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; + trade.payInCurrency, + )!; final amount = sendAmount.toAmount( fractionDigits: coin.fractionDigits, ); @@ -419,15 +441,12 @@ class _TradeDetailsViewState extends ConsumerState { ), ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -435,151 +454,117 @@ class _TradeDetailsViewState extends ConsumerState { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), - if (trade.exchangeName == - MajesticBankExchange.exchangeName && - trade.status == "Completed") - Row( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) => const StackOkDialog( - title: "Trade Info", - message: - "Majestic Bank does not store order data indefinitely", - ), - ); - }, - child: SvgPicture.asset( - Assets.svg.circleInfo, - height: 20, - width: 20, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - ), - ], - ), + Text("Status", style: STextStyles.itemSubtitle(context)), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( trade.status, style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .colorForStatus(trade.status), + color: Theme.of( + context, + ).extension()!.colorForStatus(trade.status), ), ), ], ), ), if (!sentFromStack && !hasTx) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (!sentFromStack && !hasTx) RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: isDesktop - ? Theme.of(context).extension()!.popupBG - : Theme.of(context) - .extension()! - .warningBackground, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + isDesktop + ? Theme.of(context).extension()!.popupBG + : Theme.of( + context, + ).extension()!.warningBackground, child: ConditionalParent( condition: isDesktop, - builder: (child) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + builder: + (child) => Column( + mainAxisSize: MainAxisSize.min, children: [ - Column( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Amount", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - const SizedBox( - height: 2, - ), - Text( - "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Amount", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + Text( + "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), + ), + ], ), + IconCopyButton(data: trade.payInAmount), ], ), - IconCopyButton( - data: trade.payInAmount, - ), + const SizedBox(height: 6), + child, ], ), - const SizedBox( - height: 6, - ), - child, - ], - ), child: RichText( text: TextSpan( text: - "You must send at least ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}. ", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorRed, - ) - : STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, - ), + "You must send at least ${sendAmount.toStringAsFixed(trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}. ", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorRed, + ) + : STextStyles.label(context).copyWith( + color: + Theme.of(context) + .extension()! + .warningForeground, + ), children: [ TextSpan( text: - "If you send less than ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .accentColorRed, - ) - : STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, - ), + "If you send less than ${sendAmount.toStringAsFixed(trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorRed, + ) + : STextStyles.label(context).copyWith( + color: + Theme.of(context) + .extension()! + .warningForeground, + ), ), ], ), @@ -587,39 +572,30 @@ class _TradeDetailsViewState extends ConsumerState { ), ), if (sentFromStack) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (sentFromStack) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Sent from", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), + Text("Sent from", style: STextStyles.itemSubtitle(context)), + const SizedBox(height: 4), SelectableText( widget.walletName!, style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), CustomTextButton( text: "View transaction", onTap: () { - final coin = AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; + final coin = + AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; if (isDesktop) { Navigator.of(context).push( @@ -655,16 +631,13 @@ class _TradeDetailsViewState extends ConsumerState { ), ), if (sentFromStack) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (sentFromStack) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -677,9 +650,7 @@ class _TradeDetailsViewState extends ConsumerState { "${trade.exchangeName} address", style: STextStyles.itemSubtitle(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( children: [ Flexible( @@ -693,24 +664,18 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - if (isDesktop) - IconCopyButton( - data: trade.payInAddress, - ), + if (isDesktop) IconCopyButton(data: trade.payInAddress), ], ), ), if (!sentFromStack && !hasTx) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (!sentFromStack && !hasTx) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -722,52 +687,45 @@ class _TradeDetailsViewState extends ConsumerState { style: STextStyles.itemSubtitle(context), ), isDesktop - ? IconCopyButton( - data: trade.payInAddress, - ) + ? IconCopyButton(data: trade.payInAddress) : GestureDetector( - onTap: () async { - final address = trade.payInAddress; - await Clipboard.setData( - ClipboardData( - text: address, + onTap: () async { + final address = trade.payInAddress; + await Clipboard.setData( + ClipboardData(text: address), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, ), ); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 12, - height: 12, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 12, + height: 12, + color: + Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], ), + ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( children: [ Expanded( @@ -778,9 +736,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), GestureDetector( onTap: () { showDialog( @@ -799,9 +755,7 @@ class _TradeDetailsViewState extends ConsumerState { style: STextStyles.pageTitleH2(context), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Center( child: RepaintBoundary( // key: _qrKey, @@ -815,9 +769,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Center( child: SizedBox( width: width, @@ -833,11 +785,13 @@ class _TradeDetailsViewState extends ConsumerState { ), child: Text( "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -855,13 +809,12 @@ class _TradeDetailsViewState extends ConsumerState { Assets.svg.qrcode, width: 12, height: 12, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), + const SizedBox(width: 4), Text( "Show QR code", style: STextStyles.link2(context), @@ -872,73 +825,60 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Memo", - style: STextStyles.itemSubtitle(context), - ), + Text("Memo", style: STextStyles.itemSubtitle(context)), isDesktop - ? IconCopyButton( - data: trade.payInExtraId, - ) + ? IconCopyButton(data: trade.payInExtraId) : GestureDetector( - onTap: () async { - final address = trade.payInExtraId; - await Clipboard.setData( - ClipboardData( - text: address, + onTap: () async { + final address = trade.payInExtraId; + await Clipboard.setData( + ClipboardData(text: address), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, ), ); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 12, - height: 12, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 12, + height: 12, + color: + Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], ), + ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( trade.payInExtraId, style: STextStyles.itemSubtitle12(context), @@ -947,15 +887,12 @@ class _TradeDetailsViewState extends ConsumerState { ), ), if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -968,6 +905,86 @@ class _TradeDetailsViewState extends ConsumerState { ), isDesktop ? IconPencilButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditTradeNoteView( + tradeId: tradeId, + note: ref + .read(tradeNoteServiceProvider) + .getNote(tradeId: tradeId), + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditTradeNoteView.routeName, + arguments: Tuple2( + tradeId, + ref + .read(tradeNoteServiceProvider) + .getNote(tradeId: tradeId), + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, + ), + const SizedBox(width: 4), + Text("Edit", style: STextStyles.link2(context)), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + SelectableText( + ref.watch( + tradeNoteServiceProvider.select( + (value) => value.getNote(tradeId: tradeId), + ), + ), + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (sentFromStack) + isDesktop ? const _Divider() : const SizedBox(height: 12), + if (sentFromStack) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction note", + style: STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconPencilButton( onPressed: () { showDialog( context: context, @@ -975,26 +992,22 @@ class _TradeDetailsViewState extends ConsumerState { return DesktopDialog( maxWidth: 580, maxHeight: 360, - child: EditTradeNoteView( - tradeId: tradeId, - note: ref - .read(tradeNoteServiceProvider) - .getNote(tradeId: tradeId), + child: EditNoteView( + txid: transactionIfSentFromStack!.txid, + walletId: walletId!, ), ); }, ); }, ) - : GestureDetector( + : GestureDetector( onTap: () { Navigator.of(context).pushNamed( - EditTradeNoteView.routeName, + EditNoteView.routeName, arguments: Tuple2( - tradeId, - ref - .read(tradeNoteServiceProvider) - .getNote(tradeId: tradeId), + transactionIfSentFromStack!.txid, + walletId, ), ); }, @@ -1004,13 +1017,12 @@ class _TradeDetailsViewState extends ConsumerState { Assets.svg.pencil, width: 10, height: 10, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, + color: + Theme.of(context) + .extension()! + .infoItemIcons, ), + const SizedBox(width: 4), Text( "Edit", style: STextStyles.link2(context), @@ -1018,105 +1030,16 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - ], - ), - const SizedBox( - height: 4, - ), - SelectableText( - ref.watch( - tradeNoteServiceProvider - .select((value) => value.getNote(tradeId: tradeId)), - ), - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - if (sentFromStack) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (sentFromStack) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction note", - style: STextStyles.itemSubtitle(context), - ), - isDesktop - ? IconPencilButton( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 360, - child: EditNoteView( - txid: - transactionIfSentFromStack!.txid, - walletId: walletId!, - ), - ); - }, - ); - }, - ) - : GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple2( - transactionIfSentFromStack!.txid, - walletId, - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2(context), - ), - ], - ), - ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( ref .watch( - pTransactionNote( - ( - txid: transactionIfSentFromStack!.txid, - walletId: walletId!, - ), - ), + pTransactionNote(( + txid: transactionIfSentFromStack!.txid, + walletId: walletId!, + )), ) ?.value ?? "", @@ -1125,15 +1048,12 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1142,24 +1062,20 @@ class _TradeDetailsViewState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Date", - style: STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), + Text("Date", style: STextStyles.itemSubtitle(context)), + if (isDesktop) const SizedBox(height: 2), if (isDesktop) SelectableText( Format.extractDateFrom( trade.timestamp.millisecondsSinceEpoch ~/ 1000, ), - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -1180,15 +1096,12 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1200,10 +1113,7 @@ class _TradeDetailsViewState extends ConsumerState { "Swap service", style: STextStyles.itemSubtitle(context), ), - if (isDesktop) - const SizedBox( - height: 2, - ), + if (isDesktop) const SizedBox(height: 2), if (isDesktop) SelectableText( trade.exchangeName, @@ -1211,10 +1121,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - if (isDesktop) - IconCopyButton( - data: trade.exchangeName, - ), + if (isDesktop) IconCopyButton(data: trade.exchangeName), if (!isDesktop) SelectableText( trade.exchangeName, @@ -1223,15 +1130,12 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1243,10 +1147,7 @@ class _TradeDetailsViewState extends ConsumerState { "Trade ID", style: STextStyles.itemSubtitle(context), ), - if (isDesktop) - const SizedBox( - height: 2, - ), + if (isDesktop) const SizedBox(height: 2), if (isDesktop) Text( trade.tradeId, @@ -1254,10 +1155,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - if (isDesktop) - IconCopyButton( - data: trade.tradeId, - ), + if (isDesktop) IconCopyButton(data: trade.tradeId), if (!isDesktop) Row( children: [ @@ -1265,9 +1163,7 @@ class _TradeDetailsViewState extends ConsumerState { trade.tradeId, style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), GestureDetector( onTap: () async { final data = ClipboardData(text: trade.tradeId); @@ -1284,9 +1180,10 @@ class _TradeDetailsViewState extends ConsumerState { }, child: SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, width: 12, ), ), @@ -1295,90 +1192,77 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Tracking", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), - Builder( - builder: (context) { - late final String url; - switch (trade.exchangeName) { - case ChangeNowExchange.exchangeName: - url = - "https://changenow.io/exchange/txs/${trade.tradeId}"; - break; - case SimpleSwapExchange.exchangeName: - url = - "https://simpleswap.io/exchange?id=${trade.tradeId}"; - break; - case MajesticBankExchange.exchangeName: - url = - "https://majesticbank.sc/track?trx=${trade.tradeId}"; - break; - case NanswapExchange.exchangeName: - url = - "https://nanswap.com/transaction/${trade.tradeId}"; - break; - - default: - if (trade.exchangeName - .startsWith(TrocadorExchange.exchangeName)) { + if (trade.exchangeName != "Majestic Bank") + isDesktop ? const _Divider() : const SizedBox(height: 12), + if (trade.exchangeName != "Majestic Bank") + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Tracking", style: STextStyles.itemSubtitle(context)), + const SizedBox(height: 4), + Builder( + builder: (context) { + late final String url; + switch (trade.exchangeName) { + case ChangeNowExchange.exchangeName: url = - "https://trocador.app/en/checkout/${trade.tradeId}"; - } - } - return ConditionalParent( - condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), - child: GestureDetector( - onTap: () { - launchUrl( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ); - }, - child: Text( - url, - style: STextStyles.link2(context), + "https://changenow.io/exchange/txs/${trade.tradeId}"; + break; + case SimpleSwapExchange.exchangeName: + url = + "https://simpleswap.io/exchange?id=${trade.tradeId}"; + break; + case NanswapExchange.exchangeName: + url = + "https://nanswap.com/transaction/${trade.tradeId}"; + break; + + default: + if (trade.exchangeName.startsWith( + TrocadorExchange.exchangeName, + )) { + url = + "https://trocador.app/en/checkout/${trade.tradeId}"; + } + } + return ConditionalParent( + condition: isDesktop, + builder: + (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: GestureDetector( + onTap: () { + launchUrl( + Uri.parse(url), + mode: LaunchMode.externalApplication, + ); + }, + child: Text(url, style: STextStyles.link2(context)), ), - ), - ); - }, - ), - ], - ), - ), - if (!isDesktop) - const SizedBox( - height: 12, + ); + }, + ), + ], + ), ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop && showSendFromStackButton) SecondaryButton( label: "Send from ${AppConfig.prefix}", onPressed: () { CryptoCurrency coin; try { - coin = AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; + coin = + AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; } catch (_) { coin = AppConfig.getCryptoCurrencyByPrettyName( trade.payInCurrency, @@ -1392,12 +1276,7 @@ class _TradeDetailsViewState extends ConsumerState { Navigator.of(context).pushNamed( SendFromView.routeName, - arguments: Tuple4( - coin, - amount, - address, - trade, - ), + arguments: Tuple4(coin, amount, address, trade), ); }, ), diff --git a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart index bfdb0e647..a2ffe26db 100644 --- a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart +++ b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart @@ -12,9 +12,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../models/isar/models/isar_models.dart'; -import 'exchange_form.dart'; -import 'sub_widgets/step_row.dart'; import '../../providers/exchange/exchange_form_state_provider.dart'; import '../../providers/global/prefs_provider.dart'; import '../../services/exchange/exchange_data_loading_service.dart'; @@ -25,6 +24,8 @@ import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_loading_overlay.dart'; +import 'exchange_form.dart'; +import 'sub_widgets/step_row.dart'; class WalletInitiatedExchangeView extends ConsumerStatefulWidget { const WalletInitiatedExchangeView({ @@ -110,10 +111,9 @@ class _WalletInitiatedExchangeViewState children: [ child, Material( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), child: const CustomLoadingOverlay( message: "Updating exchange data", subMessage: "This could take a few minutes", @@ -139,62 +139,51 @@ class _WalletInitiatedExchangeViewState } }, ), - title: Text( - "Swap", - style: STextStyles.navBarTitle(context), - ), + title: Text("Swap", style: STextStyles.navBarTitle(context)), ), - body: LayoutBuilder( - builder: (context, constraints) { - final width = MediaQuery.of(context).size.width - 32; - return Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - StepRow( - count: 4, - current: 0, - width: width, - ), - const SizedBox( - height: 14, - ), - Text( - "Exchange amount", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), - Text( - "Network fees and other exchange charges are included in the rate.", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 24, - ), - ExchangeForm( - walletId: walletId, - coin: coin, - contract: widget.contract, - ), - ], + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final width = MediaQuery.of(context).size.width - 32; + return Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + StepRow(count: 4, current: 0, width: width), + const SizedBox(height: 14), + Text( + "Exchange amount", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox(height: 8), + Text( + "Network fees and other exchange charges are included in the rate.", + style: STextStyles.itemSubtitle(context), + ), + const SizedBox(height: 24), + ExchangeForm( + walletId: walletId, + coin: coin, + contract: widget.contract, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/generic/single_field_edit_view.dart b/lib/pages/generic/single_field_edit_view.dart index 5ffb7196f..ee8ac8b12 100644 --- a/lib/pages/generic/single_field_edit_view.dart +++ b/lib/pages/generic/single_field_edit_view.dart @@ -65,61 +65,58 @@ class _SingleFieldEditViewState extends State { Widget build(BuildContext context) { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Edit ${widget.label}", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, - ), + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit ${widget.label}", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), + ); + }, ), - ); - }, + ), + ), ), ), - ), - ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!isDesktop) - const SizedBox( - height: 10, - ), + if (!isDesktop) const SizedBox(height: 10), if (isDesktop) Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 12, - ), + padding: const EdgeInsets.only(left: 32, bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -132,11 +129,10 @@ class _SingleFieldEditViewState extends State { ), ), Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -145,14 +141,16 @@ class _SingleFieldEditViewState extends State { autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, controller: _textController, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), focusNode: _textFocusNode, decoration: standardInputDecoration( widget.label.capitalize(), @@ -160,33 +158,35 @@ class _SingleFieldEditViewState extends State { context, desktopMed: isDesktop, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, - ) - : null, - suffixIcon: _textController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _textController.text = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: + _textController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _textController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), @@ -196,30 +196,27 @@ class _SingleFieldEditViewState extends State { ConditionalParent( condition: isDesktop, - builder: (child) => Padding( - padding: const EdgeInsets.all(32), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: () { - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), + builder: + (child) => Padding( + padding: const EdgeInsets.all(32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox(width: 16), + Expanded(child: child), + ], ), - const SizedBox( - width: 16, - ), - Expanded( - child: child, - ), - ], - ), - ), + ), child: PrimaryButton( label: "Save", buttonHeight: isDesktop ? ButtonHeight.l : null, @@ -230,10 +227,7 @@ class _SingleFieldEditViewState extends State { }, ), ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), ], ), ); diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 7619a5d34..126a16bfb 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -20,11 +20,14 @@ import '../../providers/global/notifications_provider.dart'; import '../../providers/global/prefs_provider.dart'; import '../../providers/ui/home_view_index_provider.dart'; import '../../providers/ui/unread_notifications_provider.dart'; +import '../../route_generator.dart'; import '../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; +import '../../utilities/idle_monitor.dart'; +import '../../utilities/prefs.dart'; import '../../utilities/text_styles.dart'; import '../../widgets/animated_widgets/rotate_icon.dart'; import '../../widgets/app_icon.dart'; @@ -35,6 +38,7 @@ import '../../widgets/stack_dialog.dart'; import '../buy_view/buy_view.dart'; import '../exchange_view/exchange_view.dart'; import '../notification_views/notifications_view.dart'; +import '../pinpad_views/lock_screen_view.dart'; import '../settings_views/global_settings_view/global_settings_view.dart'; import '../settings_views/global_settings_view/hidden_settings.dart'; import '../wallets_view/wallets_view.dart'; @@ -63,6 +67,51 @@ class _HomeViewState extends ConsumerState { late TorConnectionStatus _currentSyncStatus; + IdleMonitor? _idleMonitor; + + void _onIdle() async { + final context = _key.currentContext; + if (context != null) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: false, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to unlock ${AppConfig.appName}", + biometricsAuthenticationTitle: "Unlock ${AppConfig.appName}", + ), + settings: const RouteSettings(name: "/unlockTimedOutAppScreen"), + ), + ); + } + } + + late AutoLockInfo _autoLockInfo; + void _prefsTimeoutListener() { + final prefs = ref.read(prefsChangeNotifierProvider); + if (mounted && prefs.autoLockInfo != _autoLockInfo) { + _autoLockInfo = prefs.autoLockInfo; + if (_autoLockInfo.enabled) { + _idleMonitor?.detach(); + _idleMonitor = IdleMonitor( + timeout: Duration(minutes: _autoLockInfo.minutes), + onIdle: _onIdle, + ); + _idleMonitor!.attach(); + } else { + _idleMonitor?.detach(); + _idleMonitor = null; + } + } + } + // final _buyDataLoadingService = BuyDataLoadingService(); Future _onWillPop() async { @@ -83,13 +132,14 @@ class _HomeViewState extends ConsumerState { await showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async { - _exitEnabled = true; - return true; - }, - child: const StackDialog(title: "Tap back again to exit"), - ), + builder: + (_) => WillPopScope( + onWillPop: () async { + _exitEnabled = true; + return true; + }, + child: const StackDialog(title: "Tap back again to exit"), + ), ).timeout( timeout, onTimeout: () { @@ -123,6 +173,14 @@ class _HomeViewState extends ConsumerState { @override void initState() { + _autoLockInfo = ref.read(prefsChangeNotifierProvider).autoLockInfo; + if (_autoLockInfo.enabled) { + _idleMonitor = IdleMonitor( + timeout: Duration(minutes: _autoLockInfo.minutes), + onIdle: _onIdle, + ); + } + _pageController = PageController(); _rotateIconController = RotateIconController(); _children = [ @@ -139,11 +197,17 @@ class _HomeViewState extends ConsumerState { // showOneTimeTorHasBeenAddedDialogIfRequired(context); // }); + _idleMonitor?.attach(); + + ref.read(prefsChangeNotifierProvider).addListener(_prefsTimeoutListener); + super.initState(); } @override dispose() { + ref.read(prefsChangeNotifierProvider).removeListener(_prefsTimeoutListener); + _idleMonitor?.detach(); _pageController.dispose(); _rotateIconController.forward = null; _rotateIconController.reverse = null; @@ -176,16 +240,17 @@ class _HomeViewState extends ConsumerState { // dirty hack ref.listen( - prefsChangeNotifierProvider.select((value) => value.enableExchange), - (prev, next) { - if (next == false && - mounted && - ref.read(homeViewPageIndexStateProvider) != 0) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => ref.read(homeViewPageIndexStateProvider.state).state = 0, - ); - } - }); + prefsChangeNotifierProvider.select((value) => value.enableExchange), + (prev, next) { + if (next == false && + mounted && + ref.read(homeViewPageIndexStateProvider) != 0) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(homeViewPageIndexStateProvider.state).state = 0, + ); + } + }, + ); return WillPopScope( onWillPop: _onWillPop, @@ -202,18 +267,13 @@ class _HomeViewState extends ConsumerState { GestureDetector( onTap: _hiddenOptions, child: RotateIcon( - icon: const AppIcon( - width: 24, - height: 24, - ), + icon: const AppIcon(width: 24, height: 24), curve: Curves.easeInOutCubic, rotationPercent: 1.0, controller: _rotateIconController, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Text( "My ${AppConfig.prefix}", style: STextStyles.navBarTitle(context), @@ -222,22 +282,11 @@ class _HomeViewState extends ConsumerState { ), actions: [ const Padding( - padding: EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: SmallTorIcon(), - ), + padding: EdgeInsets.only(top: 10, bottom: 10, right: 10), + child: AspectRatio(aspectRatio: 1, child: SmallTorIcon()), ), Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), + padding: const EdgeInsets.only(top: 10, bottom: 10, right: 10), child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( @@ -246,65 +295,77 @@ class _HomeViewState extends ConsumerState { key: const Key("walletsViewAlertsButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .backgroundAppBar, - icon: ref.watch( - notificationsProvider - .select((value) => value.hasUnreadNotifications), - ) - ? SvgPicture.file( - File( - ref.watch( - themeProvider.select( - (value) => value.assets.bellNew, - ), - ), - ), - width: 20, - height: 20, - color: ref.watch( + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, + icon: + ref.watch( notificationsProvider.select( (value) => value.hasUnreadNotifications, ), ) - ? null - : Theme.of(context) - .extension()! - .topNavIconPrimary, - ) - : SvgPicture.asset( - Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch( - notificationsProvider.select( - (value) => value.hasUnreadNotifications, + ? SvgPicture.file( + File( + ref.watch( + themeProvider.select( + (value) => value.assets.bellNew, + ), + ), ), + width: 20, + height: 20, + color: + ref.watch( + notificationsProvider.select( + (value) => + value.hasUnreadNotifications, + ), + ) + ? null + : Theme.of(context) + .extension()! + .topNavIconPrimary, ) - ? null - : Theme.of(context) - .extension()! - .topNavIconPrimary, - ), + : SvgPicture.asset( + Assets.svg.bell, + width: 20, + height: 20, + color: + ref.watch( + notificationsProvider.select( + (value) => + value.hasUnreadNotifications, + ), + ) + ? null + : Theme.of(context) + .extension()! + .topNavIconPrimary, + ), onPressed: () { // reset unread state ref.refresh(unreadNotificationsStateProvider); - Navigator.of(context) - .pushNamed(NotificationsView.routeName) - .then((_) { - final Set unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; + Navigator.of( + context, + ).pushNamed(NotificationsView.routeName).then((_) { + final Set unreadNotificationIds = + ref + .read(unreadNotificationsStateProvider.state) + .state; if (unreadNotificationIds.isEmpty) return; final List> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { + for ( + int i = 0; + i < unreadNotificationIds.length - 1; + i++ + ) { futures.add( - ref.read(notificationsProvider).markAsRead( + ref + .read(notificationsProvider) + .markAsRead( unreadNotificationIds.elementAt(i), false, ), @@ -324,11 +385,7 @@ class _HomeViewState extends ConsumerState { ), ), Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), + padding: const EdgeInsets.only(top: 10, bottom: 10, right: 10), child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( @@ -336,89 +393,99 @@ class _HomeViewState extends ConsumerState { key: const Key("walletsViewSettingsButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .backgroundAppBar, + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, icon: SvgPicture.asset( Assets.svg.gear, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, width: 20, height: 20, ), onPressed: () { //todo: check if print needed // debugPrint("main view settings tapped"); - Navigator.of(context) - .pushNamed(GlobalSettingsView.routeName); + Navigator.of( + context, + ).pushNamed(GlobalSettingsView.routeName); }, ), ), ), ], ), - body: Column( - children: [ - if (_children.length > 1 && - ref.watch(prefsChangeNotifierProvider).enableExchange) - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - boxShadow: Theme.of(context) - .extension()! - .homeViewButtonBarBoxShadow != - null - ? [ - Theme.of(context) - .extension()! - .homeViewButtonBarBoxShadow!, - ] - : null, - ), - child: const Padding( - padding: EdgeInsets.only( - left: 16, - bottom: 12, - right: 16, - top: 0, + body: SafeArea( + child: Column( + children: [ + if (_children.length > 1 && + ref.watch(prefsChangeNotifierProvider).enableExchange) + Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, + boxShadow: + Theme.of(context) + .extension()! + .homeViewButtonBarBoxShadow != + null + ? [ + Theme.of(context) + .extension()! + .homeViewButtonBarBoxShadow!, + ] + : null, + ), + child: const Padding( + padding: EdgeInsets.only( + left: 16, + bottom: 12, + right: 16, + top: 0, + ), + child: HomeViewButtonBar(), ), - child: HomeViewButtonBar(), ), - ), - Expanded( - child: Consumer( - builder: (_, _ref, __) { - _ref.listen(homeViewPageIndexStateProvider, - (previous, next) { - if (next is int && next >= 0 && next <= 2) { - // if (next == 1) { - // _exchangeDataLoadingService.loadAll(ref); - // } - // if (next == 2) { - // _buyDataLoadingService.loadAll(ref); - // } - - _lock = true; - _animateToPage(next).then((value) => _lock = false); - } - }); - return PageView( - controller: _pageController, - children: _children, - onPageChanged: (pageIndex) { - if (!_lock) { - ref.read(homeViewPageIndexStateProvider.state).state = - pageIndex; + Expanded( + child: Consumer( + builder: (_, _ref, __) { + _ref.listen(homeViewPageIndexStateProvider, ( + previous, + next, + ) { + if (next is int && next >= 0 && next <= 2) { + // if (next == 1) { + // _exchangeDataLoadingService.loadAll(ref); + // } + // if (next == 2) { + // _buyDataLoadingService.loadAll(ref); + // } + + _lock = true; + _animateToPage(next).then((value) => _lock = false); } - }, - ); - }, + }); + return PageView( + controller: _pageController, + children: _children, + onPageChanged: (pageIndex) { + if (!_lock) { + ref + .read(homeViewPageIndexStateProvider.state) + .state = pageIndex; + } + }, + ); + }, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/intro_view.dart b/lib/pages/intro_view.dart index 7a97eb4a0..ef84551d4 100644 --- a/lib/pages/intro_view.dart +++ b/lib/pages/intro_view.dart @@ -49,145 +49,99 @@ class _IntroViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType "); - final stack = - ref.watch(themeProvider.select((value) => value.assets.stack)); + final stack = ref.watch( + themeProvider.select((value) => value.assets.stack), + ); return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, - body: Center( - child: !isDesktop - ? Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer( - flex: 2, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 300, + body: SafeArea( + child: Center( + child: + !isDesktop + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(flex: 2), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: SizedBox( + width: isDesktop ? 324 : 266, + height: isDesktop ? 324 : 266, + child: + (stack.endsWith(".png")) + ? Image.file(File(stack)) + : SvgPicture.file( + File(stack), + width: isDesktop ? 324 : 266, + height: isDesktop ? 324 : 266, + ), + ), + ), ), - child: SizedBox( - width: isDesktop ? 324 : 266, - height: isDesktop ? 324 : 266, - child: (stack.endsWith(".png")) - ? Image.file( - File( - stack, - ), - ) - : SvgPicture.file( - File( - stack, - ), - width: isDesktop ? 324 : 266, - height: isDesktop ? 324 : 266, - ), + const Spacer(flex: 1), + AppNameText(isDesktop: isDesktop), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 48), + child: IntroAboutText(isDesktop: isDesktop), ), - ), - ), - const Spacer( - flex: 1, - ), - AppNameText( - isDesktop: isDesktop, - ), - const SizedBox( - height: 8, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 48, - ), - child: IntroAboutText( - isDesktop: isDesktop, - ), - ), - const Spacer( - flex: 4, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: PrivacyAndTOSText( - isDesktop: isDesktop, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Row( + const Spacer(flex: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: PrivacyAndTOSText(isDesktop: isDesktop), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + child: GetStartedButton(isDesktop: isDesktop), + ), + ], + ), + ), + ], + ) + : SizedBox( + width: 350, + height: 540, + child: Column( children: [ - Expanded( - child: GetStartedButton( - isDesktop: isDesktop, - ), + const Spacer(flex: 2), + const SizedBox( + width: 130, + height: 130, + child: AppIcon(), ), + const Spacer(flex: 42), + AppNameText(isDesktop: isDesktop), + const Spacer(flex: 24), + IntroAboutText(isDesktop: isDesktop), + const Spacer(flex: 42), + GetStartedButton(isDesktop: isDesktop), + if (isDesktop) const SizedBox(height: 20), + if (isDesktop) + SecondaryButton( + label: "Restore from ${AppConfig.prefix} backup", + onPressed: () { + Navigator.of(context).pushNamed( + CreatePasswordView.routeName, + arguments: true, + ); + }, + ), + const Spacer(flex: 65), + PrivacyAndTOSText(isDesktop: isDesktop), ], ), ), - ], - ) - : SizedBox( - width: 350, - height: 540, - child: Column( - children: [ - const Spacer( - flex: 2, - ), - const SizedBox( - width: 130, - height: 130, - child: AppIcon(), - ), - const Spacer( - flex: 42, - ), - AppNameText( - isDesktop: isDesktop, - ), - const Spacer( - flex: 24, - ), - IntroAboutText( - isDesktop: isDesktop, - ), - const Spacer( - flex: 42, - ), - GetStartedButton( - isDesktop: isDesktop, - ), - if (isDesktop) - const SizedBox( - height: 20, - ), - if (isDesktop) - SecondaryButton( - label: "Restore from ${AppConfig.prefix} backup", - onPressed: () { - Navigator.of(context).pushNamed( - CreatePasswordView.routeName, - arguments: true, - ); - }, - ), - const Spacer( - flex: 65, - ), - PrivacyAndTOSText( - isDesktop: isDesktop, - ), - ], - ), - ), + ), ), ), ); @@ -204,11 +158,10 @@ class AppNameText extends StatelessWidget { return Text( AppConfig.appName, textAlign: TextAlign.center, - style: !isDesktop - ? STextStyles.pageTitleH1(context) - : STextStyles.pageTitleH1(context).copyWith( - fontSize: 40, - ), + style: + !isDesktop + ? STextStyles.pageTitleH1(context) + : STextStyles.pageTitleH1(context).copyWith(fontSize: 40), ); } } @@ -223,11 +176,10 @@ class IntroAboutText extends StatelessWidget { return Text( AppConfig.shortDescriptionText, textAlign: TextAlign.center, - style: !isDesktop - ? STextStyles.subtitle(context) - : STextStyles.subtitle(context).copyWith( - fontSize: 24, - ), + style: + !isDesktop + ? STextStyles.subtitle(context) + : STextStyles.subtitle(context).copyWith(fontSize: 24), ); } } @@ -246,29 +198,34 @@ class PrivacyAndTOSText extends StatelessWidget { style: STextStyles.label(context).copyWith(fontSize: fontSize), children: [ const TextSpan( - text: "By using ${AppConfig.appName}, you agree to the "), + text: "By using ${AppConfig.appName}, you agree to the ", + ), TextSpan( text: "Terms of service", style: STextStyles.richLink(context).copyWith(fontSize: fontSize), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com/terms-of-service.html"), - mode: LaunchMode.externalApplication, - ); - }, + recognizer: + TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html", + ), + mode: LaunchMode.externalApplication, + ); + }, ), const TextSpan(text: " and "), TextSpan( text: "Privacy policy", style: STextStyles.richLink(context).copyWith(fontSize: fontSize), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com/privacy-policy.html"), - mode: LaunchMode.externalApplication, - ); - }, + recognizer: + TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse("https://stackwallet.com/privacy-policy.html"), + mode: LaunchMode.externalApplication, + ); + }, ), ], ), @@ -285,39 +242,34 @@ class GetStartedButton extends StatelessWidget { Widget build(BuildContext context) { return !isDesktop ? TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), + onPressed: () { + Prefs.instance.externalCalls = true; + Navigator.of( + context, + ).pushNamed(StackPrivacyCalls.routeName, arguments: false); + }, + child: Text("Get started", style: STextStyles.button(context)), + ) + : SizedBox( + width: double.infinity, + height: 70, + child: TextButton( + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), onPressed: () { - Prefs.instance.externalCalls = true; - Navigator.of(context).pushNamed( - StackPrivacyCalls.routeName, - arguments: false, - ); + Navigator.of( + context, + ).pushNamed(StackPrivacyCalls.routeName, arguments: false); }, child: Text( - "Get started", - style: STextStyles.button(context), + "Create new ${AppConfig.prefix}", + style: STextStyles.button(context).copyWith(fontSize: 20), ), - ) - : SizedBox( - width: double.infinity, - height: 70, - child: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.of(context).pushNamed( - StackPrivacyCalls.routeName, - arguments: false, - ); - }, - child: Text( - "Create new ${AppConfig.prefix}", - style: STextStyles.button(context).copyWith(fontSize: 20), - ), - ), - ); + ), + ); } } diff --git a/lib/pages/loading_view.dart b/lib/pages/loading_view.dart index e347860ec..c3ec1ffa1 100644 --- a/lib/pages/loading_view.dart +++ b/lib/pages/loading_view.dart @@ -14,6 +14,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lottie/lottie.dart'; + import '../themes/stack_colors.dart'; import '../themes/theme_providers.dart'; import '../utilities/assets.dart'; @@ -36,40 +37,41 @@ class LoadingView extends ConsumerWidget { return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, - body: Container( - color: Theme.of(context).extension()!.background, - child: Center( - child: ConditionalParent( - condition: Theme.of(context).extension()!.themeId == - "oled_black", - builder: (child) => RoundedContainer( - color: const Color(0xFFDEDEDE), - radiusMultiplier: 100, - width: width * 1.35, - height: width * 1.35, - child: child, - ), - child: SizedBox( - width: width, - child: assetPath != null - ? Image.file( - File( - assetPath, - ), - ) - : Lottie.asset( - Assets.lottie.test2, - animate: true, - repeat: true, - ), + body: SafeArea( + child: Container( + color: Theme.of(context).extension()!.background, + child: Center( + child: ConditionalParent( + condition: + Theme.of(context).extension()!.themeId == + "oled_black", + builder: + (child) => RoundedContainer( + color: const Color(0xFFDEDEDE), + radiusMultiplier: 100, + width: width * 1.35, + height: width * 1.35, + child: child, + ), + child: SizedBox( + width: width, + child: + assetPath != null + ? Image.file(File(assetPath)) + : Lottie.asset( + Assets.lottie.test2, + animate: true, + repeat: true, + ), + ), ), + // child: Image( + // image: AssetImage( + // Assets.png.splash, + // ), + // width: MediaQuery.of(context).size.width * 0.5, + // ), ), - // child: Image( - // image: AssetImage( - // Assets.png.splash, - // ), - // width: MediaQuery.of(context).size.width * 0.5, - // ), ), ), ), diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index f8f9d3f4d..49329cc98 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -4,8 +4,8 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../notifications/show_flush_bar.dart'; import '../../providers/global/wallets_provider.dart'; @@ -14,6 +14,7 @@ import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; import '../../utilities/show_loading.dart'; +import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; @@ -30,10 +31,7 @@ import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/stack_dialog.dart'; class MonkeyView extends ConsumerStatefulWidget { - const MonkeyView({ - super.key, - required this.walletId, - }); + const MonkeyView({super.key, required this.walletId}); static const String routeName = "/monkey"; static const double navBarHeight = 65.0; @@ -56,7 +54,7 @@ class _MonkeyViewState extends ConsumerState { Future _getDocsDir() async { try { if (Platform.isAndroid) { - return Directory("/storage/emulated/0/Documents"); + return await StackFileSystem.wtfAndroidDocumentsPath(); } return await getApplicationDocumentsDirectory(); @@ -72,21 +70,17 @@ class _MonkeyViewState extends ConsumerState { bool isPNG = false, bool overwrite = false, }) async { - if (Platform.isAndroid) { - await Permission.storage.request(); - } - final dir = await _getDocsDir(); if (dir == null) { throw Exception("Failed to get documents directory to save monKey image"); } - final address = await ref - .read(pWallets) - .getWallet(walletId) - .getCurrentReceivingAddress(); - final docPath = dir.path; - String filePath = "$docPath/monkey_$address"; + final address = + await ref + .read(pWallets) + .getWallet(walletId) + .getCurrentReceivingAddress(); + String filePath = path.join(dir.path, "monkey_${address?.value}"); filePath += isPNG ? ".png" : ".svg"; @@ -119,273 +113,261 @@ class _MonkeyViewState extends ConsumerState { return Background( child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopScaffold( - appBar: DesktopAppBar( - background: Theme.of(context).extension()!.popupBG, - leading: Expanded( - child: Row( - children: [ - const SizedBox( - width: 32, - ), - 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, - ), - const SizedBox( - width: 15, - ), - SvgPicture.asset( - Assets.svg.monkey, - width: 32, - height: 32, - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - const SizedBox( - width: 12, + builder: + (child) => DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + children: [ + const SizedBox(width: 32), + 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, + ), + const SizedBox(width: 15), + SvgPicture.asset( + Assets.svg.monkey, + width: 32, + height: 32, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + const SizedBox(width: 12), + Text("MonKey", style: STextStyles.desktopH3(context)), + ], ), - Text( - "MonKey", - style: STextStyles.desktopH3(context), + ), + trailing: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), ), - ], - ), - ), - trailing: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(1000), - ), - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return DesktopDialog( - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: double.infinity, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "About MonKeys", - style: STextStyles.desktopH3(context), - ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "About MonKeys", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - const DesktopDialogCloseButton(), - ], - ), - Text( - "A MonKey is a visual representation of your Banano address.", - style: - STextStyles.desktopTextMedium(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.all( - 32, - ), - child: PrimaryButton( - width: 272.5, - label: "OK", - onPressed: () { - Navigator.of(context).pop(); - }, + Text( + "A MonKey is a visual representation of your Banano address.", + style: STextStyles.desktopTextMedium( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, ), ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: PrimaryButton( + width: 272.5, + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), ], ), - ], - ), + ); + }, ); }, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 19, - horizontal: 32, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.circleQuestion, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .customTextButtonEnabledText, - ), - const SizedBox( - width: 8, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, ), - Text( - "What is MonKey?", - style: - STextStyles.desktopMenuItemSelected(context).copyWith( - color: Theme.of(context) - .extension()! - .customTextButtonEnabledText, - ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + color: + Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + const SizedBox(width: 8), + Text( + "What is MonKey?", + style: STextStyles.desktopMenuItemSelected( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], ), - ], + ), ), + useSpacers: false, + isCompactHeight: true, ), + body: child, ), - useSpacers: false, - isCompactHeight: true, - ), - body: child, - ), child: ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "MonKey", - style: STextStyles.navBarTitle(context), - ), - actions: [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - icon: SvgPicture.asset( - Assets.svg.circleQuestion, - ), + builder: + (child) => Scaffold( + appBar: AppBar( + leading: AppBarBackButton( onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const StackOkDialog( - title: "About MonKeys", - message: - "A MonKey is a visual representation of your Banano address.", - ); - }, - ); + Navigator.of(context).pop(); }, ), + title: Text( + "MonKey", + style: STextStyles.navBarTitle(context), + ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset(Assets.svg.circleQuestion), + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackOkDialog( + title: "About MonKeys", + message: + "A MonKey is a visual representation of your Banano address.", + ); + }, + ); + }, + ), + ), + ], ), - ], - ), - body: child, - ), + body: SafeArea(child: child), + ), child: ConditionalParent( condition: isDesktop, - builder: (child) => SizedBox( - width: 318, - child: child, - ), + builder: (child) => SizedBox(width: 318, child: child), child: ConditionalParent( condition: imageBytes != null, - builder: (_) => Column( - children: [ - isDesktop - ? const SizedBox( - height: 50, - ) - : const Spacer( - flex: 1, - ), - if (imageBytes != null) - SizedBox( - width: 300, - height: 300, - child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), - ), - isDesktop - ? const SizedBox( - height: 50, - ) - : const Spacer( - flex: 1, + builder: + (_) => Column( + children: [ + isDesktop + ? const SizedBox(height: 50) + : const Spacer(flex: 1), + if (imageBytes != null) + SizedBox( + width: 300, + height: 300, + child: SvgPicture.memory( + Uint8List.fromList(imageBytes!), + ), ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SecondaryButton( - label: "Save as SVG", - onPressed: () async { - bool didError = false; - await showLoading( - whileFuture: Future.wait([ - _saveMonKeyToFile( - bytes: Uint8List.fromList( - (wallet as BananoWallet) - .getMonkeyImageBytes()!, - ), - ), - Future.delayed( - const Duration(seconds: 2), - ), - ]), - context: context, - rootNavigator: Util.isDesktop, - message: "Saving MonKey svg", - onException: (e) { - didError = true; - String msg = e.toString(); - while (msg.isNotEmpty && - msg.startsWith("Exception:")) { - msg = msg.substring(10).trim(); - } - showFloatingFlushBar( - type: FlushBarType.warning, - message: msg, + isDesktop + ? const SizedBox(height: 50) + : const Spacer(flex: 1), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SecondaryButton( + label: "Save as SVG", + onPressed: () async { + bool didError = false; + await showLoading( + whileFuture: Future.wait([ + _saveMonKeyToFile( + bytes: Uint8List.fromList( + (wallet as BananoWallet) + .getMonkeyImageBytes()!, + ), + ), + Future.delayed( + const Duration(seconds: 2), + ), + ]), context: context, + rootNavigator: Util.isDesktop, + message: "Saving MonKey svg", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, ); - }, - ); - if (!didError && mounted) { - await showFloatingFlushBar( - type: FlushBarType.success, - message: - "SVG MonKey image saved to $_monkeyPath", - context: context, - ); - } - }, - ), - const SizedBox(height: 12), - SecondaryButton( - label: "Download as PNG", - onPressed: () async { - bool didError = false; - await showLoading( - whileFuture: Future.wait([ - wallet.getCurrentReceivingAddress().then( + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: + "SVG MonKey image saved to $_monkeyPath", + context: context, + ); + } + }, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Download as PNG", + onPressed: () async { + bool didError = false; + await showLoading( + whileFuture: Future.wait([ + wallet.getCurrentReceivingAddress().then( (address) async => await ref .read(pMonKeyService) .fetchMonKey( @@ -395,80 +377,73 @@ class _MonkeyViewState extends ConsumerState { .then( (monKeyBytes) async => await _saveMonKeyToFile( - bytes: monKeyBytes, - isPNG: true, - ), + bytes: monKeyBytes, + isPNG: true, + ), ), ), - Future.delayed( - const Duration(seconds: 2), - ), - ]), - context: context, - rootNavigator: Util.isDesktop, - message: "Downloading MonKey png", - onException: (e) { - didError = true; - String msg = e.toString(); - while (msg.isNotEmpty && - msg.startsWith("Exception:")) { - msg = msg.substring(10).trim(); - } - showFloatingFlushBar( - type: FlushBarType.warning, - message: msg, + Future.delayed( + const Duration(seconds: 2), + ), + ]), context: context, + rootNavigator: Util.isDesktop, + message: "Downloading MonKey png", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, ); - }, - ); - if (!didError && mounted) { - await showFloatingFlushBar( - type: FlushBarType.success, - message: - "PNG MonKey image saved to $_monkeyPath", - context: context, - ); - } - }, + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: + "PNG MonKey image saved to $_monkeyPath", + context: context, + ); + } + }, + ), + ], ), - ], - ), + ), + // child, + ], ), - // child, - ], - ), child: Column( children: [ isDesktop - ? const SizedBox( - height: 100, - ) - : const Spacer( - flex: 4, - ), + ? const SizedBox(height: 100) + : const Spacer(flex: 4), Center( child: Column( children: [ Opacity( opacity: 0.2, child: SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 200, height: 200, ), ), - const SizedBox( - height: 70, - ), + const SizedBox(height: 70), Text( "You do not have a MonKey yet. \nFetch yours now!", style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, ), textAlign: TextAlign.center, ), @@ -476,12 +451,8 @@ class _MonkeyViewState extends ConsumerState { ), ), isDesktop - ? const SizedBox( - height: 50, - ) - : const Spacer( - flex: 6, - ), + ? const SizedBox(height: 50) + : const Spacer(flex: 6), Padding( padding: const EdgeInsets.all(16.0), child: PrimaryButton( @@ -490,16 +461,14 @@ class _MonkeyViewState extends ConsumerState { await showLoading( whileFuture: Future.wait([ wallet.getCurrentReceivingAddress().then( - (address) async => await ref - .read(pMonKeyService) - .fetchMonKey(address: address!.value) - .then( - (monKeyBytes) async => - await _updateWalletMonKey( - monKeyBytes, - ), - ), - ), + (address) async => await ref + .read(pMonKeyService) + .fetchMonKey(address: address!.value) + .then( + (monKeyBytes) async => + await _updateWalletMonKey(monKeyBytes), + ), + ), Future.delayed(const Duration(seconds: 2)), ]), context: context, diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index d0dddda3e..2943c6ec1 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -103,13 +103,14 @@ class _BuyDomainWidgetState extends ConsumerState { note: "Reserve ${widget.domainName.substring(2)}.bit", feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: myAddress.value, isChange: false, amount: Amount( rawValue: BigInt.from(kNameNewAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: myAddress.type, ), ], ); @@ -123,28 +124,30 @@ class _BuyDomainWidgetState extends ConsumerState { if (_preRegLock) return; _preRegLock = true; try { - final txData = (await showLoading( - whileFuture: _preRegFuture(), - context: context, - message: "Preparing transaction...", - onException: (e) { - throw e; - }, - ))!; + final txData = + (await showLoading( + whileFuture: _preRegFuture(), + context: context, + message: "Preparing transaction...", + onException: (e) { + throw e; + }, + ))!; if (mounted) { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => SDialog( - child: SizedBox( - width: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: widget.walletId, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), ), - ), - ), ); } else { await Navigator.of(context).pushNamed( @@ -164,12 +167,13 @@ class _BuyDomainWidgetState extends ConsumerState { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -192,50 +196,48 @@ class _BuyDomainWidgetState extends ConsumerState { builder: (context) { return Util.isDesktop ? SDialog( - child: SizedBox( - width: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Add DNS record", - style: STextStyles.desktopH3(context), - ), - ), - DesktopDialogCloseButton( - onPressedOverride: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); - }, + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Add DNS record", + style: STextStyles.desktopH3(context), ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, ), - child: AddDnsStep1( - name: _getNameFormattedForInternal(), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: AddDnsStep1( + name: _getNameFormattedForInternal(), ), - ], - ), + ), + ], ), - ) + ), + ) : StackDialogBase( - child: AddDnsStep1( - name: _getNameFormattedForInternal(), - ), - ); + child: AddDnsStep1( + name: _getNameFormattedForInternal(), + ), + ); }, ); }, @@ -254,11 +256,12 @@ class _BuyDomainWidgetState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Add DNS record failed", - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Add DNS record failed", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -289,8 +292,9 @@ class _BuyDomainWidgetState extends ConsumerState { builder: (ctx, constraints) { return SingleChildScrollView( child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight), + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -306,114 +310,120 @@ class _BuyDomainWidgetState extends ConsumerState { ); }, child: Column( - crossAxisAlignment: Util.isDesktop - ? CrossAxisAlignment.start - : CrossAxisAlignment.stretch, + crossAxisAlignment: + Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ if (!Util.isDesktop) Text( "Buy domain", - style: Util.isDesktop - ? STextStyles.desktopH3(context) - : STextStyles.pageTitleH2(context), + style: + Util.isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), Row( - mainAxisAlignment: Util.isDesktop - ? MainAxisAlignment.center - : MainAxisAlignment.start, + mainAxisAlignment: + Util.isDesktop + ? MainAxisAlignment.center + : MainAxisAlignment.start, children: [ Text( "Name registration will take approximately 2 to 4 hours.", - style: Util.isDesktop - ? STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ) - : STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), ), ], ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Domain name", - style: Util.isDesktop - ? STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .infoItemLabel, - ) - : STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .infoItemLabel, - ), + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), ), Text( "${widget.domainName.substring(2)}.bit", - style: Util.isDesktop - ? STextStyles.w500_14(context) - : STextStyles.w500_12(context), + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), ), ], ), ), - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + SizedBox(height: Util.isDesktop ? 16 : 8), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Amount", - style: Util.isDesktop - ? STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .infoItemLabel, - ) - : STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .infoItemLabel, - ), + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), ), Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( Amount( rawValue: BigInt.from(kNameNewAmountSats), fractionDigits: coin.fractionDigits, ), ), - style: Util.isDesktop - ? STextStyles.w500_14(context) - : STextStyles.w500_12(context), + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), ), ], ), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), ConditionalParent( condition: !Util.isDesktop, - builder: (child) => Row( - children: [child], - ), + builder: (child) => Row(children: [child]), child: CustomTextButton( text: _settingsHidden ? "More settings" : "Hide settings", onTap: () { @@ -423,10 +433,7 @@ class _BuyDomainWidgetState extends ConsumerState { }, ), ), - if (!_settingsHidden) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + if (!_settingsHidden) SizedBox(height: Util.isDesktop ? 24 : 16), if (!_settingsHidden) if (_dnsRecords.isEmpty) RoundedWhiteContainer( @@ -436,9 +443,10 @@ class _BuyDomainWidgetState extends ConsumerState { Text( "Add DNS records to your domain name", style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], @@ -455,27 +463,25 @@ class _BuyDomainWidgetState extends ConsumerState { (e) => DNSRecordCard( key: ValueKey(e), record: e, - onRemoveTapped: () => setState(() { - _dnsRecords.remove(e); - }), + onRemoveTapped: + () => setState(() { + _dnsRecords.remove(e); + }), ), ), - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + SizedBox(height: Util.isDesktop ? 16 : 8), SecondaryButton( - label: _dnsRecords.isEmpty - ? "Add DNS record" - : "Add another DNS record", + label: + _dnsRecords.isEmpty + ? "Add DNS record" + : "Add another DNS record", buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: _addRecord, ), ], ), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), if (!Util.isDesktop && _settingsHidden) const Spacer(), PrimaryButton( label: "Buy", @@ -483,9 +489,7 @@ class _BuyDomainWidgetState extends ConsumerState { buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: _preRegister, ), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + SizedBox(height: Util.isDesktop ? 32 : 16), ], ), ); @@ -524,13 +528,8 @@ class DNSRecordCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "${record.type.name}$_extraInfo", - ), - CustomTextButton( - text: "Remove", - onTap: onRemoveTapped, - ), + Text("${record.type.name}$_extraInfo"), + CustomTextButton(text: "Remove", onTap: onRemoveTapped), ], ), Text(record.getValueString()), diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index ec0fc926a..ff2f0b74a 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -21,7 +21,6 @@ import '../../models/isar/models/transaction_note.dart'; import '../../notifications/show_flush_bar.dart'; import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; -import '../../providers/db/main_db_provider.dart'; import '../../providers/global/secure_store_provider.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; @@ -33,6 +32,7 @@ import '../../utilities/constants.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; +import '../../wallets/crypto_currency/coins/ethereum.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; @@ -96,11 +96,7 @@ class _ConfirmNameTransactionViewState ), ); - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); final List txids = []; Future txDataFuture; @@ -111,10 +107,7 @@ class _ConfirmNameTransactionViewState txDataFuture = wallet.confirmSend(txData: widget.txData); // await futures in parallel - final futureResults = await Future.wait([ - txDataFuture, - time, - ]); + final futureResults = await Future.wait([txDataFuture, time]); final txData = (futureResults.first as TxData); @@ -126,7 +119,9 @@ class _ConfirmNameTransactionViewState Future.delayed(const Duration(seconds: 5)), // associated name data for reg tx - ref.read(secureStoreProvider).write( + ref + .read(secureStoreProvider) + .write( key: nameSaltKeyBuilder( txData.txid!, walletId, @@ -141,16 +136,16 @@ class _ConfirmNameTransactionViewState ]); txids.add(txData.txid!); - ref.refresh(desktopUseUTXOs); + if (coin is! Ethereum) { + ref.refresh(desktopUseUTXOs); + } // save note for (final txid in txids) { - await ref.read(mainDBProvider).putTransactionNote( - TransactionNote( - walletId: walletId, - txid: txid, - value: note, - ), + await ref + .read(mainDBProvider) + .putTransactionNote( + TransactionNote(walletId: walletId, txid: txid, value: note), ); } @@ -192,13 +187,8 @@ class _ConfirmNameTransactionViewState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - niceError, - style: STextStyles.desktopH3(context), - ), - const SizedBox( - height: 24, - ), + Text(niceError, style: STextStyles.desktopH3(context)), + const SizedBox(height: 24), Flexible( child: SingleChildScrollView( child: SelectableText( @@ -207,9 +197,7 @@ class _ConfirmNameTransactionViewState ), ), ), - const SizedBox( - height: 56, - ), + const SizedBox(height: 56), Row( children: [ const Spacer(), @@ -237,9 +225,10 @@ class _ConfirmNameTransactionViewState child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -284,82 +273,81 @@ class _ConfirmNameTransactionViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - ), - ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, ), - ); - }, + ), + ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Row( + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, children: [ - AppBarBackButton( - size: 40, - iconSize: 24, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(), - ), - Text( - "Confirm transaction", - style: STextStyles.desktopH3(context), + Row( + children: [ + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: + () => + Navigator.of(context, rootNavigator: true).pop(), + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), + ), + ], ), + Flexible(child: SingleChildScrollView(child: child)), ], ), - Flexible( - child: SingleChildScrollView( - child: child, - ), - ), - ], - ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, @@ -372,20 +360,13 @@ class _ConfirmNameTransactionViewState "Confirm Name transaction", style: STextStyles.pageTitleH1(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Name", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Name", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), Text( widget.txData.opNameState!.name, style: STextStyles.itemSubtitle12(context), @@ -393,20 +374,13 @@ class _ConfirmNameTransactionViewState ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Value", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Value", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), Text( widget.txData.opNameState!.value, style: STextStyles.itemSubtitle12(context), @@ -414,9 +388,7 @@ class _ConfirmNameTransactionViewState ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -425,9 +397,7 @@ class _ConfirmNameTransactionViewState "Recipient", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle12(context), @@ -435,30 +405,23 @@ class _ConfirmNameTransactionViewState ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), + Text("Amount", style: STextStyles.smallMed12(context)), SelectableText( - ref.watch(pAmountFormatter(coin)).format( - amountWithoutChange, - ), + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange), style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, ), ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -476,9 +439,7 @@ class _ConfirmNameTransactionViewState ), ), if (widget.txData.fee != null && widget.txData.vSize != null) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (widget.txData.fee != null && widget.txData.vSize != null) RoundedWhiteContainer( child: Row( @@ -488,9 +449,7 @@ class _ConfirmNameTransactionViewState "sats/vByte", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( "~${fee.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle12(context), @@ -500,22 +459,15 @@ class _ConfirmNameTransactionViewState ), if (widget.txData.note != null && widget.txData.note!.isNotEmpty) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (widget.txData.note != null && widget.txData.note!.isNotEmpty) RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Note", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), SelectableText( widget.txData.note!, style: STextStyles.itemSubtitle12(context), @@ -543,9 +495,10 @@ class _ConfirmNameTransactionViewState children: [ Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, borderRadius: BorderRadius.only( topLeft: Radius.circular( Constants.size.circularBorderRadius, @@ -573,9 +526,7 @@ class _ConfirmNameTransactionViewState width: 32, height: 32, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Text( "Send $unit Name transaction", style: STextStyles.desktopTextMedium(context), @@ -596,17 +547,16 @@ class _ConfirmNameTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( widget.txData.opNameState!.name, style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -614,9 +564,10 @@ class _ConfirmNameTransactionViewState ), Container( height: 1, - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, ), Padding( padding: const EdgeInsets.all(12), @@ -630,17 +581,16 @@ class _ConfirmNameTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( widget.txData.opNameState!.value, style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -652,27 +602,24 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ SelectableText( "Note (optional)", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -684,11 +631,13 @@ class _ConfirmNameTransactionViewState enableSuggestions: isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), onChanged: (_) => setState(() {}), @@ -704,41 +653,37 @@ class _ConfirmNameTransactionViewState bottom: 12, right: 5, ), - 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 = "", - ); - }, - ), - ], + 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, + ) + : null, ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), ], ), ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - ), + padding: const EdgeInsets.only(top: 16, left: 32), child: Text( "Amount", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -746,19 +691,16 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), child: RoundedContainer( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 18, ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Builder( builder: (context) { final externalCalls = ref.watch( @@ -769,21 +711,21 @@ class _ConfirmNameTransactionViewState String fiatAmount = "N/A"; if (externalCalls) { - final price = ref - .read( - priceAnd24hChangeNotifierProvider, - ) - .getPrice(coin) - .item1; - if (price > Decimal.zero) { + final price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + ?.value; + if (price != null && price > Decimal.zero) { fiatAmount = (amountWithoutChange.decimal * price) .toAmount(fractionDigits: 2) .fiatString( - locale: ref - .read( - localeServiceChangeNotifierProvider, - ) - .locale, + locale: + ref + .read( + localeServiceChangeNotifierProvider, + ) + .locale, ); } } @@ -791,30 +733,20 @@ class _ConfirmNameTransactionViewState return Row( children: [ SelectableText( - ref.watch(pAmountFormatter(coin)).format( - amountWithoutChange, - ), - style: STextStyles.itemSubtitle( - context, - ), + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange), + style: STextStyles.itemSubtitle(context), ), if (externalCalls) Text( " | ", - style: STextStyles.itemSubtitle( - context, - ), + style: STextStyles.itemSubtitle(context), ), if (externalCalls) SelectableText( - "~$fiatAmount ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: STextStyles.itemSubtitle( - context, - ), + "~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.itemSubtitle(context), ), ], ); @@ -824,10 +756,7 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - ), + padding: const EdgeInsets.only(top: 16, left: 32), child: Text( "Recipient", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -835,19 +764,16 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), child: RoundedContainer( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 18, ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle(context), @@ -857,10 +783,7 @@ class _ConfirmNameTransactionViewState // todo amoutn here if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - ), + padding: const EdgeInsets.only(top: 16, left: 32), child: Text( "Transaction fee", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -868,19 +791,16 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), child: RoundedContainer( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 18, ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.itemSubtitle(context), @@ -891,10 +811,7 @@ class _ConfirmNameTransactionViewState widget.txData.fee != null && widget.txData.vSize != null) Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - ), + padding: const EdgeInsets.only(top: 16, left: 32), child: Text( "sats/vByte", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -904,19 +821,16 @@ class _ConfirmNameTransactionViewState widget.txData.fee != null && widget.txData.vSize != null) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), child: RoundedContainer( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 18, ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), @@ -924,74 +838,78 @@ class _ConfirmNameTransactionViewState ), ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 23 : 12, - ), + SizedBox(height: isDesktop ? 23 : 12), Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ) - : const EdgeInsets.all(12), - color: Theme.of(context) - .extension()! - .snackBarBackSuccess, + padding: + isDesktop + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ) + : const EdgeInsets.all(12), + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( isDesktop ? "Total amount to send" : "Total amount", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), ), SelectableText( ref .watch(pAmountFormatter(coin)) .format(amountWithoutChange + fee!), - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), textAlign: TextAlign.right, ), ], ), ), ), - SizedBox( - height: isDesktop ? 28 : 16, - ), + SizedBox(height: isDesktop ? 28 : 16), Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", buttonHeight: isDesktop ? ButtonHeight.l : null, @@ -1001,31 +919,28 @@ class _ConfirmNameTransactionViewState if (isDesktop) { unlocked = await showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.end, + builder: + (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - DesktopDialogCloseButton(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend( - coin: coin, - ), - ), - ], - ), - ), + ), ); } else { unlocked = await Navigator.push( @@ -1033,18 +948,21 @@ class _ConfirmNameTransactionViewState RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", + builder: + (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: + "Confirm Transaction", + ), + settings: const RouteSettings( + name: "/confirmsendlockscreen", ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), ), ); } @@ -1057,9 +975,10 @@ class _ConfirmNameTransactionViewState unawaited( showFloatingFlushBar( type: FlushBarType.warning, - message: Util.isDesktop - ? "Invalid passphrase" - : "Invalid PIN", + message: + Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", context: context, ), ); @@ -1069,10 +988,7 @@ class _ConfirmNameTransactionViewState }, ), ), - if (isDesktop) - const SizedBox( - height: 32, - ), + if (isDesktop) const SizedBox(height: 32), ], ), ), diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index dcd8e1282..89be6129c 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -41,14 +41,12 @@ class TransferOptionWidget extends ConsumerStatefulWidget { required this.walletId, required this.utxo, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), }); final String walletId; final UTXO utxo; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; @override ConsumerState createState() => @@ -58,7 +56,7 @@ class TransferOptionWidget extends ConsumerStatefulWidget { class _TransferOptionWidgetState extends ConsumerState { late final String walletId; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; + late final TextEditingController _addressController; late final FocusNode _addressFocusNode; @@ -71,9 +69,7 @@ class _TransferOptionWidgetState extends ConsumerState { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); try { final wallet = ref.read(pWallets).getWallet(walletId) as NamecoinWallet; @@ -129,11 +125,7 @@ class _TransferOptionWidgetState extends ConsumerState { final opName = wallet.getOpNameDataFrom(widget.utxo)!; - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); final nameScriptHex = scriptNameUpdate(opName.fullname, opName.value); @@ -141,13 +133,14 @@ class _TransferOptionWidgetState extends ConsumerState { txData: TxData( feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: _address!, isChange: false, amount: Amount( rawValue: BigInt.from(kNameAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, ), ], note: "Transfer ${opName.constructedName}", @@ -164,10 +157,7 @@ class _TransferOptionWidgetState extends ConsumerState { ), ); - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); final txData = results.first as TxData; @@ -179,15 +169,16 @@ class _TransferOptionWidgetState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => SDialog( - child: SizedBox( - width: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: widget.walletId, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), ), - ), - ), ); } else { await Navigator.of(context).pushNamed( @@ -212,12 +203,13 @@ class _TransferOptionWidgetState extends ConsumerState { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -245,7 +237,7 @@ class _TransferOptionWidgetState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final coin = ref.read(pWalletCoin(walletId)); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); @@ -271,14 +263,27 @@ class _TransferOptionWidgetState extends ConsumerState { _setValidAddressProviders(_address); } } on PlatformException catch (e, s) { - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.e( - "Failed to get camera permissions while trying to scan qr code in" - " $runtimeType", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in" + " $runtimeType", + error: e, + stackTrace: s, + ); + } } } @@ -287,7 +292,7 @@ class _TransferOptionWidgetState extends ConsumerState { super.initState(); walletId = widget.walletId; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; + _addressController = TextEditingController(); _addressFocusNode = FocusNode(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -345,72 +350,64 @@ class _TransferOptionWidgetState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _addressController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _addressController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _addressController.text.isNotEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Address Field Input.", - key: const Key( - "nameTransferClearAddressFieldButtonKey", - ), - onTap: () { - _addressController.text = ""; - _address = ""; - _setValidAddressProviders( - _address, - ); - setState(() {}); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "nameTransferClearAddressFieldButtonKey", + ), + onTap: () { + _addressController.text = ""; + _address = ""; + _setValidAddressProviders(_address); + setState(() {}); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Address Field Input.", - key: const Key( - "nameTransferPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = - await clipboard.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(); - _address = content.trim(); - - _setValidAddressProviders( - _address, + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "nameTransferPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .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"), ); } - }, - child: _addressController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + + _addressController.text = content.trim(); + _address = content.trim(); + + _setValidAddressProviders(_address); + } + }, + child: + _addressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_addressController.text.isEmpty) TextFieldIconButton( semanticsLabel: "Address Book Button. Opens Address Book For Address Field.", - key: const Key( - "nameTransferAddressBookButtonKey", - ), + key: const Key("nameTransferAddressBookButtonKey"), onTap: () { Navigator.of(context).pushNamed( AddressBookView.routeName, @@ -423,9 +420,7 @@ class _TransferOptionWidgetState extends ConsumerState { TextFieldIconButton( semanticsLabel: "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key( - "nameTransferScanQrButtonKey", - ), + key: const Key("nameTransferScanQrButtonKey"), onTap: _scanQr, child: const QrCodeIcon(), ), @@ -436,32 +431,28 @@ class _TransferOptionWidgetState extends ConsumerState { ), ), ), - SizedBox( - height: Util.isDesktop ? 42 : 16, - ), + SizedBox(height: Util.isDesktop ? 42 : 16), if (!Util.isDesktop) const Spacer(), ConditionalParent( condition: Util.isDesktop, - builder: (child) => Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: child, + builder: + (child) => Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, + ), + ), + const SizedBox(width: 16), + Expanded(child: child), + ], ), - ], - ), child: PrimaryButton( label: "Transfer", enabled: _enableButton, @@ -470,10 +461,7 @@ class _TransferOptionWidgetState extends ConsumerState { onPressed: _preview, ), ), - if (!Util.isDesktop) - const SizedBox( - height: 16, - ), + if (!Util.isDesktop) const SizedBox(height: 16), ], ); } diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart index 80c016245..4fd78fb70 100644 --- a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -9,7 +9,6 @@ import '../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../providers/global/wallets_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; @@ -33,14 +32,12 @@ class UpdateOptionWidget extends ConsumerStatefulWidget { required this.walletId, required this.utxo, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), }); final String walletId; final UTXO utxo; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; @override ConsumerState createState() => _BuyDomainWidgetState(); @@ -84,9 +81,7 @@ class _BuyDomainWidgetState extends ConsumerState { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); final wallet = ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; @@ -144,11 +139,7 @@ class _BuyDomainWidgetState extends ConsumerState { final opName = wallet.getOpNameDataFrom(widget.utxo)!; - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); final nameScriptHex = scriptNameUpdate(opName.fullname, newValue); @@ -156,13 +147,14 @@ class _BuyDomainWidgetState extends ConsumerState { txData: TxData( feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: _address!.value, isChange: false, amount: Amount( rawValue: BigInt.from(kNameAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: _address.type, ), ], note: "Update ${opName.constructedName} (${opName.fullname})", @@ -179,10 +171,7 @@ class _BuyDomainWidgetState extends ConsumerState { ), ); - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); final txData = results.first as TxData; @@ -194,15 +183,16 @@ class _BuyDomainWidgetState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => SDialog( - child: SizedBox( - width: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: widget.walletId, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), ), - ), - ), ); } else { await Navigator.of(context).pushNamed( @@ -227,12 +217,13 @@ class _BuyDomainWidgetState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Update failed", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Update failed", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -269,17 +260,13 @@ class _BuyDomainWidgetState extends ConsumerState { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: Util.isDesktop - ? CrossAxisAlignment.start - : CrossAxisAlignment.stretch, + crossAxisAlignment: + Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ - Text( - "Edit value", - style: STextStyles.label(context), - ), - const SizedBox( - height: 6, - ), + Text("Edit value", style: STextStyles.label(context)), + const SizedBox(height: 6), TextField( controller: _controller, maxLines: null, @@ -296,9 +283,7 @@ class _BuyDomainWidgetState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -308,18 +293,17 @@ class _BuyDomainWidgetState extends ConsumerState { return Text( "$length/$valueMaxLength", style: STextStyles.w500_10(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle2, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, ), ); }, ), ], ), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + SizedBox(height: Util.isDesktop ? 32 : 16), if (!Util.isDesktop) const Spacer(), Row( children: [ @@ -327,15 +311,11 @@ class _BuyDomainWidgetState extends ConsumerState { child: SecondaryButton( label: "Cancel", buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - onPressed: Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop, + onPressed: + Navigator.of(context, rootNavigator: Util.isDesktop).pop, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Update", @@ -346,10 +326,7 @@ class _BuyDomainWidgetState extends ConsumerState { ), ], ), - if (!Util.isDesktop) - const SizedBox( - height: 16, - ), + if (!Util.isDesktop) const SizedBox(height: 16), ], ); } diff --git a/lib/pages/notification_views/notifications_view.dart b/lib/pages/notification_views/notifications_view.dart index 7a20efffd..417d10bae 100644 --- a/lib/pages/notification_views/notifications_view.dart +++ b/lib/pages/notification_views/notifications_view.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../notifications/notification_card.dart'; import '../../providers/providers.dart'; import '../../providers/ui/unread_notifications_provider.dart'; @@ -20,10 +21,7 @@ import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/rounded_white_container.dart'; class NotificationsView extends ConsumerStatefulWidget { - const NotificationsView({ - super.key, - this.walletId, - }); + const NotificationsView({super.key, this.walletId}); final String? walletId; @@ -46,75 +44,81 @@ class _NotificationsViewState extends ConsumerState { @override Widget build(BuildContext context) { - final notifications = widget.walletId == null - ? ref - .watch(notificationsProvider.select((value) => value.notifications)) - : ref - .watch(notificationsProvider.select((value) => value.notifications)) - .where((element) => element.walletId == widget.walletId) - .toList(growable: false); + final notifications = + widget.walletId == null + ? ref.watch( + notificationsProvider.select((value) => value.notifications), + ) + : ref + .watch( + notificationsProvider.select((value) => value.notifications), + ) + .where((element) => element.walletId == widget.walletId) + .toList(growable: false); return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( - title: Text( - "Notifications", - style: STextStyles.navBarTitle(context), - ), + title: Text("Notifications", style: STextStyles.navBarTitle(context)), leading: AppBarBackButton( onPressed: () async { Navigator.of(context).pop(); }, ), ), - body: Padding( - padding: const EdgeInsets.all(12), - child: notifications.isNotEmpty - ? Column( - children: [ - Expanded( - child: ListView.builder( - shrinkWrap: true, - itemCount: notifications.length, - itemBuilder: (builderContext, index) { - final notification = notifications[index]; - if (notification.read == false) { - ref - .read(unreadNotificationsStateProvider.state) - .state - .add(notification.id); - } - return Padding( - padding: const EdgeInsets.all(4), - child: NotificationCard( - notification: notifications[index], - ), - ); - }, - ), - ), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: RoundedWhiteContainer( - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "Notifications will appear here", - style: STextStyles.itemSubtitle(context), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: + notifications.isNotEmpty + ? Column( + children: [ + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemCount: notifications.length, + itemBuilder: (builderContext, index) { + final notification = notifications[index]; + if (notification.read == false) { + ref + .read( + unreadNotificationsStateProvider.state, + ) + .state + .add(notification.id); + } + return Padding( + padding: const EdgeInsets.all(4), + child: NotificationCard( + notification: notifications[index], + ), + ); + }, + ), + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "Notifications will appear here", + style: STextStyles.itemSubtitle(context), + ), + ), ), ), ), - ), + ], ), - ], - ), + ), ), ), ); diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 02e0ee074..05d7eb227 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -5,8 +5,8 @@ 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:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; import '../../models/isar/ordinal.dart'; @@ -21,6 +21,7 @@ import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/show_loading.dart'; +import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../widgets/background.dart'; @@ -60,20 +61,19 @@ class _OrdinalDetailsViewState extends ConsumerState { final coin = ref.watch(pWalletCoin(widget.walletId)); return Background( - child: SafeArea( - child: Scaffold( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: const AppBarBackButton(), - title: Text( - "Ordinal details", - style: STextStyles.navBarTitle(context), - ), + leading: const AppBarBackButton(), + title: Text( + "Ordinal details", + style: STextStyles.navBarTitle(context), ), - body: SingleChildScrollView( + ), + body: SafeArea( + child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( @@ -92,9 +92,7 @@ class _OrdinalDetailsViewState extends ConsumerState { title: "Inscription number", data: widget.ordinal.inscriptionNumber.toString(), ), - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), _DetailsItemWCopy( title: "Inscription ID", data: widget.ordinal.inscriptionId, @@ -103,37 +101,32 @@ class _OrdinalDetailsViewState extends ConsumerState { // height: _spacing, // ), // // todo: add utxo status - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), _DetailsItemWCopy( title: "Amount", - data: utxo == null - ? "ERROR" - : ref.watch(pAmountFormatter(coin)).format( - Amount( - rawValue: BigInt.from(utxo!.value), - fractionDigits: coin.fractionDigits, - ), - ), - ), - const SizedBox( - height: _spacing, + data: + utxo == null + ? "ERROR" + : ref + .watch(pAmountFormatter(coin)) + .format( + Amount( + rawValue: BigInt.from(utxo!.value), + fractionDigits: coin.fractionDigits, + ), + ), ), + const SizedBox(height: _spacing), _DetailsItemWCopy( title: "Owner address", data: utxo?.address ?? "ERROR", ), - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), _DetailsItemWCopy( title: "Transaction ID", data: widget.ordinal.utxoTXID, ), - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), ], ), ), @@ -145,11 +138,7 @@ class _OrdinalDetailsViewState extends ConsumerState { } class _DetailsItemWCopy extends StatelessWidget { - const _DetailsItemWCopy({ - super.key, - required this.title, - required this.data, - }); + const _DetailsItemWCopy({super.key, required this.title, required this.data}); final String title; final String data; @@ -163,10 +152,7 @@ class _DetailsItemWCopy extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - title, - style: STextStyles.itemSubtitle(context), - ), + Text(title, style: STextStyles.itemSubtitle(context)), GestureDetector( onTap: () async { await Clipboard.setData(ClipboardData(text: data)); @@ -184,20 +170,20 @@ class _DetailsItemWCopy extends StatelessWidget { children: [ SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, width: 12, ), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), Text( "Copy", style: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), ), ], @@ -205,13 +191,8 @@ class _DetailsItemWCopy extends StatelessWidget { ), ], ), - const SizedBox( - height: 4, - ), - SelectableText( - data, - style: STextStyles.itemSubtitle12(context), - ), + const SizedBox(height: 4), + SelectableText(data, style: STextStyles.itemSubtitle12(context)), ], ), ); @@ -235,9 +216,10 @@ class _OrdinalImageGroup extends ConsumerWidget { final response = await client.get( url: Uri.parse(ordinal.content), - proxyInfo: ref.read(prefsChangeNotifierProvider).useTor - ? ref.read(pTorService).getProxyInfo() - : null, + proxyInfo: + ref.read(prefsChangeNotifierProvider).useTor + ? ref.read(pTorService).getProxyInfo() + : null, ); if (response.code != 200) { @@ -248,16 +230,14 @@ class _OrdinalImageGroup extends ConsumerWidget { final bytes = response.bodyBytes; - if (Platform.isAndroid) { - await Permission.storage.request(); - } - - final dir = Platform.isAndroid - ? Directory("/storage/emulated/0/Documents") - : await getApplicationDocumentsDirectory(); - - final docPath = dir.path; - final filePath = "$docPath/ordinal_${ordinal.inscriptionNumber}.png"; + final dir = + Platform.isAndroid + ? await StackFileSystem.wtfAndroidDocumentsPath() + : await getApplicationDocumentsDirectory(); + final filePath = path.join( + dir.path, + "ordinal_${ordinal.inscriptionNumber}.png", + ); final File imgFile = File(filePath); @@ -299,9 +279,7 @@ class _OrdinalImageGroup extends ConsumerWidget { ), ), ), - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), Row( children: [ Expanded( @@ -311,9 +289,10 @@ class _OrdinalImageGroup extends ConsumerWidget { Assets.svg.arrowDown, width: 10, height: 12, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), buttonHeight: ButtonHeight.l, iconSpacing: 4, diff --git a/lib/pages/ordinals/ordinals_filter_view.dart b/lib/pages/ordinals/ordinals_filter_view.dart index 4c7d0b398..93c7e0dd7 100644 --- a/lib/pages/ordinals/ordinals_filter_view.dart +++ b/lib/pages/ordinals/ordinals_filter_view.dart @@ -69,9 +69,7 @@ class OrdinalFilter { final ordinalFilterProvider = StateProvider((_) => null); class OrdinalsFilterView extends ConsumerStatefulWidget { - const OrdinalsFilterView({ - super.key, - }); + const OrdinalsFilterView({super.key}); static const String routeName = "/ordinalsFilterView"; @@ -127,9 +125,10 @@ class _OrdinalsFilterViewState extends ConsumerState { return Text( isDateSelected ? "From..." : _fromDateString, style: STextStyles.fieldLabel(context).copyWith( - color: isDateSelected - ? Theme.of(context).extension()!.textSubtitle2 - : Theme.of(context).extension()!.accentColorDark, + color: + isDateSelected + ? Theme.of(context).extension()!.textSubtitle2 + : Theme.of(context).extension()!.accentColorDark, ), ); } @@ -139,9 +138,10 @@ class _OrdinalsFilterViewState extends ConsumerState { return Text( isDateSelected ? "To..." : _toDateString, style: STextStyles.fieldLabel(context).copyWith( - color: isDateSelected - ? Theme.of(context).extension()!.textSubtitle2 - : Theme.of(context).extension()!.accentColorDark, + color: + isDateSelected + ? Theme.of(context).extension()!.textSubtitle2 + : Theme.of(context).extension()!.accentColorDark, ), ); } @@ -154,13 +154,14 @@ class _OrdinalsFilterViewState extends ConsumerState { const middleSeparatorWidth = 12.0; final isDesktop = Util.isDesktop; - final width = isDesktop - ? null - : (MediaQuery.of(context).size.width - - (middleSeparatorWidth + - (2 * middleSeparatorPadding) + - (2 * Constants.size.standardPadding))) / - 2; + final width = + isDesktop + ? null + : (MediaQuery.of(context).size.width - + (middleSeparatorWidth + + (2 * middleSeparatorPadding) + + (2 * Constants.size.standardPadding))) / + 2; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -181,7 +182,8 @@ class _OrdinalsFilterViewState extends ConsumerState { _selectedFromDate = date; // flag to adjust date so from date is always before to date - final flag = _selectedToDate != null && + final flag = + _selectedToDate != null && !_selectedFromDate!.isBefore(_selectedToDate!); if (flag) { _selectedToDate = DateTime.fromMillisecondsSinceEpoch( @@ -191,13 +193,15 @@ class _OrdinalsFilterViewState extends ConsumerState { setState(() { if (flag) { - _toDateString = _selectedToDate == null - ? "" - : Format.formatDate(_selectedToDate!); + _toDateString = + _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); } - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); + _fromDateString = + _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); }); } } @@ -205,15 +209,18 @@ class _OrdinalsFilterViewState extends ConsumerState { child: Container( width: width, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), border: Border.all( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, width: 1, ), ), @@ -228,18 +235,15 @@ class _OrdinalsFilterViewState extends ConsumerState { Assets.svg.calendar, height: 20, width: 20, - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - const SizedBox( - width: 10, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, ), + const SizedBox(width: 10), Align( alignment: Alignment.centerLeft, - child: FittedBox( - child: _dateFromText, - ), + child: FittedBox(child: _dateFromText), ), ], ), @@ -248,8 +252,9 @@ class _OrdinalsFilterViewState extends ConsumerState { ), ), Padding( - padding: - const EdgeInsets.symmetric(horizontal: middleSeparatorPadding), + padding: const EdgeInsets.symmetric( + horizontal: middleSeparatorPadding, + ), child: Container( width: middleSeparatorWidth, // height: 1, @@ -272,7 +277,8 @@ class _OrdinalsFilterViewState extends ConsumerState { _selectedToDate = date; // flag to adjust date so from date is always before to date - final flag = _selectedFromDate != null && + final flag = + _selectedFromDate != null && !_selectedToDate!.isAfter(_selectedFromDate!); if (flag) { _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( @@ -282,13 +288,15 @@ class _OrdinalsFilterViewState extends ConsumerState { setState(() { if (flag) { - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); + _fromDateString = + _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); } - _toDateString = _selectedToDate == null - ? "" - : Format.formatDate(_selectedToDate!); + _toDateString = + _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); }); } } @@ -296,15 +304,18 @@ class _OrdinalsFilterViewState extends ConsumerState { child: Container( width: width, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), border: Border.all( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, width: 1, ), ), @@ -319,18 +330,15 @@ class _OrdinalsFilterViewState extends ConsumerState { Assets.svg.calendar, height: 20, width: 20, - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - const SizedBox( - width: 10, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, ), + const SizedBox(width: 10), Align( alignment: Alignment.centerLeft, - child: FittedBox( - child: _dateToText, - ), + child: FittedBox(child: _dateToText), ), ], ), @@ -338,10 +346,7 @@ class _OrdinalsFilterViewState extends ConsumerState { ), ), ), - if (isDesktop) - const SizedBox( - width: 24, - ), + if (isDesktop) const SizedBox(width: 24), ], ); } @@ -353,10 +358,7 @@ class _OrdinalsFilterViewState extends ConsumerState { maxWidth: 576, maxHeight: double.infinity, child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, bottom: 32), child: _buildContent(context), ), ); @@ -384,22 +386,23 @@ class _OrdinalsFilterViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: EdgeInsets.symmetric( - horizontal: Constants.size.standardPadding, - ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: _buildContent(context), + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: Constants.size.standardPadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: _buildContent(context)), ), - ), - ); - }, + ); + }, + ), ), ), ), @@ -424,9 +427,7 @@ class _OrdinalsFilterViewState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - SizedBox( - height: isDesktop ? 14 : 10, - ), + SizedBox(height: isDesktop ? 14 : 10), // if (!isDesktop) // Align( // alignment: Alignment.centerLeft, @@ -572,33 +573,29 @@ class _OrdinalsFilterViewState extends ConsumerState { child: FittedBox( child: Text( "Date", - style: isDesktop - ? STextStyles.labelExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), ), ), - SizedBox( - height: isDesktop ? 10 : 8, - ), + SizedBox(height: isDesktop ? 10 : 8), _buildDateRangePicker(), - SizedBox( - height: isDesktop ? 32 : 24, - ), + SizedBox(height: isDesktop ? 32 : 24), Align( alignment: Alignment.centerLeft, child: FittedBox( child: Text( "Inscription", - style: isDesktop - ? STextStyles.labelExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), ), ), - SizedBox( - height: isDesktop ? 10 : 8, - ), + SizedBox(height: isDesktop ? 10 : 8), Padding( padding: EdgeInsets.only(right: isDesktop ? 32 : 0), child: ClipRRect( @@ -612,65 +609,68 @@ class _OrdinalsFilterViewState extends ConsumerState { controller: _inscriptionTextEditingController, focusNode: inscriptionTextFieldFocusNode, onChanged: (_) => setState(() {}), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), decoration: standardInputDecoration( "Enter inscription number...", keywordTextFieldFocusNode, context, desktopMed: isDesktop, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ) - : null, - suffixIcon: _inscriptionTextEditingController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _inscriptionTextEditingController.text = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: + _inscriptionTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _inscriptionTextEditingController.text = + ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), - SizedBox( - height: isDesktop ? 32 : 24, - ), + SizedBox(height: isDesktop ? 32 : 24), Align( alignment: Alignment.centerLeft, child: FittedBox( child: Text( "Keyword", - style: isDesktop - ? STextStyles.labelExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), ), ), - SizedBox( - height: isDesktop ? 10 : 8, - ), + SizedBox(height: isDesktop ? 10 : 8), Padding( padding: EdgeInsets.only(right: isDesktop ? 32 : 0), child: ClipRRect( @@ -683,13 +683,16 @@ class _OrdinalsFilterViewState extends ConsumerState { key: const Key("OrdinalsViewKeywordFieldKey"), controller: _keywordTextEditingController, focusNode: keywordTextFieldFocusNode, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), onChanged: (_) => setState(() {}), decoration: standardInputDecoration( "Type keyword...", @@ -697,39 +700,39 @@ class _OrdinalsFilterViewState extends ConsumerState { context, desktopMed: isDesktop, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ) - : null, - suffixIcon: _keywordTextEditingController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _keywordTextEditingController.text = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: + _keywordTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _keywordTextEditingController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 32 : 20, - ), + SizedBox(height: isDesktop ? 32 : 20), Row( children: [ Expanded( @@ -741,9 +744,7 @@ class _OrdinalsFilterViewState extends ConsumerState { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed( - const Duration( - milliseconds: 75, - ), + const Duration(milliseconds: 75), ); } } @@ -753,9 +754,7 @@ class _OrdinalsFilterViewState extends ConsumerState { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: isDesktop ? ButtonHeight.l : null, @@ -765,16 +764,10 @@ class _OrdinalsFilterViewState extends ConsumerState { label: "Save", ), ), - if (isDesktop) - const SizedBox( - width: 32, - ), + if (isDesktop) const SizedBox(width: 32), ], ), - if (!isDesktop) - const SizedBox( - height: 20, - ), + if (!isDesktop) const SizedBox(height: 20), ], ); } diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 17aaffb86..ed47fcd5c 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'widgets/ordinals_list.dart'; + import '../../providers/global/wallets_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; @@ -20,12 +20,10 @@ import '../../utilities/text_styles.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import 'widgets/ordinals_list.dart'; class OrdinalsView extends ConsumerStatefulWidget { - const OrdinalsView({ - super.key, - required this.walletId, - }); + const OrdinalsView({super.key, required this.walletId}); static const routeName = "/ordinalsView"; @@ -59,73 +57,66 @@ class _OrdinalsViewState extends ConsumerState { @override Widget build(BuildContext context) { return Background( - child: SafeArea( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - automaticallyImplyLeading: false, - leading: const AppBarBackButton(), - title: Text( - "Ordinals", - style: STextStyles.navBarTitle(context), - ), - titleSpacing: 0, - actions: [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - size: 36, - icon: SvgPicture.asset( - Assets.svg.arrowRotate, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: () async { - // show loading for a minimum of 2 seconds on refreshing - await showLoading( - whileFuture: Future.wait([ - Future.delayed(const Duration(seconds: 2)), - (ref.read(pWallets).getWallet(widget.walletId) - as OrdinalsInterface) - .refreshInscriptions(), - ]), - context: context, - message: "Refreshing...", - ); - }, + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: const AppBarBackButton(), + title: Text("Ordinals", style: STextStyles.navBarTitle(context)), + titleSpacing: 0, + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.arrowRotate, + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), + onPressed: () async { + // show loading for a minimum of 2 seconds on refreshing + await showLoading( + whileFuture: Future.wait([ + Future.delayed(const Duration(seconds: 2)), + (ref.read(pWallets).getWallet(widget.walletId) + as OrdinalsInterface) + .refreshInscriptions(), + ]), + context: context, + message: "Refreshing...", + ); + }, ), - // AspectRatio( - // aspectRatio: 1, - // child: AppBarIconButton( - // size: 36, - // icon: SvgPicture.asset( - // Assets.svg.filter, - // width: 20, - // height: 20, - // color: Theme.of(context) - // .extension()! - // .topNavIconPrimary, - // ), - // onPressed: () { - // Navigator.of(context).pushNamed( - // OrdinalsFilterView.routeName, - // ); - // }, - // ), - // ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, ), + // AspectRatio( + // aspectRatio: 1, + // child: AppBarIconButton( + // size: 36, + // icon: SvgPicture.asset( + // Assets.svg.filter, + // width: 20, + // height: 20, + // color: Theme.of(context) + // .extension()! + // .topNavIconPrimary, + // ), + // onPressed: () { + // Navigator.of(context).pushNamed( + // OrdinalsFilterView.routeName, + // ); + // }, + // ), + // ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 8), child: Column( children: [ // ClipRRect( @@ -185,11 +176,7 @@ class _OrdinalsViewState extends ConsumerState { // const SizedBox( // height: 16, // ), - Expanded( - child: OrdinalsList( - walletId: widget.walletId, - ), - ), + Expanded(child: OrdinalsList(walletId: widget.walletId)), ], ), ), diff --git a/lib/pages/paynym/add_new_paynym_follow_view.dart b/lib/pages/paynym/add_new_paynym_follow_view.dart index 5f9e7bb71..85e4c3ac7 100644 --- a/lib/pages/paynym/add_new_paynym_follow_view.dart +++ b/lib/pages/paynym/add_new_paynym_follow_view.dart @@ -16,9 +16,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/paynym/paynym_account.dart'; import '../../providers/global/paynym_api_provider.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/constants.dart'; +import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../widgets/conditional_parent.dart'; @@ -40,10 +42,7 @@ import 'subwidgets/featured_paynyms_widget.dart'; import 'subwidgets/paynym_card.dart'; class AddNewPaynymFollowView extends ConsumerStatefulWidget { - const AddNewPaynymFollowView({ - super.key, - required this.walletId, - }); + const AddNewPaynymFollowView({super.key, required this.walletId}); final String walletId; @@ -73,9 +72,7 @@ class _AddNewPaynymFollowViewState showDialog( barrierDismissible: false, context: context, - builder: (context) => const LoadingIndicator( - width: 200, - ), + builder: (context) => const LoadingIndicator(width: 200), ).then((_) => didPopLoading = true), ); @@ -104,12 +101,7 @@ class _AddNewPaynymFollowViewState if (data?.text != null && data!.text!.isNotEmpty) { String content = data.text!.trim(); if (content.contains("\n")) { - content = content.substring( - 0, - content.indexOf( - "\n", - ), - ); + content = content.substring(0, content.indexOf("\n")); } _searchString = content; @@ -129,7 +121,7 @@ class _AddNewPaynymFollowViewState await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await const BarcodeScannerWrapper().scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final pCodeString = qrResult.rawContent; @@ -141,6 +133,21 @@ class _AddNewPaynymFollowViewState offset: pCodeString.length, ); }); + } on PlatformException catch (e, s) { + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } } catch (_) { // scan failed } @@ -166,100 +173,95 @@ class _AddNewPaynymFollowViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => MasterScaffold( - isDesktop: isDesktop, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Text( - "New follow", - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), + builder: + (child) => MasterScaffold( + isDesktop: isDesktop, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "New follow", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: + (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ), ), ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "New follow", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "New follow", - style: STextStyles.desktopH3(context), + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, ), + child: child, ), - const DesktopDialogCloseButton(), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: child, - ), - ], - ), - ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), Text( "Featured PayNyms", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.sectionLabelMedium12(context), - ), - const SizedBox( - height: 12, - ), - FeaturedPaynymsWidget( - walletId: widget.walletId, - ), - const SizedBox( - height: 24, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.sectionLabelMedium12(context), ), + const SizedBox(height: 12), + FeaturedPaynymsWidget(walletId: widget.walletId), + const SizedBox(height: 24), Text( "Add new", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.sectionLabelMedium12(context), - ), - const SizedBox( - height: 12, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.sectionLabelMedium12(context), ), + const SizedBox(height: 12), if (isDesktop) Row( children: [ @@ -268,9 +270,10 @@ class _AddNewPaynymFollowViewState children: [ RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, height: 56, child: Center( child: TextField( @@ -286,9 +289,10 @@ class _AddNewPaynymFollowViewState style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of(context) + .extension()! + .textFieldActiveText, // height: 1.8, ), decoration: InputDecoration( @@ -296,11 +300,9 @@ class _AddNewPaynymFollowViewState hoverColor: Colors.transparent, fillColor: Colors.transparent, contentPadding: const EdgeInsets.all(16), - hintStyle: - STextStyles.desktopTextFieldLabel(context) - .copyWith( - fontSize: 14, - ), + hintStyle: STextStyles.desktopTextFieldLabel( + context, + ).copyWith(fontSize: 14), enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, errorBorder: InputBorder.none, @@ -313,30 +315,38 @@ class _AddNewPaynymFollowViewState children: [ _searchController.text.isNotEmpty ? TextFieldIconButton( - onTap: _clear, - child: RoundedContainer( - padding: - const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: const XIcon(), + onTap: _clear, + child: RoundedContainer( + padding: const EdgeInsets.all( + 8, ), - ) + color: + Theme.of(context) + .extension< + StackColors + >()! + .buttonBackSecondary, + child: const XIcon(), + ), + ) : TextFieldIconButton( - key: const Key( - "paynymPasteAddressFieldButtonKey", - ), - onTap: _paste, - child: RoundedContainer( - padding: - const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: const ClipboardIcon(), + key: const Key( + "paynymPasteAddressFieldButtonKey", + ), + onTap: _paste, + child: RoundedContainer( + padding: const EdgeInsets.all( + 8, ), + color: + Theme.of(context) + .extension< + StackColors + >()! + .buttonBackSecondary, + child: const ClipboardIcon(), ), + ), TextFieldIconButton( key: const Key( "paynymScanQrButtonKey", @@ -344,9 +354,10 @@ class _AddNewPaynymFollowViewState onTap: _scanQr, child: RoundedContainer( padding: const EdgeInsets.all(8), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, child: const QrCodeIcon(), ), ), @@ -361,9 +372,7 @@ class _AddNewPaynymFollowViewState ], ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), PaynymSearchButton(onPressed: _search), ], ), @@ -396,16 +405,16 @@ class _AddNewPaynymFollowViewState children: [ _searchController.text.isNotEmpty ? TextFieldIconButton( - onTap: _clear, - child: const XIcon(), - ) + onTap: _clear, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "paynymPasteAddressFieldButtonKey", - ), - onTap: _paste, - child: const ClipboardIcon(), + key: const Key( + "paynymPasteAddressFieldButtonKey", ), + onTap: _paste, + child: const ClipboardIcon(), + ), TextFieldIconButton( key: const Key("paynymScanQrButtonKey"), onTap: _scanQr, @@ -418,34 +427,27 @@ class _AddNewPaynymFollowViewState ), ), ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop) - const SizedBox( - height: 12, - ), - if (!isDesktop) - SecondaryButton( - label: "Search", - onPressed: _search, - ), - if (_didSearch) - const SizedBox( - height: 20, - ), + SecondaryButton(label: "Search", onPressed: _search), + if (_didSearch) const SizedBox(height: 20), if (_didSearch && _searchResult == null) RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Nothing found. Please check the payment code.", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.label(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.label(context), ), ], ), @@ -453,11 +455,12 @@ class _AddNewPaynymFollowViewState if (_didSearch && _searchResult != null) RoundedWhiteContainer( padding: const EdgeInsets.all(0), - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, child: PaynymCard( key: UniqueKey(), label: _searchResult!.nymName, diff --git a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart index 6b08b84db..3185144f2 100644 --- a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart +++ b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart @@ -62,12 +62,11 @@ class _PaynymDetailsPopupState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - onWillPop: () async => canPop, - child: const LoadingIndicator( - width: 200, - ), - ), + builder: + (context) => WillPopScope( + onWillPop: () async => canPop, + child: const LoadingIndicator(width: 200), + ), ), ); @@ -111,45 +110,47 @@ class _PaynymDetailsPopupState extends ConsumerState { // show info pop up await showDialog( context: context, - builder: (context) => ConfirmPaynymConnectDialog( - nymName: widget.accountLite.nymName, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - onConfirmPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - unawaited( - showDialog( - context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmTransactionView( - walletId: widget.walletId, - isPaynymNotificationTransaction: true, - txData: preparedTx, - onSuccess: () { - // do nothing extra - }, - onSuccessInsteadOfRouteOnSuccess: () { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context, rootNavigator: true).pop(); - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: - "Connection initiated to ${widget.accountLite.nymName}", - iconAsset: Assets.svg.copy, - context: context, + builder: + (context) => ConfirmPaynymConnectDialog( + nymName: widget.accountLite.nymName, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + onConfirmPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + unawaited( + showDialog( + context: context, + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + walletId: widget.walletId, + isPaynymNotificationTransaction: true, + txData: preparedTx, + onSuccess: () { + // do nothing extra + }, + onSuccessInsteadOfRouteOnSuccess: () { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: + "Connection initiated to ${widget.accountLite.nymName}", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + }, + ), ), - ); - }, ), - ), - ), - ); - }, - amount: preparedTx.amount! + preparedTx.fee!, - coin: ref.read(pWalletCoin(widget.walletId)), - ), + ); + }, + amount: preparedTx.amount! + preparedTx.fee!, + coin: ref.read(pWalletCoin(widget.walletId)), + ), ); } } @@ -157,10 +158,11 @@ class _PaynymDetailsPopupState extends ConsumerState { Future _onSend() async { await showDialog( context: context, - builder: (context) => DesktopPaynymSendDialog( - walletId: widget.walletId, - accountLite: widget.accountLite, - ), + builder: + (context) => DesktopPaynymSendDialog( + walletId: widget.walletId, + accountLite: widget.accountLite, + ), ); } @@ -185,9 +187,7 @@ class _PaynymDetailsPopupState extends ConsumerState { paymentCodeString: widget.accountLite.code, size: 36, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -196,8 +196,9 @@ class _PaynymDetailsPopupState extends ConsumerState { style: STextStyles.desktopTextSmall(context), ), FutureBuilder( - future: paynymWallet - .hasConnected(widget.accountLite.code), + future: paynymWallet.hasConnected( + widget.accountLite.code, + ), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -205,16 +206,16 @@ class _PaynymDetailsPopupState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( "Connected", - style: STextStyles.desktopTextSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorGreen, + style: STextStyles.desktopTextSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorGreen, ), ), ], @@ -228,15 +229,14 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ], ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Row( children: [ Expanded( child: FutureBuilder( - future: - paynymWallet.hasConnected(widget.accountLite.code), + future: paynymWallet.hasConnected( + widget.accountLite.code, + ), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -249,9 +249,10 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.circleArrowUpRight, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: + Theme.of(context) + .extension()! + .buttonTextPrimary, ), iconSpacing: 6, onPressed: _onSend, @@ -264,9 +265,10 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.circlePlusFilled, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: + Theme.of(context) + .extension()! + .buttonTextPrimary, ), iconSpacing: 6, onPressed: _onConnectPressed, @@ -281,44 +283,41 @@ class _PaynymDetailsPopupState extends ConsumerState { }, ), ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), kDisableFollowing ? const Spacer() : Expanded( - child: PaynymFollowToggleButton( - walletId: widget.walletId, - paymentCodeStringToFollow: - widget.accountLite.code, - style: - PaynymFollowToggleButtonStyle.detailsDesktop, - ), + child: PaynymFollowToggleButton( + walletId: widget.walletId, + paymentCodeStringToFollow: widget.accountLite.code, + style: PaynymFollowToggleButtonStyle.detailsDesktop, ), + ), ], ), if (_showInsufficientFundsInfo) Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), RoundedContainer( - color: Theme.of(context) - .extension()! - .warningBackground, + color: + Theme.of( + context, + ).extension()!.warningBackground, child: Text( "Adding a PayNym to your contacts requires a one-time " "transaction fee for creating the record on the " "blockchain. Please deposit more " "${ref.watch(pWalletCoin(widget.walletId)).ticker} " "into your wallet and try again.", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.warningForeground, ), ), ), @@ -341,9 +340,7 @@ class _PaynymDetailsPopupState extends ConsumerState { "PayNym address", style: STextStyles.desktopTextExtraExtraSmall(context), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Row( children: [ Expanded( @@ -351,18 +348,18 @@ class _PaynymDetailsPopupState extends ConsumerState { constraints: const BoxConstraints(minHeight: 100), child: Text( widget.accountLite.code, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ), ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), QR( padding: const EdgeInsets.all(0), size: 100, @@ -370,16 +367,12 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), CustomTextButton( text: "Copy", onTap: () async { await Clipboard.setData( - ClipboardData( - text: widget.accountLite.code, - ), + ClipboardData(text: widget.accountLite.code), ); unawaited( showFloatingFlushBar( diff --git a/lib/pages/pinpad_views/create_pin_view.dart b/lib/pages/pinpad_views/create_pin_view.dart index 586e6c9fd..df1b4e557 100644 --- a/lib/pages/pinpad_views/create_pin_view.dart +++ b/lib/pages/pinpad_views/create_pin_view.dart @@ -26,6 +26,7 @@ import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_pin_put/custom_pin_put.dart'; import '../home_view/home_view.dart'; +import 'lock_screen_view.dart'; class CreatePinView extends ConsumerStatefulWidget { const CreatePinView({ @@ -55,8 +56,10 @@ class _CreatePinViewState extends ConsumerState { ); } - final PageController _pageController = - PageController(initialPage: 0, keepPage: true); + final PageController _pageController = PageController( + initialPage: 0, + keepPage: true, + ); // Attributes for Page 1 of the pageview final TextEditingController _pinPutController1 = TextEditingController(); @@ -118,27 +121,18 @@ class _CreatePinViewState extends ConsumerState { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - "Create a PIN", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), + Text("Create a PIN", style: STextStyles.pageTitleH1(context)), + const SizedBox(height: 8), Text( "This PIN protects access to your wallet.", style: STextStyles.subtitle(context), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, - ), + textStyle: STextStyles.label(context).copyWith(fontSize: 1), focusNode: _pinPutFocusNode1, controller: _pinPutController1, useNativeKeyboard: false, @@ -150,9 +144,10 @@ class _CreatePinViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, + fillColor: + Theme.of( + context, + ).extension()!.background, counterText: "", ), isRandom: @@ -188,28 +183,22 @@ class _CreatePinViewState extends ConsumerState { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - "Confirm PIN", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 8, - ), + Text("Confirm PIN", style: STextStyles.pageTitleH1(context)), + const SizedBox(height: 8), Text( "This PIN protects access to your wallet.", style: STextStyles.subtitle(context), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, textStyle: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, + color: + Theme.of( + context, + ).extension()!.textSubtitle3, fontSize: 1, ), focusNode: _pinPutFocusNode2, @@ -223,20 +212,23 @@ class _CreatePinViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, + fillColor: + Theme.of( + context, + ).extension()!.background, counterText: "", ), submittedFieldDecoration: _pinPutDecoration.copyWith( - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, border: Border.all( width: 1, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), ), selectedFieldDecoration: _pinPutDecoration, @@ -249,14 +241,15 @@ class _CreatePinViewState extends ConsumerState { if (_pinPutController1.text == _pinPutController2.text) { // ask if want to use biometrics - final bool useBiometrics = (Platform.isLinux) - ? false - : await biometrics.authenticate( - cancelButtonText: "SKIP", - localizedReason: - "You can use your fingerprint to unlock the wallet and confirm transactions.", - title: "Enable fingerprint authentication", - ); + final bool useBiometrics = + (Platform.isLinux) + ? false + : await biometrics.authenticate( + cancelButtonText: "SKIP", + localizedReason: + "You can use your fingerprint to unlock the wallet and confirm transactions.", + title: "Enable fingerprint authentication", + ); //TODO investigate why this crashes IOS, maybe ios persists securestorage even after an uninstall? // This should never fail as we are writing a new pin @@ -270,7 +263,7 @@ class _CreatePinViewState extends ConsumerState { ref.read(prefsChangeNotifierProvider).hasPin == false, ); - await _secureStore.write(key: "stack_pin", value: pin); + await _secureStore.write(key: kPinKey, value: pin); ref.read(prefsChangeNotifierProvider).useBiometrics = useBiometrics; @@ -280,11 +273,13 @@ class _CreatePinViewState extends ConsumerState { const Duration(milliseconds: 200), ); - if (mounted) { + if (context.mounted) { if (!widget.popOnSuccess) { - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), ); } else { Navigator.of(context).pop(); @@ -292,17 +287,21 @@ class _CreatePinViewState extends ConsumerState { } } else { // _onSubmitFailCount++; - _pageController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.linear, + unawaited( + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ), ); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "PIN codes do not match. Try again.", - context: context, - iconAsset: Assets.svg.alertCircle, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ), ); _pinPutController1.text = ''; diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index c7d6bb254..7a21a99c3 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -15,11 +15,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mutex/mutex.dart'; import '../../notifications/show_flush_bar.dart'; -// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; -import '../../providers/global/node_service_provider.dart'; -import '../../providers/global/prefs_provider.dart'; import '../../providers/global/secure_store_provider.dart'; -import '../../providers/global/wallets_provider.dart'; +import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; // import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart'; import '../../utilities/assets.dart'; @@ -30,6 +27,7 @@ import '../../utilities/show_node_tor_settings_mismatch.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../wallets/wallet/wallet.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; @@ -38,6 +36,9 @@ import '../../widgets/shake/shake.dart'; import '../home_view/home_view.dart'; import '../wallet_view/wallet_view.dart'; +const kPinKey = "stack_pin"; +const kDuressPinKey = "stack_pin_duress"; + class LockscreenView extends ConsumerStatefulWidget { const LockscreenView({ super.key, @@ -82,6 +83,54 @@ class _LockscreenViewState extends ConsumerState { static const maxAttemptsBeforeThrottling = 3; Timer? _timer; + Future _loadWallets(String? loadIntoWalletId) async { + await ref + .read(pWallets) + .load( + ref.read(prefsChangeNotifierProvider), + ref.read(mainDBProvider), + ref.read(pDuress), + ); + + if (loadIntoWalletId != null) { + final Wallet wallet; + try { + wallet = ref.read(pWallets).getWallet(loadIntoWalletId); + } catch (_) { + // wallet isn't marked as duress, continue without loading into it + return true; + } + + final canContinue = + mounted && + await checkShowNodeTorSettingsMismatch( + context: context, + currency: wallet.cryptoCurrency, + prefs: ref.read(prefsChangeNotifierProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + allowCancel: false, + rootNavigator: Util.isDesktop, + ); + + if (!canContinue) { + return false; + } + + final Future loadFuture; + if (wallet is ExternalWallet) { + loadFuture = wallet.init().then( + (value) async => await (wallet as ExternalWallet).open(), + ); + } else { + loadFuture = wallet.init(); + } + + await loadFuture; + } + + return true; + } + Future _onUnlock() async { final now = DateTime.now().toUtc(); ref.read(prefsChangeNotifierProvider).lastUnlocked = @@ -97,41 +146,23 @@ class _LockscreenViewState extends ConsumerState { if (widget.popOnSuccess) { Navigator.of(context).pop(widget.routeOnSuccessArguments); } else { - final loadIntoWallet = widget.routeOnSuccess == HomeView.routeName && + final loadIntoWallet = + widget.routeOnSuccess == HomeView.routeName && widget.routeOnSuccessArguments is String; - if (loadIntoWallet) { - final walletId = widget.routeOnSuccessArguments as String; - - final wallet = ref.read(pWallets).getWallet(walletId); - - final canContinue = await checkShowNodeTorSettingsMismatch( + if (widget.isInitialAppLogin) { + final loadSuccess = await showLoading( + whileFuture: _loadWallets( + loadIntoWallet ? widget.routeOnSuccessArguments as String : null, + ), + opaqueBG: true, context: context, - currency: wallet.cryptoCurrency, - prefs: ref.read(prefsChangeNotifierProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - allowCancel: false, - rootNavigator: Util.isDesktop, + message: "Loading wallets...", ); - if (!canContinue) { + if (loadSuccess == null || loadSuccess == false) { return; } - - final Future loadFuture; - if (wallet is ExternalWallet) { - loadFuture = - wallet.init().then((value) async => await (wallet).open()); - } else { - loadFuture = wallet.init(); - } - - await showLoading( - opaqueBG: true, - whileFuture: loadFuture, - context: context, - message: "Loading ${wallet.info.coin.prettyName} wallet...", - ); } if (mounted) { @@ -146,10 +177,9 @@ class _LockscreenViewState extends ConsumerState { final walletId = widget.routeOnSuccessArguments as String; unawaited( - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: walletId, - ), + Navigator.of( + context, + ).pushNamed(WalletView.routeName, arguments: walletId), ); } } @@ -169,11 +199,30 @@ class _LockscreenViewState extends ConsumerState { final cancelButtonText = widget.biometricsCancelButtonString; if (useBiometrics) { - if (await biometrics.authenticate( - title: title, - localizedReason: localizedReason, - cancelButtonText: cancelButtonText, - )) { + final bool canTryUnlock; + if (widget.isInitialAppLogin) { + final hasDuressPin = + (await _secureStore.read(key: kDuressPinKey)) != null; + + ref.read(pDuress.notifier).state = + hasDuressPin && + ref.read(prefsChangeNotifierProvider).biometricsDuress; + + canTryUnlock = true; + } else { + if (ref.read(pDuress)) { + canTryUnlock = ref.read(prefsChangeNotifierProvider).biometricsDuress; + } else { + canTryUnlock = true; + } + } + + if (canTryUnlock && + (await biometrics.authenticate( + title: title, + localizedReason: localizedReason, + cancelButtonText: cancelButtonText, + ))) { // check if initial log in // if (widget.routeOnSuccess == "/mainview") { // await logIn(await walletsService.networkName, currentWalletName, @@ -239,6 +288,29 @@ class _LockscreenViewState extends ConsumerState { final _pinTextController = TextEditingController(); + Future _checkCanUnlock(String pin) async { + final bool canUnlock; + if (widget.isInitialAppLogin) { + final storedPin = await _secureStore.read(key: kPinKey); + final duressPin = await _secureStore.read(key: kDuressPinKey); + if (pin == storedPin || pin == duressPin) { + ref.read(pDuress.notifier).state = pin == duressPin; + canUnlock = true; + } else { + canUnlock = false; + } + } else { + if (ref.read(pDuress)) { + final duressPin = await _secureStore.read(key: kDuressPinKey); + canUnlock = pin == duressPin; + } else { + final storedPin = await _secureStore.read(key: kPinKey); + canUnlock = pin == storedPin; + } + } + return canUnlock; + } + final Mutex _autoPinCheckLock = Mutex(); void _onPinChangedAutologinCheck() async { if (mounted) { @@ -247,11 +319,8 @@ class _LockscreenViewState extends ConsumerState { try { if (_autoPin && _pinTextController.text.length >= 4) { - final storedPin = await _secureStore.read(key: 'stack_pin'); - if (_pinTextController.text == storedPin) { - await Future.delayed( - const Duration(milliseconds: 200), - ); + if (await _checkCanUnlock(_pinTextController.text)) { + await Future.delayed(const Duration(milliseconds: 200)); unawaited(_onUnlock()); } } @@ -319,21 +388,15 @@ class _LockscreenViewState extends ConsumerState { ), ); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); _pinTextController.text = ''; return; } - final storedPin = await _secureStore.read(key: 'stack_pin'); - - if (storedPin == pin) { - await Future.delayed( - const Duration(milliseconds: 200), - ); + if (await _checkCanUnlock(pin)) { + await Future.delayed(const Duration(milliseconds: 200)); unawaited(_onUnlock()); } else { unawaited(_shakeController.shake()); @@ -348,136 +411,128 @@ class _LockscreenViewState extends ConsumerState { ); } - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); _pinTextController.text = ''; } } Widget get _body => Background( - child: SafeArea( - child: Scaffold( - extendBodyBehindAppBar: true, - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: widget.showBackButton + child: SafeArea( + child: Scaffold( + extendBodyBehindAppBar: true, + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: + widget.showBackButton ? AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 70), - ); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ) + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 70), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ) : Container(), - actions: [ - // check prefs and hide if user has biometrics toggle off? - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - right: 16.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (ref - .read(prefsChangeNotifierProvider) - .useBiometrics == - true) - CustomTextButton( - text: "Use biometrics", - onTap: () async { - await _checkUseBiometrics(); - }, - ), - ], - ), - ), - ], - ), - ], - ), - body: Column( + actions: [ + // check prefs and hide if user has biometrics toggle off? + Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Shake( - animationDuration: const Duration(milliseconds: 700), - animationRange: 12, - controller: _shakeController, - child: Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Enter PIN", - style: STextStyles.pageTitleH1(context), - ), - ), - const SizedBox( - height: 52, - ), - CustomPinPut( - fieldsCount: pinCount, - eachFieldHeight: 12, - eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, - ), - focusNode: _pinFocusNode, - controller: _pinTextController, - useNativeKeyboard: false, - obscureText: "", - inputDecoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, - counterText: "", - ), - submittedFieldDecoration: _pinPutDecoration, - isRandom: ref - .read(prefsChangeNotifierProvider) - .randomizePIN, - onSubmit: (pin) { - if (!_autoPinCheckLock.isLocked) { - _onSubmitPin(pin); - } + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (ref.read(prefsChangeNotifierProvider).useBiometrics == + true) + CustomTextButton( + text: "Use biometrics", + onTap: () async { + await _checkUseBiometrics(); }, ), - ], - ), + ], ), ), ], ), - ), + ], ), - ); + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Shake( + animationDuration: const Duration(milliseconds: 700), + animationRange: 12, + controller: _shakeController, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Enter PIN", + style: STextStyles.pageTitleH1(context), + ), + ), + const SizedBox(height: 52), + CustomPinPut( + fieldsCount: pinCount, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label( + context, + ).copyWith(fontSize: 1), + focusNode: _pinFocusNode, + controller: _pinTextController, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: + Theme.of( + context, + ).extension()!.background, + counterText: "", + ), + submittedFieldDecoration: _pinPutDecoration, + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, + onSubmit: (pin) { + if (!_autoPinCheckLock.isLocked) { + _onSubmitPin(pin); + } + }, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); @override Widget build(BuildContext context) { return widget.showBackButton ? _body : WillPopScope( - onWillPop: () async { - return widget.showBackButton; - }, - child: _body, - ); + onWillPop: () async { + return widget.showBackButton; + }, + child: _body, + ); } } diff --git a/lib/pages/pinpad_views/pinpad_dialog.dart b/lib/pages/pinpad_views/pinpad_dialog.dart index 68dbc144c..f962632d0 100644 --- a/lib/pages/pinpad_views/pinpad_dialog.dart +++ b/lib/pages/pinpad_views/pinpad_dialog.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mutex/mutex.dart'; import '../../notifications/show_flush_bar.dart'; +import '../../providers/global/duress_provider.dart'; import '../../providers/global/prefs_provider.dart'; import '../../providers/global/secure_store_provider.dart'; import '../../themes/stack_colors.dart'; @@ -15,6 +16,7 @@ import '../../utilities/text_styles.dart'; import '../../widgets/custom_pin_put/custom_pin_put.dart'; import '../../widgets/shake/shake.dart'; import '../../widgets/stack_dialog.dart'; +import 'lock_screen_view.dart'; class PinpadDialog extends ConsumerStatefulWidget { const PinpadDialog({ @@ -74,11 +76,14 @@ class _PinpadDialogState extends ConsumerState { try { if (_autoPin && _pinTextController.text.length >= 4) { - final storedPin = await _secureStore.read(key: 'stack_pin'); + final String? storedPin; + if (ref.read(pDuress)) { + storedPin = await _secureStore.read(key: kDuressPinKey); + } else { + storedPin = await _secureStore.read(key: kPinKey); + } if (_pinTextController.text == storedPin) { - await Future.delayed( - const Duration(milliseconds: 200), - ); + await Future.delayed(const Duration(milliseconds: 200)); unawaited(_onUnlock()); } } @@ -181,22 +186,23 @@ class _PinpadDialogState extends ConsumerState { ), ); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); _pinTextController.text = ''; return; } - final storedPin = await _secureStore.read(key: 'stack_pin'); + final String? storedPin; + if (ref.read(pDuress)) { + storedPin = await _secureStore.read(key: kDuressPinKey); + } else { + storedPin = await _secureStore.read(key: kPinKey); + } if (mounted) { if (storedPin == pin) { - await Future.delayed( - const Duration(milliseconds: 200), - ); + await Future.delayed(const Duration(milliseconds: 200)); unawaited(_onUnlock()); } else { unawaited(_shakeController.shake()); @@ -209,9 +215,7 @@ class _PinpadDialogState extends ConsumerState { ), ); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); _pinTextController.text = ''; } @@ -262,16 +266,12 @@ class _PinpadDialogState extends ConsumerState { style: STextStyles.pageTitleH1(context), ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, - ), + textStyle: STextStyles.label(context).copyWith(fontSize: 1), focusNode: _pinFocusNode, controller: _pinTextController, useNativeKeyboard: false, @@ -296,9 +296,7 @@ class _PinpadDialogState extends ConsumerState { } }, ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), ], ), ), diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index 25e18becb..96c2e66a6 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -70,60 +70,60 @@ class _AddressDetailsViewState extends ConsumerState { void _showDesktopAddressQrCode() { showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 480, - maxHeight: 400, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 480, + maxHeight: 400, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Address QR code", - style: STextStyles.desktopH3(context), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Address QR code", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: RepaintBoundary( - key: _qrKey, - child: QR( - data: AddressUtils.buildUriString( - ref.watch(pWalletCoin(widget.walletId)).uriScheme, - address.value, - {}, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: RepaintBoundary( + key: _qrKey, + child: QR( + data: AddressUtils.buildUriString( + ref.watch(pWalletCoin(widget.walletId)).uriScheme, + address.value, + {}, + ), + size: 220, + ), ), - size: 220, ), - ), + ], ), - ], - ), - ), - const SizedBox( - height: 32, + ), + const SizedBox(height: 32), + ], ), - ], - ), - ), + ), ); } @override void initState() { - address = MainDB.instance.isar.addresses - .where() - .idEqualTo(widget.addressId) - .findFirstSync()!; + address = + MainDB.instance.isar.addresses + .where() + .idEqualTo(widget.addressId) + .findFirstSync()!; label = MainDB.instance.getAddressLabelSync(widget.walletId, address.value); Id? id = label?.id; @@ -132,9 +132,10 @@ class _AddressDetailsViewState extends ConsumerState { walletId: widget.walletId, addressString: address.value, value: "", - tags: address.subType == AddressSubType.receiving - ? ["receiving"] - : address.subType == AddressSubType.change + tags: + address.subType == AddressSubType.receiving + ? ["receiving"] + : address.subType == AddressSubType.change ? ["change"] : null, ); @@ -151,43 +152,46 @@ class _AddressDetailsViewState extends ConsumerState { final wallet = ref.watch(pWallets).getWallet(widget.walletId); return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.backgroundAppBar, - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Text( - "Address details", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ); - }, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Address details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + }, + ), + ), ), ), - ), - ), child: StreamBuilder( stream: stream, builder: (context, snapshot) { @@ -200,9 +204,7 @@ class _AddressDetailsViewState extends ConsumerState { builder: (child) { return Column( children: [ - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), RoundedWhiteContainer( padding: const EdgeInsets.all(24), child: Column( @@ -215,9 +217,10 @@ class _AddressDetailsViewState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), CustomTextButton( @@ -226,19 +229,16 @@ class _AddressDetailsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), RoundedWhiteContainer( padding: EdgeInsets.zero, - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, + borderColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, child: child, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -247,34 +247,35 @@ class _AddressDetailsViewState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( padding: EdgeInsets.zero, - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, - child: ref - .watch(pWallets) - .getWallet(widget.walletId) - .isarTransactionVersion == - 2 - ? _AddressDetailsTxV2List( - walletId: widget.walletId, - address: address, - ) - : _AddressDetailsTxList( - walletId: widget.walletId, - address: address, - ), + borderColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + child: + ref + .watch(pWallets) + .getWallet(widget.walletId) + .isarTransactionVersion == + 2 + ? _AddressDetailsTxV2List( + walletId: widget.walletId, + address: address, + ) + : _AddressDetailsTxList( + walletId: widget.walletId, + address: address, + ), ), ], ), @@ -299,24 +300,16 @@ class _AddressDetailsViewState extends ConsumerState { ), ), ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), DetailItem( title: "Address", detail: address.value, - button: isDesktop - ? IconCopyButton( - data: address.value, - ) - : SimpleCopyButton( - data: address.value, - ), - ), - const _Div( - height: 12, + button: + isDesktop + ? IconCopyButton(data: address.value) + : SimpleCopyButton(data: address.value), ), + const _Div(height: 12), DetailItem( title: "Label", detail: label!.value, @@ -325,59 +318,47 @@ class _AddressDetailsViewState extends ConsumerState { editLabel: 'label', onValueChanged: (value) { MainDB.instance.putAddressLabel( - label!.copyWith( - label: value, - ), + label!.copyWith(label: value), ); }, ), ), - const _Div( - height: 12, - ), - _Tags( - tags: label!.tags, - ), - if (address.derivationPath != null) - const _Div( - height: 12, - ), + const _Div(height: 12), + _Tags(tags: label!.tags), + if (address.derivationPath != null) const _Div(height: 12), if (address.derivationPath != null) DetailItem( title: "Derivation path", detail: address.derivationPath!.value, button: Container(), ), - if (address.type == AddressType.spark) - const _Div( - height: 12, - ), + if (address.type == AddressType.spark) const _Div(height: 12), if (address.type == AddressType.spark) DetailItem( title: "Diversifier", detail: address.derivationIndex.toString(), button: Container(), ), - const _Div( - height: 12, - ), + if (address.type == AddressType.mweb) const _Div(height: 12), + if (address.type == AddressType.mweb) + DetailItem( + title: "Index", + detail: address.derivationIndex.toString(), + button: Container(), + ), + const _Div(height: 12), DetailItem( title: "Type", detail: address.type.readableName, button: Container(), ), - const _Div( - height: 12, - ), + const _Div(height: 12), DetailItem( title: "Sub type", detail: address.subType.prettyName, button: Container(), ), - if (kDebugMode) - const _Div( - height: 12, - ), + if (kDebugMode) const _Div(height: 12), if (kDebugMode) DetailItem( title: "frost secure (kDebugMode)", @@ -385,18 +366,13 @@ class _AddressDetailsViewState extends ConsumerState { button: Container(), ), if (wallet is Bip39HDWallet && !wallet.isViewOnly) - const _Div( - height: 12, - ), + const _Div(height: 12), if (wallet is Bip39HDWallet && !wallet.isViewOnly) AddressPrivateKey( walletId: widget.walletId, address: address, ), - if (!isDesktop) - const SizedBox( - height: 20, - ), + if (!isDesktop) const SizedBox(height: 20), if (!isDesktop) Text( "Transactions", @@ -406,10 +382,7 @@ class _AddressDetailsViewState extends ConsumerState { Theme.of(context).extension()!.textDark3, ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop) ref .watch(pWallets) @@ -417,13 +390,13 @@ class _AddressDetailsViewState extends ConsumerState { .isarTransactionVersion == 2 ? _AddressDetailsTxV2List( - walletId: widget.walletId, - address: address, - ) + walletId: widget.walletId, + address: address, + ) : _AddressDetailsTxList( - walletId: widget.walletId, - address: address, - ), + walletId: widget.walletId, + address: address, + ), ], ), ); @@ -458,10 +431,9 @@ class _AddressDetailsTxList extends StatelessWidget { return ListView.separated( shrinkWrap: true, primary: false, - itemBuilder: (_, index) => TransactionCard( - transaction: txns[index], - walletId: walletId, - ), + itemBuilder: + (_, index) => + TransactionCard(transaction: txns[index], walletId: walletId), separatorBuilder: (_, __) => const _Div(height: 1), itemCount: count, ); @@ -470,15 +442,14 @@ class _AddressDetailsTxList extends StatelessWidget { padding: EdgeInsets.zero, child: Column( mainAxisSize: MainAxisSize.min, - children: query - .findAllSync() - .map( - (e) => TransactionCard( - transaction: e, - walletId: walletId, - ), - ) - .toList(), + children: + query + .findAllSync() + .map( + (e) => + TransactionCard(transaction: e, walletId: walletId), + ) + .toList(), ), ); } @@ -503,40 +474,35 @@ class _AddressDetailsTxV2List extends ConsumerWidget { final walletTxFilter = ref.watch(pWallets).getWallet(walletId).transactionFilterOperation; - final query = - ref.watch(mainDBProvider).isar.transactionV2s.buildQuery( - whereClauses: [ - IndexWhereClause.equalTo( - indexName: 'walletId', - value: [walletId], + final query = ref + .watch(mainDBProvider) + .isar + .transactionV2s + .buildQuery( + whereClauses: [ + IndexWhereClause.equalTo(indexName: 'walletId', value: [walletId]), + ], + filter: FilterGroup.and([ + if (walletTxFilter != null) walletTxFilter, + FilterGroup.or([ + ObjectFilter( + property: 'inputs', + filter: FilterCondition.contains( + property: "addresses", + value: address.value, ), - ], - filter: FilterGroup.and([ - if (walletTxFilter != null) walletTxFilter, - FilterGroup.or([ - ObjectFilter( - property: 'inputs', - filter: FilterCondition.contains( - property: "addresses", - value: address.value, - ), - ), - ObjectFilter( - property: 'outputs', - filter: FilterCondition.contains( - property: "addresses", - value: address.value, - ), - ), - ]), - ]), - sortBy: [ - const SortProperty( - property: "timestamp", - sort: Sort.desc, + ), + ObjectFilter( + property: 'outputs', + filter: FilterCondition.contains( + property: "addresses", + value: address.value, ), - ], - ); + ), + ]), + ]), + sortBy: [const SortProperty(property: "timestamp", sort: Sort.desc)], + ); final count = query.countSync(); @@ -546,9 +512,8 @@ class _AddressDetailsTxV2List extends ConsumerWidget { return ListView.separated( shrinkWrap: true, primary: false, - itemBuilder: (_, index) => TransactionCardV2( - transaction: txns[index], - ), + itemBuilder: + (_, index) => TransactionCardV2(transaction: txns[index]), separatorBuilder: (_, __) => const _Div(height: 1), itemCount: count, ); @@ -557,14 +522,11 @@ class _AddressDetailsTxV2List extends ConsumerWidget { padding: EdgeInsets.zero, child: Column( mainAxisSize: MainAxisSize.min, - children: query - .findAllSync() - .map( - (e) => TransactionCardV2( - transaction: e, - ), - ) - .toList(), + children: + query + .findAllSync() + .map((e) => TransactionCardV2(transaction: e)) + .toList(), ), ); } @@ -575,10 +537,7 @@ class _AddressDetailsTxV2List extends ConsumerWidget { } class _Div extends StatelessWidget { - const _Div({ - super.key, - required this.height, - }); + const _Div({super.key, required this.height}); final double height; @@ -591,18 +550,13 @@ class _Div extends StatelessWidget { width: double.infinity, ); } else { - return SizedBox( - height: height, - ); + return SizedBox(height: height); } } } class _Tags extends StatelessWidget { - const _Tags({ - super.key, - required this.tags, - }); + const _Tags({super.key, required this.tags}); final List? tags; @@ -615,10 +569,7 @@ class _Tags extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Tags", - style: STextStyles.itemSubtitle(context), - ), + Text("Tags", style: STextStyles.itemSubtitle(context)), Container(), // SimpleEditButton( // onPressedOverride: () { @@ -627,29 +578,20 @@ class _Tags extends StatelessWidget { // ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), tags != null && tags!.isNotEmpty ? Wrap( - spacing: 10, - runSpacing: 10, - children: tags! - .map( - (e) => AddressTag( - tag: e, - ), - ) - .toList(), - ) + spacing: 10, + runSpacing: 10, + children: tags!.map((e) => AddressTag(tag: e)).toList(), + ) : Text( - "Tags will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, - ), + "Tags will appear here", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle3, ), + ), ], ), ); diff --git a/lib/pages/receive_view/addresses/edit_address_label_view.dart b/lib/pages/receive_view/addresses/edit_address_label_view.dart index 2e6052690..f0508f4a5 100644 --- a/lib/pages/receive_view/addresses/edit_address_label_view.dart +++ b/lib/pages/receive_view/addresses/edit_address_label_view.dart @@ -28,10 +28,7 @@ import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; class EditAddressLabelView extends ConsumerStatefulWidget { - const EditAddressLabelView({ - super.key, - required this.addressLabelId, - }); + const EditAddressLabelView({super.key, required this.addressLabelId}); static const String routeName = "/editAddressLabel"; @@ -54,10 +51,11 @@ class _EditAddressLabelViewState extends ConsumerState { void initState() { isDesktop = Util.isDesktop; _labelFieldController = TextEditingController(); - addressLabel = MainDB.instance.isar.addressLabels - .where() - .idEqualTo(widget.addressLabelId) - .findFirstSync()!; + addressLabel = + MainDB.instance.isar.addressLabels + .where() + .idEqualTo(widget.addressLabelId) + .findFirstSync()!; _labelFieldController.text = addressLabel.value; super.initState(); } @@ -73,145 +71,163 @@ class _EditAddressLabelViewState extends ConsumerState { Widget build(BuildContext context) { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: child, - ), + builder: (child) => Background(child: child), child: Scaffold( - backgroundColor: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.background, - appBar: isDesktop - ? null - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, + backgroundColor: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.background, + appBar: + isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit label", + style: STextStyles.navBarTitle(context), + ), ), - title: Text( - "Edit label", - style: STextStyles.navBarTitle(context), + body: SafeArea( + child: ConditionalParent( + condition: !isDesktop, + builder: + (child) => Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ); + }, + ), ), - ), - body: ConditionalParent( - condition: !isDesktop, - builder: (child) => Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + Padding( + padding: const EdgeInsets.only(left: 32, bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Edit label", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], ), ), - ); - }, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (isDesktop) Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 12, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Edit label", - style: STextStyles.desktopH3(context), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _labelFieldController, + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + focusNode: labelFieldFocusNode, + decoration: standardInputDecoration( + "Address label", + labelFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: + isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: + _labelFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _labelFieldController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), - const DesktopDialogCloseButton(), - ], + ), ), ), - Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _labelFieldController, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - focusNode: labelFieldFocusNode, - decoration: standardInputDecoration( - "Address label", - labelFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, - ) - : null, - suffixIcon: _labelFieldController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _labelFieldController.text = ""; - }); - }, - ), - ], - ), - ), - ) - : null, + // if (!isDesktop) + const Spacer(), + if (isDesktop) + Padding( + padding: const EdgeInsets.all(32), + child: PrimaryButton( + label: "Save", + onPressed: () async { + await MainDB.instance.updateAddressLabel( + addressLabel.copyWith( + label: _labelFieldController.text, + ), + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, ), ), - ), - ), - // if (!isDesktop) - const Spacer(), - if (isDesktop) - Padding( - padding: const EdgeInsets.all(32), - child: PrimaryButton( - label: "Save", + if (!isDesktop) + TextButton( onPressed: () async { await MainDB.instance.updateAddressLabel( addressLabel.copyWith( @@ -222,29 +238,13 @@ class _EditAddressLabelViewState extends ConsumerState { Navigator.of(context).pop(); } }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text("Save", style: STextStyles.button(context)), ), - ), - if (!isDesktop) - TextButton( - onPressed: () async { - await MainDB.instance.updateAddressLabel( - addressLabel.copyWith( - label: _labelFieldController.text, - ), - ); - if (mounted) { - Navigator.of(context).pop(); - } - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/receive_view/addresses/wallet_addresses_view.dart b/lib/pages/receive_view/addresses/wallet_addresses_view.dart index 6c1441b43..8b87fd074 100644 --- a/lib/pages/receive_view/addresses/wallet_addresses_view.dart +++ b/lib/pages/receive_view/addresses/wallet_addresses_view.dart @@ -27,10 +27,7 @@ import 'address_card.dart'; import 'address_details_view.dart'; class WalletAddressesView extends ConsumerStatefulWidget { - const WalletAddressesView({ - super.key, - required this.walletId, - }); + const WalletAddressesView({super.key, required this.walletId}); static const String routeName = "/walletAddressesView"; @@ -85,23 +82,24 @@ class _WalletAddressesViewState extends ConsumerState { .findAll(); } - final labels = await MainDB.instance - .getAddressLabels(widget.walletId) - .filter() - .group( - (q) => q - .valueContains(term, caseSensitive: false) - .or() - .addressStringContains(term, caseSensitive: false) - .or() - .group( - (q) => q - .tagsIsNotNull() - .and() - .tagsElementContains(term, caseSensitive: false), - ), - ) - .findAll(); + final labels = + await MainDB.instance + .getAddressLabels(widget.walletId) + .filter() + .group( + (q) => q + .valueContains(term, caseSensitive: false) + .or() + .addressStringContains(term, caseSensitive: false) + .or() + .group( + (q) => q.tagsIsNotNull().and().tagsElementContains( + term, + caseSensitive: false, + ), + ), + ) + .findAll(); if (labels.isEmpty) { return []; @@ -165,139 +163,137 @@ class _WalletAddressesViewState extends ConsumerState { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.backgroundAppBar, - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Text( - "Wallet addresses", - style: STextStyles.navBarTitle(context), + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Wallet addresses", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding(padding: const EdgeInsets.all(16), child: child), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - 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, - // ), - Expanded( - child: FutureBuilder( - future: _search(_searchString), - builder: (context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.data != null) { - // listview - return ListView.separated( - itemCount: snapshot.data!.length, - separatorBuilder: (_, __) => Container( - height: 10, - ), - itemBuilder: (_, index) => AddressCard( - walletId: widget.walletId, - addressId: snapshot.data![index], - coin: coin, - onPressed: () { - Navigator.of(context).pushNamed( - AddressDetailsView.routeName, - arguments: Tuple2( - snapshot.data![index], - widget.walletId, + child: SafeArea( + 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, + // ), + Expanded( + child: FutureBuilder( + future: _search(_searchString), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.data != null) { + // listview + return ListView.separated( + itemCount: snapshot.data!.length, + separatorBuilder: (_, __) => Container(height: 10), + itemBuilder: + (_, index) => AddressCard( + walletId: widget.walletId, + addressId: snapshot.data![index], + coin: coin, + onPressed: () { + Navigator.of(context).pushNamed( + AddressDetailsView.routeName, + arguments: Tuple2( + snapshot.data![index], + widget.walletId, + ), + ); + }, ), - ); - }, - ), - ); - } else { - return const Center( - child: LoadingIndicator( - height: 200, - width: 200, - ), - ); - } - }, + ); + } else { + return const Center( + child: LoadingIndicator(height: 200, width: 200), + ); + } + }, + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 7b02e525f..d485cfa14 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -80,8 +80,9 @@ class _GenerateUriQrCodeViewState extends State { 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 ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); final Uint8List pngBytes = byteData!.buffer.asUint8List(); if (shouldSaveInsteadOfShare) { @@ -131,10 +132,9 @@ class _GenerateUriQrCodeViewState extends State { 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", - ); + await Share.shareFiles([ + "${tempDir.path}/qrcode.png", + ], text: "Receive URI QR Code"); } } catch (e) { //todo: comeback to this @@ -216,25 +216,18 @@ class _GenerateUriQrCodeViewState extends State { style: STextStyles.pageTitleH2(context), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Center( child: RepaintBoundary( key: _qrKey, child: SizedBox( width: width + 20, height: width + 20, - child: QR( - data: uriString, - size: width, - ), + child: QR(data: uriString, size: width), ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Center( child: SizedBox( width: width, @@ -244,9 +237,10 @@ class _GenerateUriQrCodeViewState extends State { Assets.svg.share, width: 14, height: 14, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), onPressed: () async { await _capturePng(false); @@ -299,62 +293,68 @@ class _GenerateUriQrCodeViewState extends State { return ConditionalParent( condition: !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: 70)); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Generate QR code", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (buildContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + 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: 70), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + title: Text( + "Generate QR code", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (buildContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - ), - ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, ), - ); - }, + ), + ), ), - ), - ), child: Padding( - padding: isDesktop - ? const EdgeInsets.only( - top: 12, - left: 32, - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.only( + top: 12, + left: 32, + right: 32, + bottom: 32, + ) + : const EdgeInsets.all(0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, @@ -366,24 +366,23 @@ class _GenerateUriQrCodeViewState extends State { style: STextStyles.itemSubtitle(context), ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), Text( "Amount (Optional)", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - SizedBox( - height: isDesktop ? 10 : 8, - ), + SizedBox(height: isDesktop ? 10 : 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -393,70 +392,77 @@ class _GenerateUriQrCodeViewState extends State { enableSuggestions: Util.isDesktop ? false : true, controller: amountController, focusNode: _amountFocusNode, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, - height: 1.8, - ) - : STextStyles.field(context), - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions(decimal: true), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, + height: 1.8, + ) + : STextStyles.field(context), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions(decimal: true), onChanged: (_) => setState(() {}), decoration: standardInputDecoration( "Amount", _amountFocusNode, context, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, - ) - : null, - suffixIcon: amountController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - amountController.text = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: + amountController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + amountController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), - SizedBox( - height: isDesktop ? 20 : 12, - ), + SizedBox(height: isDesktop ? 20 : 12), Text( "Note (Optional)", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ) + : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - SizedBox( - height: isDesktop ? 10 : 8, - ), + SizedBox(height: isDesktop ? 10 : 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -466,68 +472,73 @@ class _GenerateUriQrCodeViewState extends State { enableSuggestions: Util.isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, + height: 1.8, + ) + : STextStyles.field(context), onChanged: (_) => setState(() {}), decoration: standardInputDecoration( "Note", _noteFocusNode, context, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, - ) - : null, - 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 = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + 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, + ) + : null, ), ), ), - SizedBox( - height: isDesktop ? 20 : 8, - ), + SizedBox(height: isDesktop ? 20 : 8), PrimaryButton( label: "Generate QR code", - onPressed: isDesktop - ? () { - final uriString = _generateURI(); - if (uriString == null) { - return; + onPressed: + isDesktop + ? () { + final uriString = _generateURI(); + if (uriString == null) { + return; + } + + setState(() { + didGenerate = true; + _uriString = uriString; + }); } - - setState(() { - didGenerate = true; - _uriString = uriString; - }); - } - : onGeneratePressed, + : onGeneratePressed, buttonHeight: isDesktop ? ButtonHeight.l : null, ), if (isDesktop && didGenerate) @@ -538,13 +549,12 @@ class _GenerateUriQrCodeViewState extends State { Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), RoundedWhiteContainer( - borderColor: Theme.of(context) - .extension()! - .background, + borderColor: + Theme.of( + context, + ).extension()!.background, width: isDesktop ? 370 : null, child: Column( children: [ @@ -552,29 +562,23 @@ class _GenerateUriQrCodeViewState extends State { "New QR Code", style: STextStyles.desktopTextMedium(context), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Center( child: RepaintBoundary( key: _qrKey, child: SizedBox( width: 234, height: 234, - child: QR( - data: _uriString, - size: 220, - ), + child: QR(data: _uriString, size: 220), ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Row( - mainAxisAlignment: isDesktop - ? MainAxisAlignment.center - : MainAxisAlignment.start, + mainAxisAlignment: + isDesktop + ? MainAxisAlignment.center + : MainAxisAlignment.start, children: [ if (!isDesktop) SecondaryButton( @@ -589,15 +593,13 @@ class _GenerateUriQrCodeViewState extends State { Assets.svg.share, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), ), - if (!isDesktop) - const SizedBox( - width: 16, - ), + if (!isDesktop) const SizedBox(width: 16), PrimaryButton( width: 170, buttonHeight: @@ -612,9 +614,10 @@ class _GenerateUriQrCodeViewState extends State { Assets.svg.arrowDown, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: + Theme.of(context) + .extension()! + .buttonTextPrimary, ), ), ], diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 43a5c6c8d..fe44acc9b 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -20,7 +20,6 @@ import 'package:isar/isar.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/keys/view_only_wallet_data.dart'; import '../../notifications/show_flush_bar.dart'; -import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; import '../../themes/stack_colors.dart'; @@ -29,6 +28,7 @@ import '../../utilities/assets.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/derive_path_type_enum.dart'; +import '../../utilities/show_loading.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; @@ -37,6 +37,7 @@ import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../widgets/background.dart'; @@ -75,6 +76,7 @@ class _ReceiveViewState extends ConsumerState { late final ClipboardInterface clipboard; late final bool _supportsSpark; late final bool _showMultiType; + late bool supportsMweb; int _currentIndex = 0; @@ -94,10 +96,9 @@ class _ReceiveViewState extends ConsumerState { return WillPopScope( onWillPop: () async => shouldPop, child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -112,8 +113,9 @@ class _ReceiveViewState extends ConsumerState { if (wallet is Bip39HDWallet && wallet is! BCashInterface) { DerivePathType? type; if (wallet.isViewOnly && wallet is ExtendedKeysInterface) { - final voData = await wallet.getViewOnlyWalletData() - as ExtendedKeysViewOnlyWalletData; + final voData = + await wallet.getViewOnlyWalletData() + as ExtendedKeysViewOnlyWalletData; for (final t in wallet.cryptoCurrency.supportedDerivationPathTypes) { final testPath = wallet.cryptoCurrency.constructDerivePath( derivePathType: t, @@ -151,8 +153,9 @@ class _ReceiveViewState extends ConsumerState { shouldPop = true; if (mounted) { - Navigator.of(context) - .popUntil(ModalRoute.withName(ReceiveView.routeName)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(ReceiveView.routeName)); setState(() { _addressMap[_walletAddressTypes[_currentIndex]] = @@ -173,10 +176,9 @@ class _ReceiveViewState extends ConsumerState { return WillPopScope( onWillPop: () async => shouldPop, child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -203,6 +205,31 @@ class _ReceiveViewState extends ConsumerState { } } + Future

_generateNewMwebAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId) as MwebInterface; + + final address = await wallet.generateNextMwebAddress(); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address); + }); + + return address; + } + + Future generateNewMwebAddress() async { + final address = await showLoading
( + whileFuture: _generateNewMwebAddress(), + context: context, + message: "Generating address", + ); + + if (mounted && address != null) { + setState(() { + _addressMap[AddressType.mweb] = address.value; + }); + } + } + @override void initState() { walletId = widget.walletId; @@ -210,11 +237,17 @@ class _ReceiveViewState extends ConsumerState { clipboard = widget.clipboard; final wallet = ref.read(pWallets).getWallet(walletId); _supportsSpark = wallet is SparkInterface; + supportsMweb = + wallet is MwebInterface && + !wallet.info.isViewOnly && + wallet.info.isMwebEnabled; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { _showMultiType = false; } else { - _showMultiType = _supportsSpark || + _showMultiType = + _supportsSpark || + supportsMweb || (wallet is! BCashInterface && wallet is Bip39HDWallet && wallet.supportedAddressTypes.length > 1); @@ -227,10 +260,14 @@ class _ReceiveViewState extends ConsumerState { _walletAddressTypes.insert(0, AddressType.spark); } else { _walletAddressTypes.addAll( - (wallet as Bip39HDWallet) - .supportedAddressTypes - .where((e) => e != wallet.info.mainAddressType), + (wallet as Bip39HDWallet).supportedAddressTypes.where( + (e) => e != wallet.info.mainAddressType, + ), ); + + if (supportsMweb) { + _walletAddressTypes.insert(0, AddressType.mweb); + } } } @@ -238,8 +275,9 @@ class _ReceiveViewState extends ConsumerState { _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); } - _addressMap[_walletAddressTypes[_currentIndex]] = - ref.read(pWalletReceivingAddress(walletId)); + _addressMap[_walletAddressTypes[_currentIndex]] = ref.read( + pWalletReceivingAddress(walletId), + ); if (_showMultiType) { for (final type in _walletAddressTypes) { @@ -251,19 +289,22 @@ class _ReceiveViewState extends ConsumerState { .walletIdEqualTo(walletId) .filter() .typeEqualTo(type) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) .sortByDerivationIndexDesc() .findFirst() .asStream() .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _addressMap[type] = - event?.value ?? _addressMap[type] ?? "[No address yet]"; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[type] = + event?.value ?? _addressMap[type] ?? "[No address yet]"; + }); + } }); - } - }); - }); + }); } } @@ -284,6 +325,57 @@ class _ReceiveViewState extends ConsumerState { final ticker = widget.tokenContract?.symbol ?? coin.ticker; + ref.listen(pWalletInfo(walletId), (prev, next) { + if (prev?.isMwebEnabled != next.isMwebEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + supportsMweb = next.isMwebEnabled; + + if (supportsMweb && + !_walletAddressTypes.contains(AddressType.mweb)) { + _walletAddressTypes.insert(0, AddressType.mweb); + _addressSubMap[AddressType.mweb] = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[AddressType.mweb] = + event?.value ?? + _addressMap[AddressType.mweb] ?? + "[No address yet]"; + }); + } + }); + }); + } else { + _walletAddressTypes.remove(AddressType.mweb); + _addressSubMap[AddressType.mweb]?.cancel(); + _addressSubMap.remove(AddressType.mweb); + } + + if (_currentIndex >= _walletAddressTypes.length) { + _currentIndex = _walletAddressTypes.length - 1; + } + }); + } + }); + } + }); + final String address; if (_showMultiType) { address = _addressMap[_walletAddressTypes[_currentIndex]]!; @@ -291,8 +383,9 @@ class _ReceiveViewState extends ConsumerState { address = ref.watch(pWalletReceivingAddress(walletId)); } - final wallet = - ref.watch(pWallets.select((value) => value.getWallet(walletId))); + final wallet = ref.watch( + pWallets.select((value) => value.getWallet(walletId)), + ); final bool canGen; if (wallet is ViewOnlyOptionInterface && @@ -300,7 +393,8 @@ class _ReceiveViewState extends ConsumerState { wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { canGen = false; } else { - canGen = (wallet is MultiAddressInterface || _supportsSpark); + canGen = + (wallet is MultiAddressInterface || _supportsSpark || supportsMweb); } return Background( @@ -318,11 +412,7 @@ class _ReceiveViewState extends ConsumerState { ), actions: [ Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), + padding: const EdgeInsets.only(top: 10, bottom: 10, right: 10), child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( @@ -334,9 +424,10 @@ class _ReceiveViewState extends ConsumerState { color: Theme.of(context).extension()!.background, icon: SvgPicture.asset( Assets.svg.verticalEllipsis, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -353,9 +444,10 @@ class _ReceiveViewState extends ConsumerState { right: 10, child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, + color: + Theme.of( + context, + ).extension()!.popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -407,114 +499,176 @@ class _ReceiveViewState extends ConsumerState { ), ], ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ConditionalParent( - condition: _showMultiType, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Address type", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .infoItemLabel, - ), - ), - const SizedBox( - height: 10, - ), - DropdownButtonHideUnderline( - child: DropdownButton2( - value: _currentIndex, - items: [ - for (int i = 0; - i < _walletAddressTypes.length; - i++) - DropdownMenuItem( - value: i, - child: Text( - _supportsSpark && - _walletAddressTypes[i] == - AddressType.p2pkh - ? "Transparent address" - : "${_walletAddressTypes[i].readableName} address", - style: STextStyles.w500_14(context), - ), - ), - ], - onChanged: (value) { - if (value != null && value != _currentIndex) { - setState(() { - _currentIndex = value; - }); - } - }, - isExpanded: true, - iconStyleData: IconStyleData( - icon: Padding( - padding: const EdgeInsets.only(right: 10), - child: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ConditionalParent( + condition: _showMultiType, + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Address type", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, ), ), - ), - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 10), + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _currentIndex, + items: [ + for ( + int i = 0; + i < _walletAddressTypes.length; + i++ + ) + DropdownMenuItem( + value: i, + child: Text( + _supportsSpark && + _walletAddressTypes[i] == + AddressType.p2pkh + ? "Transparent address" + : "${_walletAddressTypes[i].readableName} address", + style: STextStyles.w500_14(context), + ), + ), + ], + onChanged: (value) { + if (value != null && + value != _currentIndex) { + setState(() { + _currentIndex = value; + }); + } + }, + isExpanded: true, + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + ), + ), + 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, + decoration: BoxDecoration( + color: + Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), ), ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, -10), - elevation: 0, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + const SizedBox(height: 12), + child, + ], + ), + child: GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + clipboard.setData(ClipboardData(text: address)); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + child: RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Text( + "Your $ticker address", + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 10, + height: 10, + color: + Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ], ), - ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: Text( + address, + style: STextStyles.itemSubtitle12( + context, + ), + ), + ), + ], ), - ), + ], ), ), - const SizedBox( - height: 12, - ), - child, - ], + ), ), - child: GestureDetector( - onTap: () { + const SizedBox(height: 12), + PrimaryButton( + label: "Copy address", + onPressed: () { HapticFeedback.lightImpact(); - clipboard.setData( - ClipboardData( - text: address, - ), - ); + clipboard.setData(ClipboardData(text: address)); showFloatingFlushBar( type: FlushBarType.info, message: "Copied to clipboard", @@ -522,134 +676,66 @@ class _ReceiveViewState extends ConsumerState { context: context, ); }, - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your $ticker address", - style: STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 10, - height: 10, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 4, - ), - Row( - children: [ - Expanded( - child: Text( - address, - style: STextStyles.itemSubtitle12(context), - ), - ), - ], - ), - ], - ), - ), ), - ), - const SizedBox( - height: 12, - ), - PrimaryButton( - label: "Copy address", - onPressed: () { - HapticFeedback.lightImpact(); - clipboard.setData( - ClipboardData( - text: address, - ), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - ), - if (canGen) - const SizedBox( - height: 12, - ), - if (canGen) - SecondaryButton( - label: "Generate new address", - onPressed: _supportsSpark && - _walletAddressTypes[_currentIndex] == - AddressType.spark - ? generateNewSparkAddress - : generateNewAddress, - ), - const SizedBox( - height: 30, - ), - RoundedWhiteContainer( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: Column( - children: [ - QR( - data: AddressUtils.buildUriString( - coin.uriScheme, - address, - {}, + if (canGen) const SizedBox(height: 12), + if (canGen) + SecondaryButton( + label: "Generate new address", + onPressed: + supportsMweb && + _walletAddressTypes[_currentIndex] == + AddressType.mweb + ? generateNewMwebAddress + : _supportsSpark && + _walletAddressTypes[_currentIndex] == + AddressType.spark + ? generateNewSparkAddress + : generateNewAddress, + ), + const SizedBox(height: 30), + RoundedWhiteContainer( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Column( + children: [ + QR( + data: AddressUtils.buildUriString( + coin.uriScheme, + address, + {}, + ), + size: MediaQuery.of(context).size.width / 2, ), - size: MediaQuery.of(context).size.width / 2, - ), - const SizedBox( - height: 20, - ), - CustomTextButton( - text: "Advanced options", - onTap: () async { - unawaited( - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: address, - ), - settings: const RouteSettings( - name: GenerateUriQrCodeView.routeName, + const SizedBox(height: 20), + CustomTextButton( + text: "Advanced options", + onTap: () async { + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: address, + ), + settings: const RouteSettings( + name: GenerateUriQrCodeView.routeName, + ), ), ), - ), - ); - }, - ), - ], + ); + }, + ), + ], + ), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 5090645e3..220d0e04f 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -21,7 +21,6 @@ import '../../models/isar/models/transaction_note.dart'; import '../../notifications/show_flush_bar.dart'; import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; -import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; import '../../providers/wallet/public_private_balance_state_provider.dart'; import '../../route_generator.dart'; @@ -30,9 +29,11 @@ import '../../themes/theme_providers.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/constants.dart'; +import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/coins/epiccash.dart'; +import '../../wallets/crypto_currency/coins/ethereum.dart'; import '../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; @@ -118,11 +119,7 @@ class _ConfirmTransactionViewState ), ); - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); final List txids = []; Future txDataFuture; @@ -131,30 +128,29 @@ class _ConfirmTransactionViewState try { if (widget.isTokenTx) { - txDataFuture = - ref.read(pCurrentTokenWallet)!.confirmSend(txData: widget.txData); + txDataFuture = ref + .read(pCurrentTokenWallet)! + .confirmSend(txData: widget.txData); } else if (widget.isPaynymNotificationTransaction) { - txDataFuture = (wallet as PaynymInterface) - .broadcastNotificationTx(txData: widget.txData); + txDataFuture = (wallet as PaynymInterface).broadcastNotificationTx( + txData: widget.txData, + ); } else if (widget.isPaynymTransaction) { txDataFuture = wallet.confirmSend(txData: widget.txData); } else { if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (widget.txData.sparkMints == null) { txDataFuture = wallet.confirmSend(txData: widget.txData); } else { - txDataFuture = - wallet.confirmSparkMintTransactions(txData: widget.txData); + txDataFuture = wallet.confirmSparkMintTransactions( + txData: widget.txData, + ); } break; - case FiroType.lelantus: - txDataFuture = wallet.confirmSendLelantus(txData: widget.txData); - break; - - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.confirmSendSpark(txData: widget.txData); break; } @@ -171,10 +167,7 @@ class _ConfirmTransactionViewState } } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); sendProgressController.triggerSuccess?.call(); await Future.delayed(const Duration(seconds: 5)); @@ -185,16 +178,16 @@ class _ConfirmTransactionViewState } else { txids.add((results.first as TxData).txid!); } - ref.refresh(desktopUseUTXOs); + if (coin is! Ethereum) { + ref.refresh(desktopUseUTXOs); + } // save note for (final txid in txids) { - await ref.read(mainDBProvider).putTransactionNote( - TransactionNote( - walletId: walletId, - txid: txid, - value: note, - ), + await ref + .read(mainDBProvider) + .putTransactionNote( + TransactionNote(walletId: walletId, txid: txid, value: note), ); } @@ -207,16 +200,17 @@ class _ConfirmTransactionViewState widget.onSuccess.call(); // pop back to wallet - if (mounted) { + if (context.mounted) { if (widget.onSuccessInsteadOfRouteOnSuccess == null) { - Navigator.of(context) - .popUntil(ModalRoute.withName(routeOnSuccessName)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(routeOnSuccessName)); } else { widget.onSuccessInsteadOfRouteOnSuccess!.call(); } } } on BadEpicHttpAddressException catch (_) { - if (mounted) { + if (context.mounted) { // pop building dialog Navigator.of(context).pop(); unawaited( @@ -230,83 +224,79 @@ class _ConfirmTransactionViewState return; } } catch (e, s) { - //todo: comeback to this - debugPrint("$e\n$s"); + const message = "Broadcast transaction failed"; + Logging.instance.e(message, error: e, stackTrace: s); // pop sending dialog - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - if (isDesktop) { - return DesktopDialog( - maxWidth: 450, - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Broadcast transaction failed", - style: STextStyles.desktopH3(context), - ), - const SizedBox( - height: 24, - ), - Flexible( - child: SingleChildScrollView( - child: SelectableText( - e.toString(), - style: STextStyles.smallMed14(context), + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message, style: STextStyles.desktopH3(context)), + const SizedBox(height: 24), + Flexible( + child: SingleChildScrollView( + child: SelectableText( + e.toString(), + style: STextStyles.smallMed14(context), + ), ), ), - ), - const SizedBox( - height: 56, - ), - Row( - children: [ - const Spacer(), - Expanded( - child: PrimaryButton( - buttonHeight: ButtonHeight.l, - label: "Ok", - onPressed: Navigator.of(context).pop, + const SizedBox(height: 56), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: Navigator.of(context).pop, + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), - ), - ); - } else { - return StackDialog( - title: "Broadcast 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()! - .accentColorDark, + ); + } else { + return StackDialog( + title: message, + 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(); + }, ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - } - }, - ); + ); + } + }, + ); + } } } @@ -356,7 +346,7 @@ class _ConfirmTransactionViewState if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (widget.txData.sparkMints != null) { fee = widget.txData.sparkMints! .map((e) => e.fee!) @@ -370,14 +360,10 @@ class _ConfirmTransactionViewState } break; - case FiroType.lelantus: - fee = widget.txData.fee; - amountWithoutChange = widget.txData.amountWithoutChange!; - break; - - case FiroType.spark: + case BalanceType.private: fee = widget.txData.fee; - amountWithoutChange = (widget.txData.amountWithoutChange ?? + amountWithoutChange = + (widget.txData.amountWithoutChange ?? Amount.zeroWith( fractionDigits: wallet.cryptoCurrency.fractionDigits, )) + @@ -394,82 +380,81 @@ class _ConfirmTransactionViewState return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Confirm transaction", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - ), - ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, ), - ); - }, + ), + ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Row( + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, children: [ - AppBarBackButton( - size: 40, - iconSize: 24, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(), - ), - Text( - "Confirm $unit transaction", - style: STextStyles.desktopH3(context), + Row( + children: [ + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: + () => + Navigator.of(context, rootNavigator: true).pop(), + ), + Text( + "Confirm $unit transaction", + style: STextStyles.desktopH3(context), + ), + ], ), + Flexible(child: SingleChildScrollView(child: child)), ], ), - Flexible( - child: SingleChildScrollView( - child: child, - ), - ), - ], - ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, @@ -478,13 +463,8 @@ class _ConfirmTransactionViewState Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Send $unit", - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 12, - ), + Text("Send $unit", style: STextStyles.pageTitleH1(context)), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -495,9 +475,7 @@ class _ConfirmTransactionViewState : "Recipient", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName @@ -508,25 +486,23 @@ class _ConfirmTransactionViewState ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), + Text("Amount", style: STextStyles.smallMed12(context)), SelectableText( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( amountWithoutChange, - ethContract: widget.isTokenTx - ? ref - .watch(pCurrentTokenWallet)! - .tokenContract - : null, + ethContract: + widget.isTokenTx + ? ref + .watch(pCurrentTokenWallet)! + .tokenContract + : null, ), style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, @@ -534,10 +510,7 @@ class _ConfirmTransactionViewState ], ), ), - if (coin is! NanoCurrency) - const SizedBox( - height: 12, - ), + if (coin is! NanoCurrency) const SizedBox(height: 12), if (coin is! NanoCurrency) RoundedWhiteContainer( child: Row( @@ -555,10 +528,23 @@ class _ConfirmTransactionViewState ], ), ), - if (widget.txData.fee != null && widget.txData.vSize != null) - const SizedBox( - height: 12, + if (coin is Ethereum) const SizedBox(height: 12), + if (coin is Ethereum) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Nonce", style: STextStyles.smallMed12(context)), + SelectableText( + widget.txData.nonce.toString(), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), ), + if (widget.txData.fee != null && widget.txData.vSize != null) + const SizedBox(height: 12), if (widget.txData.fee != null && widget.txData.vSize != null) RoundedWhiteContainer( child: Row( @@ -568,9 +554,7 @@ class _ConfirmTransactionViewState "sats/vByte", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle12(context), @@ -579,9 +563,7 @@ class _ConfirmTransactionViewState ), ), if (coin is Epiccash && widget.txData.noteOnChain!.isNotEmpty) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (coin is Epiccash && widget.txData.noteOnChain!.isNotEmpty) RoundedWhiteContainer( child: Column( @@ -591,9 +573,7 @@ class _ConfirmTransactionViewState "On chain note", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( widget.txData.noteOnChain!, style: STextStyles.itemSubtitle12(context), @@ -602,9 +582,7 @@ class _ConfirmTransactionViewState ), ), if (widget.txData.note!.isNotEmpty) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (widget.txData.note!.isNotEmpty) RoundedWhiteContainer( child: Column( @@ -614,9 +592,7 @@ class _ConfirmTransactionViewState (coin is Epiccash) ? "Local Note" : "Note", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( widget.txData.note!, style: STextStyles.itemSubtitle12(context), @@ -644,9 +620,10 @@ class _ConfirmTransactionViewState children: [ Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, borderRadius: BorderRadius.only( topLeft: Radius.circular( Constants.size.circularBorderRadius, @@ -674,9 +651,7 @@ class _ConfirmTransactionViewState width: 32, height: 32, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Text( "Send $unit", style: STextStyles.desktopTextMedium(context), @@ -697,9 +672,7 @@ class _ConfirmTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Builder( builder: (context) { final externalCalls = ref.watch( @@ -710,77 +683,81 @@ class _ConfirmTransactionViewState String fiatAmount = "N/A"; if (externalCalls) { - final price = widget.isTokenTx - ? ref - .read( - priceAnd24hChangeNotifierProvider, - ) - .getTokenPrice( - ref - .read(pCurrentTokenWallet)! - .tokenContract - .address, - ) - .item1 - : ref - .read( - priceAnd24hChangeNotifierProvider, - ) - .getPrice(coin) - .item1; - if (price > Decimal.zero) { - fiatAmount = - (amountWithoutChange.decimal * price) - .toAmount(fractionDigits: 2) - .fiatString( - locale: ref + final price = + widget.isTokenTx + ? ref + .read( + priceAnd24hChangeNotifierProvider, + ) + .getTokenPrice( + ref + .read(pCurrentTokenWallet)! + .tokenContract + .address, + ) + ?.value + : ref + .read( + priceAnd24hChangeNotifierProvider, + ) + .getPrice(coin) + ?.value; + if (price != null && price > Decimal.zero) { + fiatAmount = (amountWithoutChange.decimal * + price) + .toAmount(fractionDigits: 2) + .fiatString( + locale: + ref .read( localeServiceChangeNotifierProvider, ) .locale, - ); + ); } } return Row( children: [ SelectableText( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( amountWithoutChange, - ethContract: widget.isTokenTx - ? ref - .watch(pCurrentTokenWallet)! - .tokenContract - : null, + ethContract: + widget.isTokenTx + ? ref + .watch( + pCurrentTokenWallet, + )! + .tokenContract + : null, + ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, ), - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), ), if (externalCalls) Text( " | ", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), if (externalCalls) SelectableText( - "~$fiatAmount ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ), + "~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ], ); @@ -791,9 +768,10 @@ class _ConfirmTransactionViewState ), Container( height: 1, - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, ), Padding( padding: const EdgeInsets.all(12), @@ -809,22 +787,24 @@ class _ConfirmTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( // TODO: [prio=med] spark transaction specifics - better handling widget.isPaynymTransaction ? widget.txData.paynymAccountLite!.nymName : widget.txData.recipients?.first.address ?? - widget.txData.sparkRecipients!.first + widget + .txData + .sparkRecipients! + .first .address, style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -833,9 +813,10 @@ class _ConfirmTransactionViewState if (widget.isPaynymTransaction) Container( height: 1, - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, ), if (widget.isPaynymTransaction) Padding( @@ -850,17 +831,52 @@ class _ConfirmTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], + ), + ), + if (coin is Ethereum) + Container( + height: 1, + color: + Theme.of( + context, + ).extension()!.background, + ), + if (coin is Ethereum) + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Nonce", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + SelectableText( + widget.txData.nonce.toString(), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -905,10 +921,7 @@ class _ConfirmTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -919,10 +932,7 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin is Epiccash) - const SizedBox( - height: 8, - ), + if (coin is Epiccash) const SizedBox(height: 8), if (coin is Epiccash) ClipRRect( borderRadius: BorderRadius.circular( @@ -941,47 +951,46 @@ class _ConfirmTransactionViewState _onChainNoteFocusNode, context, ).copyWith( - suffixIcon: onChainNoteController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - onChainNoteController.text = ""; - }); - }, - ), - ], + suffixIcon: + onChainNoteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + onChainNoteController.text = + ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), - if (coin is Epiccash) - const SizedBox( - height: 12, - ), + if (coin is Epiccash) const SizedBox(height: 12), SelectableText( (coin is Epiccash) ? "Local Note (optional)" : "Note (optional)", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -993,11 +1002,13 @@ class _ConfirmTransactionViewState enableSuggestions: isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), onChanged: (_) => setState(() {}), @@ -1013,39 +1024,36 @@ class _ConfirmTransactionViewState bottom: 12, right: 5, ), - 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 = "", - ); - }, - ), - ], + 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, + ) + : null, ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), ], ), ), if (isDesktop && !widget.isPaynymTransaction) Padding( - padding: const EdgeInsets.only( - left: 32, - ), + padding: const EdgeInsets.only(left: 32), child: Text( "Transaction fee", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -1053,19 +1061,16 @@ class _ConfirmTransactionViewState ), if (isDesktop && !widget.isPaynymTransaction) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), child: RoundedContainer( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 18, ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.itemSubtitle(context), @@ -1077,9 +1082,7 @@ class _ConfirmTransactionViewState widget.txData.fee != null && widget.txData.vSize != null) Padding( - padding: const EdgeInsets.only( - left: 32, - ), + padding: const EdgeInsets.only(left: 32), child: Text( "sats/vByte", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -1090,19 +1093,16 @@ class _ConfirmTransactionViewState widget.txData.fee != null && widget.txData.vSize != null) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), child: RoundedContainer( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 18, ), - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), @@ -1110,154 +1110,167 @@ class _ConfirmTransactionViewState ), ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 23 : 12, - ), + SizedBox(height: isDesktop ? 23 : 12), if (!widget.isTokenTx) Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 16, - vertical: 18, - ) - : const EdgeInsets.all(12), - color: Theme.of(context) - .extension()! - .snackBarBackSuccess, + padding: + isDesktop + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ) + : const EdgeInsets.all(12), + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( isDesktop ? "Total amount to send" : "Total amount", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), ), SelectableText( ref .watch(pAmountFormatter(coin)) .format(amountWithoutChange + fee!), - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), textAlign: TextAlign.right, ), ], ), ), ), - SizedBox( - height: isDesktop ? 28 : 16, - ), + SizedBox(height: isDesktop ? 28 : 16), Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: () async { - final dynamic unlocked; - if (isDesktop) { - unlocked = await showDialog( + final unlocked = await showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.end, + builder: + (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - DesktopDialogCloseButton(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend( - coin: coin, - ), - ), - ], - ), - ), + ), ); + if (context.mounted && unlocked is bool) { + if (unlocked) { + unawaited(_attemptSend(context)); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid passphrase", + context: context, + ), + ); + } + } } else { - unlocked = await Navigator.push( + final unlocked = await Navigator.push( context, RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - popOnSuccess: true, - routeOnSuccessArguments: true, - routeOnSuccess: "", - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to send transaction", - biometricsAuthenticationTitle: "Confirm Transaction", + builder: + (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: + "Confirm Transaction", + ), + settings: const RouteSettings( + name: "/confirmsendlockscreen", ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), ), ); - } - if (mounted) { - if (unlocked == true) { - unawaited(_attemptSend(context)); - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: Util.isDesktop - ? "Invalid passphrase" - : "Invalid PIN", - context: context, - ), - ); + if (context.mounted) { + if (unlocked == true) { + unawaited(_attemptSend(context)); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: + Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", + context: context, + ), + ); + } } } }, ), ), - if (isDesktop) - const SizedBox( - height: 32, - ), + if (isDesktop) const SizedBox(height: 32), ], ), ), 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 3d90a896f..4b1014158 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -51,11 +51,7 @@ import '../../coin_control/coin_control_view.dart'; import 'recipient.dart'; class FrostSendView extends ConsumerStatefulWidget { - const FrostSendView({ - super.key, - required this.walletId, - required this.coin, - }); + const FrostSendView({super.key, required this.walletId, required this.coin}); static const String routeName = "/frostSendView"; @@ -87,7 +83,14 @@ 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) => TxRecipient( + address: e!.address, + amount: e.amount!, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(e.address)!, + ), + ) .toList(growable: false); final txData = await wallet.frostCreateSignConfig( @@ -107,9 +110,7 @@ class _FrostSendViewState extends ConsumerState { try { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); TxData? txData; if (mounted) { @@ -143,9 +144,7 @@ class _FrostSendViewState extends ConsumerState { callerRouteName: FrostSendView.routeName, ); - await Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - ); + await Navigator.of(context).pushNamed(FrostStepScaffold.routeName); } } catch (e) { if (mounted) { @@ -165,9 +164,10 @@ class _FrostSendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -229,68 +229,72 @@ class _FrostSendViewState extends ConsumerState { debugPrint("BUILD: $runtimeType"); final wallet = ref.watch(pWallets).getWallet(walletId); - final showCoinControl = wallet is CoinControlInterface && + final showCoinControl = + wallet is CoinControlInterface && ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, ), ) && (coin is Firo - ? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public + ? ref.watch(publicPrivateBalanceStateProvider) == BalanceType.public : true); return ConditionalParent( condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 50)); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Send ${coin.ticker}", - style: STextStyles.navBarTitle(context), - ), - ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - // subtract top and bottom padding set in parent - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: child, - ), - ), + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 50), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, ), - ); - }, + title: Text( + "Send ${coin.ticker}", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), ), - ), - ), child: ConditionalParent( condition: Util.isDesktop, - builder: (child) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 14, - ), - child: child, - ), + builder: + (child) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: child, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -307,24 +311,19 @@ class _FrostSendViewState extends ConsumerState { child: Row( children: [ SvgPicture.file( - File( - ref.watch( - coinIconProvider(coin), - ), - ), + File(ref.watch(coinIconProvider(coin))), width: 22, height: 22, ), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( ref.watch(pWalletName(walletId)), - style: STextStyles.titleBold12(context) - .copyWith(fontSize: 14), + style: STextStyles.titleBold12( + context, + ).copyWith(fontSize: 14), overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -333,15 +332,14 @@ class _FrostSendViewState extends ConsumerState { // ), Text( "Available balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), + style: STextStyles.label( + context, + ).copyWith(fontSize: 10), ), ], ), Util.isDesktop - ? const SizedBox( - height: 24, - ) + ? const SizedBox(height: 24) : const Spacer(), GestureDetector( onTap: () {}, @@ -351,15 +349,16 @@ class _FrostSendViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref .watch(pWalletBalance(walletId)) .spendable, ), - style: - STextStyles.titleBold12(context).copyWith( - fontSize: 10, - ), + style: STextStyles.titleBold12( + context, + ).copyWith(fontSize: 10), textAlign: TextAlign.right, ), ], @@ -370,41 +369,40 @@ class _FrostSendViewState extends ConsumerState { ), ), ), - SizedBox( - height: recipientWidgetIndexes.length > 1 ? 8 : 16, - ), + SizedBox(height: recipientWidgetIndexes.length > 1 ? 8 : 16), Column( children: [ for (int i = 0; i < recipientWidgetIndexes.length; i++) ConditionalParent( condition: recipientWidgetIndexes.length > 1, - builder: (child) => Padding( - padding: const EdgeInsets.only(top: 8), - child: child, - ), + builder: + (child) => Padding( + padding: const EdgeInsets.only(top: 8), + child: child, + ), child: Recipient( - key: Key( - "recipientKey_${recipientWidgetIndexes[i]}", - ), + key: Key("recipientKey_${recipientWidgetIndexes[i]}"), index: recipientWidgetIndexes[i], displayNumber: i + 1, coin: coin, onChanged: () { _validateRecipientFormStates(); }, - remove: i == 0 && recipientWidgetIndexes.length == 1 - ? null - : () { - ref - .read( - pRecipient(recipientWidgetIndexes[i]) - .notifier, - ) - .state = null; - recipientWidgetIndexes.removeAt(i); - setState(() {}); - _validateRecipientFormStates(); - }, + remove: + i == 0 && recipientWidgetIndexes.length == 1 + ? null + : () { + ref + .read( + pRecipient( + recipientWidgetIndexes[i], + ).notifier, + ) + .state = null; + recipientWidgetIndexes.removeAt(i); + setState(() {}); + _validateRecipientFormStates(); + }, addAnotherRecipientTapped: () { // used for tracking recipient forms _greatestWidgetIndex++; @@ -413,7 +411,9 @@ class _FrostSendViewState extends ConsumerState { _validateRecipientFormStates(); }, sendAllTapped: () { - return ref.read(pAmountFormatter(coin)).format( + return ref + .read(pAmountFormatter(coin)) + .format( ref.read(pWalletBalance(walletId)).spendable, withUnitName: false, ); @@ -422,10 +422,7 @@ class _FrostSendViewState extends ConsumerState { ), ], ), - if (recipientWidgetIndexes.length > 1) - const SizedBox( - height: 12, - ), + if (recipientWidgetIndexes.length > 1) const SizedBox(height: 12), if (recipientWidgetIndexes.length > 1) SecondaryButton( width: double.infinity, @@ -437,10 +434,7 @@ class _FrostSendViewState extends ConsumerState { setState(() {}); }, ), - if (showCoinControl) - const SizedBox( - height: 8, - ), + if (showCoinControl) const SizedBox(height: 8), if (showCoinControl) RoundedWhiteContainer( child: Row( @@ -449,15 +443,17 @@ class _FrostSendViewState extends ConsumerState { Text( "Coin control", style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), CustomTextButton( - text: selectedUTXOs.isEmpty - ? "Select coins" - : "Selected coins (${selectedUTXOs.length})", + text: + selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", onTap: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -492,17 +488,13 @@ class _FrostSendViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Text( "Note (optional)", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -519,36 +511,32 @@ class _FrostSendViewState extends ConsumerState { _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 = ""; - }); - }, - ), - ], + 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, + ) + : null, ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.only( - bottom: 12, - top: 16, - ), + padding: const EdgeInsets.only(bottom: 12, top: 16), child: FeeSlider( coin: coin, showWU: true, @@ -557,22 +545,14 @@ class _FrostSendViewState extends ConsumerState { }, ), ), - Util.isDesktop - ? const SizedBox( - height: 12, - ) - : const Spacer(), - const SizedBox( - height: 12, - ), + Util.isDesktop ? const SizedBox(height: 12) : const Spacer(), + const SizedBox(height: 12), PrimaryButton( label: "Create multisig transaction", enabled: _buttonEnabled, onPressed: _createSignConfig, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ), diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index ba8b9dbd7..150eecb0b 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../providers/global/locale_provider.dart'; +import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/address_utils.dart'; import '../../../utilities/amount/amount.dart'; @@ -11,7 +11,6 @@ import '../../../utilities/amount/amount_formatter.dart'; import '../../../utilities/amount/amount_input_formatter.dart'; import '../../../utilities/amount/amount_unit.dart'; import '../../../utilities/barcode_scanner_interface.dart'; -import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/text_styles.dart'; @@ -25,12 +24,6 @@ import '../../../widgets/rounded_container.dart'; import '../../../widgets/stack_text_field.dart'; import '../../../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 @@ -40,8 +33,8 @@ final pBarcodeScanner = final pRecipient = StateProvider.family<({String address, Amount? amount})?, int>( - (ref, index) => null, -); + (ref, index) => null, + ); class Recipient extends ConsumerStatefulWidget { const Recipient({ @@ -79,8 +72,9 @@ class _RecipientState extends ConsumerState { void _updateRecipientData() { final address = addressController.text; - final amount = - ref.read(pAmountFormatter(widget.coin)).tryParse(amountController.text); + final amount = ref + .read(pAmountFormatter(widget.coin)) + .tryParse(amountController.text); ref.read(pRecipient(widget.index).notifier).state = ( address: address, @@ -91,9 +85,9 @@ class _RecipientState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - Amount? cryptoAmount = ref.read(pAmountFormatter(widget.coin)).tryParse( - amountController.text, - ); + 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) { @@ -124,14 +118,10 @@ class _RecipientState extends ConsumerState { try { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration( - milliseconds: 75, - ), - ); + await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); @@ -150,14 +140,12 @@ class _RecipientState extends ConsumerState { // autofill amount field if (paymentData.amount != null) { - final Amount amount = Decimal.parse(paymentData.amount!).toAmount( - fractionDigits: widget.coin.fractionDigits, - ); - amountController.text = - ref.read(pAmountFormatter(widget.coin)).format( - amount, - withUnitName: false, - ); + final Amount amount = Decimal.parse( + paymentData.amount!, + ).toAmount(fractionDigits: widget.coin.fractionDigits); + amountController.text = ref + .read(pAmountFormatter(widget.coin)) + .format(amount, withUnitName: false); } } else { addressController.text = qrResult.rawContent.trim(); @@ -169,12 +157,27 @@ class _RecipientState extends ConsumerState { _updateRecipientData(); } on PlatformException catch (e, s) { - Logging.instance.e( - "Failed to get camera permissions while " - "trying to scan qr code in SendView: $e\n$s", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while " + "trying to scan qr code in SendView: $e\n$s", + error: e, + stackTrace: s, + ); + } } } @@ -221,9 +224,7 @@ class _RecipientState extends ConsumerState { @override Widget build(BuildContext context) { final String locale = ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), + localeServiceChangeNotifierProvider.select((value) => value.locale), ); return RoundedContainer( @@ -248,9 +249,7 @@ class _RecipientState extends ConsumerState { ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -281,72 +280,73 @@ class _RecipientState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _addressIsEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + 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 = ""; + 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 = true; + _addressIsEmpty = + addressController.text.isEmpty; }); _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(), - ), + } + }, + child: + _addressIsEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_addressIsEmpty) TextFieldIconButton( - semanticsLabel: "Scan QR Button. " + semanticsLabel: + "Scan QR Button. " "Opens Camera For Scanning QR Code.", - key: const Key( - "sendViewScanQrButtonKey", - ), + key: const Key("sendViewScanQrButtonKey"), onTap: _onQrTapped, child: const QrCodeIcon(), ), @@ -357,9 +357,7 @@ class _RecipientState extends ConsumerState { ), ), ), - SizedBox( - height: isSingle ? 12 : 8, - ), + SizedBox(height: isSingle ? 12 : 8), if (isSingle) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -380,10 +378,7 @@ class _RecipientState extends ConsumerState { // ), ], ), - if (isSingle) - const SizedBox( - height: 8, - ), + if (isSingle) const SizedBox(height: 8), TextField( autocorrect: false, enableSuggestions: false, @@ -396,12 +391,13 @@ class _RecipientState extends ConsumerState { onChanged: (_) { _updateRecipientData(); }, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -411,14 +407,9 @@ class _RecipientState extends ConsumerState { ), ], decoration: InputDecoration( - contentPadding: const EdgeInsets.only( - top: 12, - right: 12, - ), + contentPadding: const EdgeInsets.only(top: 12, right: 12), hintText: "0", - hintStyle: STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), + hintStyle: STextStyles.fieldLabel(context).copyWith(fontSize: 14), prefixIcon: FittedBox( fit: BoxFit.scaleDown, child: Padding( @@ -428,9 +419,10 @@ class _RecipientState extends ConsumerState { .watch(pAmountUnit(widget.coin)) .unitForCoin(widget.coin), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), 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 ade993b22..e5f6581a1 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 @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import '../../../../frost_route_generator.dart'; +import '../../../../models/input.dart'; import '../../../../models/isar/models/isar_models.dart'; import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/frost_wallet/frost_wallet_providers.dart'; @@ -60,9 +61,9 @@ class _FrostSendStep1bState extends ConsumerState { } final config = configFieldController.text; - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; final data = Frost.extractDataFromSignConfig( signConfig: config, @@ -70,28 +71,38 @@ class _FrostSendStep1bState extends ConsumerState { serializedKeys: (await wallet.getSerializedKeys())!, ); - 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(); + 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(), + recipients: + data.recipients + .map( + (e) => TxRecipient( + address: e.address, + amount: e.amount, + isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(e.address)!, + ), + ) + .toList(), + utxos: utxos.map((e) => StandardInput(e)).toSet(), ); final attemptSignRes = await wallet.frostAttemptSignConfig( @@ -112,11 +123,12 @@ class _FrostSendStep1bState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Import and attempt sign config failed", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), + builder: + (_) => StackOkDialog( + title: "Import and attempt sign config failed", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), ); } } finally { @@ -128,9 +140,9 @@ class _FrostSendStep1bState extends ConsumerState { void initState() { configFieldController = TextEditingController(); configFocusNode = FocusNode(); - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(pFrostMyName.state).state = wallet.frostInfo.myName; }); @@ -152,9 +164,7 @@ class _FrostSendStep1bState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const FrostStepUserSteps( - userSteps: info, - ), + const FrostStepUserSteps(userSteps: info), const SizedBox(height: 20), FrostStepField( controller: configFieldController, @@ -169,11 +179,10 @@ class _FrostSendStep1bState extends ConsumerState { }, ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), CheckboxTextButton( - label: "I have verified that everyone has imported he config and" + label: + "I have verified that everyone has imported he config and" " is ready to sign", onChanged: (value) { setState(() { @@ -181,9 +190,7 @@ class _FrostSendStep1bState extends ConsumerState { }); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), PrimaryButton( label: "Start signing", enabled: !_configEmpty && _userVerifyContinue, 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 6ce475a0b..f3ddc90cc 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 @@ -51,9 +51,9 @@ class _FrostSendStep3State extends ConsumerState { @override void initState() { - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; final frostInfo = wallet.frostInfo; @@ -62,12 +62,13 @@ class _FrostSendStep3State extends ConsumerState { 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 = + frostInfo.participants + .toSet() + .intersection( + ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet(), + ) + .toList(); participantsWithoutMe.remove(myName); @@ -98,46 +99,28 @@ class _FrostSendStep3State extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const FrostStepUserSteps( - userSteps: info, - ), - const SizedBox( - height: 12, - ), + const FrostStepUserSteps(userSteps: info), + const SizedBox(height: 12), DetailItem( title: "My name", detail: myName, - button: Util.isDesktop - ? IconCopyButton( - data: myName, - ) - : SimpleCopyButton( - data: myName, - ), - ), - const SizedBox( - height: 12, + button: + Util.isDesktop + ? IconCopyButton(data: myName) + : SimpleCopyButton(data: myName), ), + const SizedBox(height: 12), DetailItem( title: "My share", detail: myShare, - button: Util.isDesktop - ? IconCopyButton( - data: myShare, - ) - : SimpleCopyButton( - data: myShare, - ), - ), - const SizedBox( - height: 12, - ), - FrostQrDialogPopupButton( - data: myShare, - ), - const SizedBox( - height: 12, + button: + Util.isDesktop + ? IconCopyButton(data: myShare) + : SimpleCopyButton(data: myShare), ), + const SizedBox(height: 12), + FrostQrDialogPopupButton(data: myShare), + const SizedBox(height: 12), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -158,9 +141,7 @@ class _FrostSendStep3State extends ConsumerState { ], ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), CheckboxTextButton( label: "I have verified that everyone has my share", onChanged: (value) { @@ -169,12 +150,11 @@ class _FrostSendStep3State extends ConsumerState { }); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), PrimaryButton( label: "Generate transaction", - enabled: _userVerifyContinue && + enabled: + _userVerifyContinue && !fieldIsEmptyFlags.fold(false, (v, e) => v |= e), onPressed: () async { // collect Share strings @@ -206,23 +186,21 @@ class _FrostSendStep3State extends ConsumerState { final inputTotal = Amount( rawValue: txData.utxos! - .map((e) => BigInt.from(e.value)) + .map((e) => e.value) .reduce((v, e) => v += e), fractionDigits: fractionDigits, ); final outputTotal = Amount( - rawValue: - tx.outputs.map((e) => e.value).reduce((v, e) => v += e), + 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, - ], + frostSigners: [myName, ...participantsWithoutMe], ); ref.read(pFrostCreateCurrentStep.state).state = 4; @@ -233,14 +211,15 @@ class _FrostSendStep3State extends ConsumerState { .routeName, ); } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); if (context.mounted) { return await showDialog( context: context, - builder: (_) => const FrostErrorDialog( - title: "Failed to complete signing process", - ), + builder: + (_) => const FrostErrorDialog( + title: "Failed to complete signing process", + ), ); } } diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 4bdcef6e2..cfaa41f64 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -19,6 +19,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tuple/tuple.dart'; +import '../../models/input.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; import '../../models/send_view_auto_fill_data.dart'; @@ -27,6 +28,7 @@ import '../../providers/ui/fee_rate_type_state_provider.dart'; import '../../providers/ui/preview_tx_button_state_provider.dart'; import '../../providers/wallet/public_private_balance_state_provider.dart'; import '../../route_generator.dart'; +import '../../services/spark_names_service.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/address_utils.dart'; @@ -39,6 +41,7 @@ import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../utilities/eth_commons.dart'; import '../../utilities/extensions/extensions.dart'; import '../../utilities/logger.dart'; import '../../utilities/prefs.dart'; @@ -50,6 +53,7 @@ import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../widgets/animated_text.dart'; @@ -57,6 +61,7 @@ import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/dialogs/firo_exchange_address_dialog.dart'; +import '../../widgets/eth_fee_form.dart'; import '../../widgets/fee_slider.dart'; import '../../widgets/icon_widgets/addressbook_icon.dart'; import '../../widgets/icon_widgets/clipboard_icon.dart'; @@ -70,7 +75,7 @@ import '../address_book_views/address_book_view.dart'; import '../coin_control/coin_control_view.dart'; import 'confirm_transaction_view.dart'; import 'sub_widgets/building_transaction_dialog.dart'; -import 'sub_widgets/firo_balance_selection_sheet.dart'; +import 'sub_widgets/dual_balance_selection_sheet.dart'; import 'sub_widgets/transaction_fee_selection_sheet.dart'; class SendView extends ConsumerStatefulWidget { @@ -80,7 +85,6 @@ class SendView extends ConsumerStatefulWidget { required this.coin, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), this.accountLite, }); @@ -90,7 +94,6 @@ class SendView extends ConsumerStatefulWidget { final CryptoCurrency coin; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; final PaynymAccountLite? accountLite; @override @@ -98,10 +101,16 @@ class SendView extends ConsumerStatefulWidget { } class _SendViewState extends ConsumerState { + static const stringsToLoopThrough = [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ]; + late final String walletId; late final CryptoCurrency coin; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late TextEditingController sendToController; late TextEditingController cryptoAmountController; @@ -122,6 +131,7 @@ class _SendViewState extends ConsumerState { late final bool isStellar; late final bool isFiro; + late final bool isEth; Amount? _cachedAmountToSend; String? _address; @@ -133,7 +143,7 @@ class _SendViewState extends ConsumerState { bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; - Set selectedUTXOs = {}; + Set selectedUTXOs = {}; void _applyUri(PaymentUriData paymentData) { try { @@ -150,13 +160,12 @@ class _SendViewState extends ConsumerState { // autofill amount field if (paymentData.amount != null) { - final Amount amount = Decimal.parse(paymentData.amount!).toAmount( - fractionDigits: coin.fractionDigits, - ); - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + final Amount amount = Decimal.parse( + paymentData.amount!, + ).toAmount(fractionDigits: coin.fractionDigits); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); ref.read(pSendAmount.notifier).state = amount; } @@ -173,6 +182,82 @@ class _SendViewState extends ConsumerState { } } + Future _checkSparkNameAndOrSetAddress( + String content, { + bool setController = true, + }) async { + void setContent() { + if (setController) { + sendToController.text = content; + } + _address = content; + } + + // check for spark name + if (coin is Firo) { + final address = await SparkNamesService.getAddressFor( + content, + network: coin.network, + ); + if (address != null) { + // found a spark name + sendToController.text = content; + _address = address; + } else { + setContent(); + } + } else { + setContent(); + } + } + + Future _pasteAddress() async { + final ClipboardData? data = await clipboard.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")).trim(); + } + + try { + final paymentData = AddressUtils.parsePaymentUri( + content, + logging: Logging.instance, + ); + + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { + _applyUri(paymentData); + } else { + if (coin is Epiccash) { + content = AddressUtils().formatEpicCashAddress(content); + } + + sendToController.text = content; + _address = content; + + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } catch (e) { + if (coin is Epiccash) { + // strip http:// and https:// if content contains @ + content = AddressUtils().formatEpicCashAddress(content); + } + + await _checkSparkNameAndOrSetAddress(content); + + // Trigger validation after pasting. + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + } + Future _scanQr() async { try { // ref @@ -185,7 +270,7 @@ class _SendViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), @@ -221,13 +306,27 @@ class _SendViewState extends ConsumerState { // shouldShowLockscreenOnResumeStateProvider // .state) // .state = true; - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.e( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in SendView: ", + error: e, + stackTrace: s, + ); + } } } @@ -238,29 +337,27 @@ class _SendViewState extends ConsumerState { ); final Amount? amount; if (baseAmount != null) { - final Decimal _price = - ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + final _price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin)?.value; - if (_price == Decimal.zero) { + if (_price == null || _price == Decimal.zero) { amount = 0.toAmountAsRaw(fractionDigits: coin.fractionDigits); } else { - amount = baseAmount <= Amount.zero - ? 0.toAmountAsRaw(fractionDigits: coin.fractionDigits) - : (baseAmount.decimal / _price) - .toDecimal( - scaleOnInfinitePrecision: coin.fractionDigits, - ) - .toAmount(fractionDigits: coin.fractionDigits); + amount = + baseAmount <= Amount.zero + ? 0.toAmountAsRaw(fractionDigits: coin.fractionDigits) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: coin.fractionDigits) + .toAmount(fractionDigits: coin.fractionDigits); } if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } _cachedAmountToSend = amount; - final amountString = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + final amountString = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; @@ -281,9 +378,9 @@ class _SendViewState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( - cryptoAmountController.text, - ); + final cryptoAmount = ref + .read(pAmountFormatter(coin)) + .tryParse(cryptoAmountController.text); final Amount? amount; if (cryptoAmount != null) { amount = cryptoAmount; @@ -293,13 +390,11 @@ class _SendViewState extends ConsumerState { _cachedAmountToSend = amount; final price = - ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin)?.value; - if (price > Decimal.zero) { + if (price != null && price > Decimal.zero) { baseAmountController.text = (amount.decimal * price) - .toAmount( - fractionDigits: 2, - ) + .toAmount(fractionDigits: 2) .fiatString( locale: ref.read(localeServiceChangeNotifierProvider).locale, ); @@ -356,10 +451,12 @@ class _SendViewState extends ConsumerState { fee = fee.split(" ").first; } - final value = fee.contains(",") - ? Decimal.parse(fee.replaceFirst(",", ".")) - .toAmount(fractionDigits: coin.fractionDigits) - : Decimal.parse(fee).toAmount(fractionDigits: coin.fractionDigits); + final value = + fee.contains(",") + ? Decimal.parse( + fee.replaceFirst(",", "."), + ).toAmount(fractionDigits: coin.fractionDigits) + : Decimal.parse(fee).toAmount(fractionDigits: coin.fractionDigits); if (shouldSetState) { setState(() => _currentFee = value); @@ -368,38 +465,24 @@ class _SendViewState extends ConsumerState { } } - String? _updateInvalidAddressText(String address) { - if (_data != null && _data.contactLabel == address) { - return null; - } - - if (address.isNotEmpty && - !ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(address)) { - return "Invalid address"; - } - return null; - } - void _setValidAddressProviders(String? address) { if (isPaynymSend) { ref.read(pValidSendToAddress.notifier).state = true; } else { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is SparkInterface) { - ref.read(pValidSparkSendToAddress.notifier).state = - SparkInterface.validateSparkAddress( + ref + .read(pValidSparkSendToAddress.notifier) + .state = SparkInterface.validateSparkAddress( address: address ?? "", isTestNet: wallet.cryptoCurrency.network.isTestNet, ); - ref.read(pIsExchangeAddress.state).state = - (coin as Firo).isExchangeAddress(address ?? ""); + ref.read(pIsExchangeAddress.state).state = (coin as Firo) + .isExchangeAddress(address ?? ""); - if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && + if (ref.read(publicPrivateBalanceStateProvider) == + BalanceType.private && ref.read(pIsExchangeAddress) && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -410,15 +493,14 @@ class _SendViewState extends ConsumerState { } } - ref.read(pValidSendToAddress.notifier).state = - wallet.cryptoCurrency.validateAddress(address ?? ""); + ref.read(pValidSendToAddress.notifier).state = wallet.cryptoCurrency + .validateAddress(address ?? ""); } } late Future _calculateFeesFuture; Map cachedFees = {}; - Map cachedFiroLelantusFees = {}; Map cachedFiroSparkFees = {}; Map cachedFiroPublicFees = {}; @@ -429,17 +511,12 @@ class _SendViewState extends ConsumerState { if (isFiro) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (cachedFiroPublicFees[amount] != null) { return cachedFiroPublicFees[amount]!; } break; - case FiroType.lelantus: - if (cachedFiroLelantusFees[amount] != null) { - return cachedFiroLelantusFees[amount]!; - } - break; - case FiroType.spark: + case BalanceType.private: if (cachedFiroSparkFees[amount] != null) { return cachedFiroSparkFees[amount]!; } @@ -452,9 +529,9 @@ class _SendViewState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(walletId); final feeObject = await wallet.fees; - late final int feeRate; + late final BigInt feeRate; - switch (ref.read(feeRateTypeStateProvider.state).state) { + switch (ref.read(feeRateTypeMobileStateProvider.state).state) { case FeeRateType.fast: feeRate = feeObject.fast; break; @@ -465,13 +542,13 @@ class _SendViewState extends ConsumerState { feeRate = feeObject.slow; break; default: - feeRate = -1; + feeRate = BigInt.from(-1); } Amount fee; if (coin is Monero) { lib_monero.TransactionPriority specialMoneroId; - switch (ref.read(feeRateTypeStateProvider.state).state) { + switch (ref.read(feeRateTypeMobileStateProvider.state).state) { case FeeRateType.fast: specialMoneroId = lib_monero.TransactionPriority.high; break; @@ -485,53 +562,38 @@ class _SendViewState extends ConsumerState { throw ArgumentError("custom fee not available for monero"); } - fee = await wallet.estimateFeeFor(amount, specialMoneroId.value); - cachedFees[amount] = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + fee = await wallet.estimateFeeFor( + amount, + BigInt.from(specialMoneroId.value), + ); + cachedFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFees[amount]!; } else if (isFiro) { final firoWallet = wallet as FiroWallet; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: fee = await firoWallet.estimateFeeFor(amount, feeRate); - cachedFiroPublicFees[amount] = - ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFiroPublicFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFiroPublicFees[amount]!; - case FiroType.lelantus: - fee = await firoWallet.estimateFeeForLelantus(amount); - cachedFiroLelantusFees[amount] = - ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); - return cachedFiroLelantusFees[amount]!; - case FiroType.spark: + case BalanceType.private: fee = await firoWallet.estimateFeeForSpark(amount); - cachedFiroSparkFees[amount] = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFiroSparkFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFiroSparkFees[amount]!; } } else { fee = await wallet.estimateFeeFor(amount, feeRate); - cachedFees[amount] = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFees[amount]!; } @@ -540,23 +602,21 @@ class _SendViewState extends ConsumerState { Future _previewTransaction() async { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); final wallet = ref.read(pWallets).getWallet(walletId); final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; - if (isFiro) { + if (isFiro || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: availableBalance = wallet.info.cachedBalance.spendable; break; - case FiroType.lelantus: - availableBalance = wallet.info.cachedBalanceSecondary.spendable; - break; - case FiroType.spark: - availableBalance = wallet.info.cachedBalanceTertiary.spendable; + case BalanceType.private: + availableBalance = + isFiro + ? wallet.info.cachedBalanceTertiary.spendable + : wallet.info.cachedBalanceSecondary.spendable; break; } } else { @@ -591,9 +651,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: () { @@ -604,10 +665,7 @@ class _SendViewState extends ConsumerState { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - child: Text( - "Yes", - style: STextStyles.button(context), - ), + child: Text("Yes", style: STextStyles.button(context)), onPressed: () { Navigator.of(context).pop(true); }, @@ -636,9 +694,10 @@ class _SendViewState extends ConsumerState { builder: (context) { return BuildingTransactionDialog( coin: wallet.info.coin, - isSpark: wallet is FiroWallet && + isSpark: + wallet is FiroWallet && ref.read(publicPrivateBalanceStateProvider.state).state == - FiroType.spark, + BalanceType.private, onCancel: () { wasCancelled = true; @@ -650,38 +709,36 @@ class _SendViewState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); Future txDataFuture; if (isPaynymSend) { - final feeRate = ref.read(feeRateTypeStateProvider); + final feeRate = ref.read(feeRateTypeMobileStateProvider); txDataFuture = (wallet as PaynymInterface).preparePaymentCodeSend( txData: TxData( paynymAccountLite: widget.accountLite!, recipients: [ - ( + TxRecipient( address: widget.accountLite!.code, amount: amount, isChange: false, + addressType: AddressType.unknown, ), ], - satsPerVByte: isCustomFee ? customFeeRate : null, + satsPerVByte: isCustomFee.value ? customFeeRate : null, feeRateType: feeRate, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (ref.read(pValidSparkSendToAddress)) { txDataFuture = wallet.prepareSparkMintTransaction( txData: TxData( @@ -693,100 +750,121 @@ class _SendViewState extends ConsumerState { isChange: false, ), ], - feeRateType: ref.read(feeRateTypeStateProvider), - satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + feeRateType: ref.read(feeRateTypeMobileStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : null, + utxos: + (coinControlEnabled && selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } else { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - ( + TxRecipient( address: _address!, amount: amount, isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(_address!)!, ), ], - feeRateType: ref.read(feeRateTypeStateProvider), - satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + feeRateType: ref.read(feeRateTypeMobileStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : null, + utxos: + (coinControlEnabled && selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } break; - case FiroType.lelantus: - txDataFuture = wallet.prepareSendLelantus( - txData: TxData( - recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], - ), - ); - break; - - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: ref.read(pValidSparkSendToAddress) - ? null - : [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], - sparkRecipients: ref.read(pValidSparkSendToAddress) - ? [ - ( - address: _address!, - amount: amount, - memo: memoController.text, - isChange: false, - ), - ] - : null, + recipients: + ref.read(pValidSparkSendToAddress) + ? null + : [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType( + _address!, + )!, + ), + ], + sparkRecipients: + ref.read(pValidSparkSendToAddress) + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + isChange: false, + ), + ] + : null, ), ); break; } + } else if (wallet is MwebInterface && + ref.read(pWalletInfo(walletId)).isMwebEnabled && + ref.read(publicPrivateBalanceStateProvider) == BalanceType.private) { + txDataFuture = wallet.prepareSendMweb( + txData: TxData( + recipients: [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), + ], + feeRateType: ref.read(feeRateTypeDesktopStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : null, + + // these will need to be mweb utxos + // utxos: + // (wallet is CoinControlInterface && + // coinControlEnabled && + // ref.read(pDesktopUseUTXOs).isNotEmpty) + // ? ref.read(pDesktopUseUTXOs) + // : null, + ), + ); } else { final memo = coin is Stellar ? memoController.text : null; txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - ( + TxRecipient( address: _address!, amount: amount, isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, ), ], memo: memo, - feeRateType: ref.read(feeRateTypeStateProvider), - satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + feeRateType: ref.read(feeRateTypeMobileStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : null, + ethEIP1559Fee: ethFee, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); TxData txData = results.first as TxData; @@ -794,9 +872,10 @@ class _SendViewState extends ConsumerState { if (isPaynymSend) { txData = txData.copyWith( paynymAccountLite: widget.accountLite!, - note: noteController.text.isNotEmpty - ? noteController.text - : "PayNym send", + note: + noteController.text.isNotEmpty + ? noteController.text + : "PayNym send", ); } else { txData = txData.copyWith(note: noteController.text); @@ -810,12 +889,13 @@ class _SendViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: txData, - walletId: walletId, - isPaynymTransaction: isPaynymSend, - onSuccess: clearSendForm, - ), + builder: + (_) => ConfirmTransactionView( + txData: txData, + walletId: walletId, + isPaynymTransaction: isPaynymSend, + onSuccess: clearSendForm, + ), settings: const RouteSettings( name: ConfirmTransactionView.routeName, ), @@ -845,9 +925,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: () { @@ -877,7 +958,10 @@ class _SendViewState extends ConsumerState { } } - String _getSendAllTitle(bool showCoinControl, Set selectedUTXOs) { + String _getSendAllTitle( + bool showCoinControl, + Set selectedUTXOs, + ) { if (showCoinControl && selectedUTXOs.isNotEmpty) { return "Send all selected"; } @@ -885,45 +969,115 @@ class _SendViewState extends ConsumerState { return "Send all ${coin.ticker}"; } - Amount _selectedUtxosAmount(Set utxos) => Amount( - rawValue: - utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), - fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, - ); + Amount _selectedUtxosAmount(Set utxos) => Amount( + rawValue: utxos.map((e) => e.value).reduce((v, e) => v += e), + fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, + ); Future _sendAllTapped(bool showCoinControl) async { final Amount amount; if (showCoinControl && selectedUTXOs.isNotEmpty) { amount = _selectedUtxosAmount(selectedUTXOs); - } else if (isFiro) { + } else if (isFiro || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.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; + + case BalanceType.private: + amount = + isFiro + ? ref.read(pWalletBalanceTertiary(walletId)).spendable + : ref.read(pWalletBalanceSecondary(walletId)).spendable; break; } } else { amount = ref.read(pWalletBalance(walletId)).spendable; } - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); _cryptoAmountChanged(); } bool get isPaynymSend => widget.accountLite != null; - bool isCustomFee = false; - + final isCustomFee = ValueNotifier(false); int customFeeRate = 1; + EthEIP1559Fee? ethFee; + + late final bool hasFees; + + void _onSendToAddressPasteButtonPressed() async { + final ClipboardData? data = await clipboard.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")); + } + + if (coin is Epiccash) { + // strip http:// and https:// if content contains @ + content = AddressUtils().formatEpicCashAddress(content); + } + + final trimmed = content.trim(); + final parsed = AddressUtils.parsePaymentUri( + trimmed, + logging: Logging.instance, + ); + if (parsed != null) { + _applyUri(parsed); + } else { + sendToController.text = content; + _address = content; + + _setValidAddressProviders(_address); + + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + } + + void _onFeeSelectPressed() { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: + (_) => TransactionFeeSelectionSheet( + walletId: walletId, + amount: (Decimal.tryParse(cryptoAmountController.text) ?? + ref.watch(pSendAmount)?.decimal ?? + Decimal.zero) + .toAmount(fractionDigits: coin.fractionDigits), + updateChosen: (String fee) { + if (fee == "custom") { + if (!isCustomFee.value) { + setState(() { + isCustomFee.value = true; + }); + } + return; + } + + _setCurrentFee(fee, true); + setState(() { + _calculateFeesFuture = Future(() => fee); + if (isCustomFee.value) { + isCustomFee.value = false; + } + }); + }, + ), + ); + } @override void initState() { @@ -932,16 +1086,24 @@ class _SendViewState extends ConsumerState { ref.refresh(feeSheetSessionCacheProvider); ref.refresh(pIsExchangeAddress); }); + isCustomFee.addListener(() { + if (!isCustomFee.value) { + customFeeRate = 1; + ethFee = null; + } + }); + hasFees = coin is! Epiccash && coin is! NanoCurrency && coin is! Tezos; _currentFee = 0.toAmountAsRaw(fractionDigits: coin.fractionDigits); - _calculateFeesFuture = - calculateFees(0.toAmountAsRaw(fractionDigits: coin.fractionDigits)); + _calculateFeesFuture = calculateFees( + 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), + ); _data = widget.autoFillData; walletId = widget.walletId; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; isStellar = coin is Stellar; isFiro = coin is Firo; + isEth = coin is Ethereum; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); @@ -962,10 +1124,9 @@ class _SendViewState extends ConsumerState { fractionDigits: coin.fractionDigits, ); - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); } sendToController.text = _data.contactLabel; _address = _data.address.trim(); @@ -1040,61 +1201,63 @@ class _SendViewState extends ConsumerState { _cryptoFocus.dispose(); _baseFocus.dispose(); _memoFocus.dispose(); + isCustomFee.dispose(); super.dispose(); } @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final wallet = ref.watch(pWallets).getWallet(walletId); final String locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final showCoinControl = wallet is CoinControlInterface && + final balType = ref.watch(publicPrivateBalanceStateProvider); + + final isMwebEnabled = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final showPrivateBalance = coin is Firo || isMwebEnabled; + + final showCoinControl = ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, ), ) && - (coin is Firo - ? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public - : true); - - if (isFiro) { - final isExchangeAddress = ref.watch(pIsExchangeAddress); + ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && + (showPrivateBalance ? balType == BalanceType.public : true); - ref.listen(publicPrivateBalanceStateProvider, (previous, next) { - selectedUTXOs = {}; + final isExchangeAddress = ref.watch(pIsExchangeAddress); - if (ref.read(pSendAmount) == null) { - setState(() { - _calculateFeesFuture = calculateFees( - 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), - ); - }); - } else { - setState(() { - _calculateFeesFuture = calculateFees( - ref.read(pSendAmount)!, - ); - }); - } + ref.listen(publicPrivateBalanceStateProvider, (previous, next) { + selectedUTXOs = {}; - if (previous != next && - next == FiroType.spark && - isExchangeAddress && - !_isFiroExWarningDisplayed) { - _isFiroExWarningDisplayed = true; - WidgetsBinding.instance.addPostFrameCallback( - (_) => showFiroExchangeAddressWarning( - context, - () => _isFiroExWarningDisplayed = false, - ), + if (ref.read(pSendAmount) == null) { + setState(() { + _calculateFeesFuture = calculateFees( + 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), ); - } - }); - } + }); + } else { + setState(() { + _calculateFeesFuture = calculateFees(ref.read(pSendAmount)!); + }); + } + + if (previous != next && + next == BalanceType.private && + isExchangeAddress && + !_isFiroExWarningDisplayed) { + _isFiroExWarningDisplayed = true; + WidgetsBinding.instance.addPostFrameCallback( + (_) => showFiroExchangeAddressWarning( + context, + () => _isFiroExWarningDisplayed = false, + ), + ); + } + }); // add listener for epic cash to strip http:// and https:// prefixes if the address also ocntains an @ symbol (indicating an epicbox address) if (coin is Epiccash) { @@ -1107,11 +1270,22 @@ class _SendViewState extends ConsumerState { _address = _address!.substring(0, _address!.indexOf("\n")); } - sendToController.text = AddressUtils().formatAddress(_address!); + sendToController.text = AddressUtils().formatEpicCashAddress( + _address!, + ); } }); } + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin)?.value, + ), + ); + } + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -1132,281 +1306,275 @@ class _SendViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - // subtract top and bottom padding set in parent - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + 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), - ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + SvgPicture.file( + File(ref.watch(coinIconProvider(coin))), + width: 22, + height: 22, ), - 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, - // ), - if (isFiro) - Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), - ), - if (coin is! Firo) + const SizedBox(width: 6), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ Text( - "Available balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), + ref.watch(pWalletName(walletId)), + style: STextStyles.titleBold12( + context, + ).copyWith(fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - ], - ), - const Spacer(), - 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; + // const SizedBox( + // height: 2, + // ), + if (isFiro || isMwebEnabled) + Text( + "${balType.name.capitalize()} balance", + style: STextStyles.label( + context, + ).copyWith(fontSize: 10), + ), + if (coin is! Firo) + Text( + "Available balance", + style: STextStyles.label( + context, + ).copyWith(fontSize: 10), + ), + ], + ), + const Spacer(), + Builder( + builder: (context) { + final Amount amount; + if (showPrivateBalance) { + switch (balType) { + case BalanceType.public: + amount = + ref + .read( + pWalletBalance( + walletId, + ), + ) + .spendable; + break; + + case BalanceType.private: + amount = + ref + .read( + isMwebEnabled + ? pWalletBalanceSecondary( + walletId, + ) + : pWalletBalanceTertiary( + walletId, + ), + ) + .spendable; + break; + } + } else { + amount = + ref + .read( + pWalletBalance(walletId), + ) + .spendable; } - } 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, + 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, ), - textAlign: TextAlign.right, - ), - ], + if (price != null) + Text( + "${(amount.decimal * price).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( - isPaynymSend - ? "Send to PayNym address" - : "Send to", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - // if (coin is Monero) - // CustomTextButton( - // text: "Use OpenAlias", - // onTap: () async { - // await showModalBottomSheet( - // context: context, - // builder: (context) => - // OpenAliasBottomSheet( - // onSelected: (address) { - // sendToController.text = address; - // }, - // ), - // ); - // }, - // ), - ], - ), - const SizedBox( - height: 8, - ), - if (isPaynymSend) - TextField( - key: const Key("sendViewPaynymAddressFieldKey"), - controller: sendToController, - enabled: false, - readOnly: true, - style: STextStyles.fieldLabel(context), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isPaynymSend + ? "Send to PayNym address" + : "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + // if (coin is Monero) + // CustomTextButton( + // text: "Use OpenAlias", + // onTap: () async { + // await showModalBottomSheet( + // context: context, + // builder: (context) => + // OpenAliasBottomSheet( + // onSelected: (address) { + // sendToController.text = address; + // }, + // ), + // ); + // }, + // ), + ], ), - if (!isPaynymSend) - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("sendViewAddressFieldKey"), + const SizedBox(height: 8), + if (isPaynymSend) + TextField( + key: const Key("sendViewPaynymAddressFieldKey"), controller: sendToController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - // inputFormatters: [ - // FilteringTextInputFormatter.allow( - // RegExp("[a-zA-Z0-9]{34}")), - // ], - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, + enabled: false, + readOnly: true, + style: STextStyles.fieldLabel(context), + ), + if (!isPaynymSend) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onChanged: (newValue) { - final trimmed = newValue.trim(); - - if ((trimmed.length - (_address?.length ?? 0)).abs() > 1) { - final parsed = AddressUtils.parsePaymentUri( - trimmed, - logging: Logging.instance, - ); - if (parsed != null) { - _applyUri(parsed); + child: TextField( + key: const Key("sendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) async { + final trimmed = newValue.trim(); + + if ((trimmed.length - + (_address?.length ?? 0)) + .abs() > + 1) { + final parsed = + AddressUtils.parsePaymentUri( + trimmed, + logging: Logging.instance, + ); + if (parsed != null) { + _applyUri(parsed); + } else { + await _checkSparkNameAndOrSetAddress( + newValue, + ); + } } else { - _address = newValue; - sendToController.text = newValue; + await _checkSparkNameAndOrSetAddress( + newValue, + setController: false, + ); } - } else { - _address = newValue; - } - _setValidAddressProviders(_address); - - setState(() { - _addressToggleFlag = newValue.isNotEmpty; - }); - }, - focusNode: _addressFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${coin.ticker} address", - _addressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _addressToggleFlag - ? TextFieldIconButton( + _setValidAddressProviders(_address); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${coin.ticker} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: + sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( semanticsLabel: "Clear Button. Clears The Address Field Input.", key: const Key( @@ -1425,149 +1593,97 @@ class _SendViewState extends ConsumerState { }, child: const XIcon(), ) - : TextFieldIconButton( + : TextFieldIconButton( semanticsLabel: "Paste Button. Pastes From Clipboard To Address Field Input.", key: const Key( "sendViewPasteAddressFieldButtonKey", ), - onTap: () async { - final ClipboardData? data = - await clipboard.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", - ), - ); - } - - if (coin is Epiccash) { - // strip http:// and https:// if content contains @ - content = AddressUtils() - .formatAddress( - content, - ); - } - - final trimmed = content.trim(); - final parsed = AddressUtils.parsePaymentUri( - trimmed, - logging: Logging.instance, - ); - if (parsed != null) { - _applyUri(parsed); - } else { - sendToController.text = - content; - _address = content; - - _setValidAddressProviders(_address,); - - setState(() { - _addressToggleFlag = - sendToController - .text - .isNotEmpty; - }); - } - } - }, - child: sendToController - .text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + onTap: _pasteAddress, + child: + sendToController + .text + .isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Address Book Button. Opens Address Book For Address Field.", + key: const Key( + "sendViewAddressBookButtonKey", ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - semanticsLabel: - "Address Book Button. Opens Address Book For Address Field.", - key: const Key( - "sendViewAddressBookButtonKey", + onTap: () { + Navigator.of( + context, + ).pushNamed( + AddressBookView.routeName, + arguments: widget.coin, + ); + }, + child: const AddressBookIcon(), ), - onTap: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: widget.coin, - ); - }, - child: const AddressBookIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - semanticsLabel: - "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key( - "sendViewScanQrButtonKey", + if (sendToController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: _scanQr, + child: const QrCodeIcon(), ), - onTap: _scanQr, - child: const QrCodeIcon(), - ), - ], + ], + ), ), ), ), ), ), - ), - const SizedBox( - height: 10, - ), - if (isStellar || - (ref.watch(pValidSparkSendToAddress) && - ref.watch( - publicPrivateBalanceStateProvider, - ) != - FiroType.lelantus)) - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("sendViewMemoFieldKey"), - maxLength: (coin is Firo) ? 31 : null, - controller: memoController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - focusNode: _memoFocus, - style: STextStyles.field(context), - onChanged: (_) { - setState(() {}); - }, - decoration: standardInputDecoration( - "Enter memo (optional)", - _memoFocus, - context, - ).copyWith( - counterText: '', - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: memoController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - memoController.text.isNotEmpty - ? TextFieldIconButton( + const SizedBox(height: 10), + if (isStellar || + ref.watch(pValidSparkSendToAddress)) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("sendViewMemoFieldKey"), + maxLength: (coin is Firo) ? 31 : null, + controller: memoController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + focusNode: _memoFocus, + style: STextStyles.field(context), + onChanged: (_) { + setState(() {}); + }, + decoration: standardInputDecoration( + "Enter memo (optional)", + _memoFocus, + context, + ).copyWith( + counterText: '', + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: + memoController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + memoController.text.isNotEmpty + ? TextFieldIconButton( semanticsLabel: "Clear Button. Clears The Memo Field Input.", key: const Key( @@ -1579,7 +1695,7 @@ class _SendViewState extends ConsumerState { }, child: const XIcon(), ) - : TextFieldIconButton( + : TextFieldIconButton( semanticsLabel: "Paste Button. Pastes From Clipboard To Memo Field Input.", key: const Key( @@ -1588,11 +1704,12 @@ class _SendViewState extends ConsumerState { onTap: () async { final ClipboardData? data = await clipboard.getData( - Clipboard.kTextPlain, - ); + Clipboard.kTextPlain, + ); if (data?.text != null && data! - .text!.isNotEmpty) { + .text! + .isNotEmpty) { final String content = data.text!.trim(); @@ -1604,466 +1721,502 @@ class _SendViewState extends ConsumerState { }, child: const ClipboardIcon(), ), - ], + ], + ), ), ), ), ), ), - ), - Builder( - builder: (_) { - final String? error; - - if (_address == null || _address!.isEmpty) { - error = null; - } else if (isFiro) { - if (ref.watch( - publicPrivateBalanceStateProvider, - ) == - FiroType.lelantus) { + Builder( + builder: (_) { + final String? error; + + if (_address == null || _address!.isEmpty) { + error = null; + } else if (isFiro) { if (_data != null && _data.contactLabel == _address) { - error = SparkInterface.validateSparkAddress( - address: _data.address, - isTestNet: coin.network == - CryptoCurrencyNetwork.test, - ) - ? "Unsupported" - : null; - } else if (ref - .watch(pValidSparkSendToAddress)) { - error = "Unsupported"; + error = null; + } else if (!ref.watch(pValidSendToAddress) && + !ref.watch(pValidSparkSendToAddress)) { + error = "Invalid address"; } else { - error = ref.watch(pValidSendToAddress) - ? null - : "Invalid address"; + error = null; } } else { if (_data != null && _data.contactLabel == _address) { error = null; - } else if (!ref.watch(pValidSendToAddress) && - !ref.watch(pValidSparkSendToAddress)) { + } else if (!ref.watch(pValidSendToAddress)) { error = "Invalid address"; } else { error = null; } } - } else { - if (_data != null && - _data.contactLabel == _address) { - error = null; - } else if (!ref.watch(pValidSendToAddress)) { - error = "Invalid address"; - } else { - error = null; - } - } - if (error == null || error.isEmpty) { - return Container(); - } else { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - top: 4.0, - ), - child: Text( - error, - textAlign: TextAlign.left, - style: - STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .textError, + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: STextStyles.label( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textError, + ), ), ), - ), - ); - } - }, - ), - if (isFiro) - const SizedBox( - height: 12, - ), - if (isFiro) - Text( - "Send from", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - if (isFiro) - const SizedBox( - height: 8, + ); + } + }, ), - if (isFiro) - Stack( - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, - readOnly: true, - textInputAction: TextInputAction.none, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, + if (isFiro || isMwebEnabled) + const SizedBox(height: 12), + if (isFiro || isMwebEnabled) + Text( + "Send from", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + if (isFiro || isMwebEnabled) + const SizedBox(height: 8), + if (isFiro || isMwebEnabled) + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, ), - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, ), - onPressed: () { - showModalBottomSheet( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - FiroBalanceSelectionSheet( - walletId: walletId, + child: RawMaterialButton( + splashColor: + Theme.of( + context, + ).extension()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", - style: STextStyles.itemSubtitle12( - context, - ), - ), - const SizedBox( - width: 10, + ), + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), ), - 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( - pWalletBalanceSecondary( - walletId, - ), - ) - .spendable; - break; - case FiroType.spark: - amount = ref + ), + builder: + (_) => DualBalanceSelectionSheet( + walletId: walletId, + ), + ); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", + style: + STextStyles.itemSubtitle12( + context, + ), + ), + const SizedBox(width: 10), + Builder( + builder: (_) { + final Amount amount; + switch (ref + .read( + publicPrivateBalanceStateProvider + .state, + ) + .state) { + case BalanceType.public: + amount = + ref + .watch( + pWalletBalance( + walletId, + ), + ) + .spendable; + break; + case BalanceType.private: + amount = + ref + .watch( + isFiro + ? pWalletBalanceTertiary( + walletId, + ) + : pWalletBalanceSecondary( + walletId, + ), + ) + .spendable; + break; + } + + return Text( + ref .watch( - pWalletBalanceTertiary( - walletId, + pAmountFormatter( + coin, ), ) - .spendable; - break; - } - - return Text( - ref - .watch( - pAmountFormatter(coin), - ) - .format( - amount, - ), - style: - STextStyles.itemSubtitle( - context, - ), - ); - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - ], + .format(amount), + style: + STextStyles.itemSubtitle( + context, + ), + ); + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: + Theme.of(context) + .extension()! + .textSubtitle2, + ), + ], + ), ), ), - ), - ], - ), - const SizedBox( - height: 12, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, + ], ), - if (coin is! Ethereum && coin is! Tezos) - CustomTextButton( - text: _getSendAllTitle( - showCoinControl, - selectedUTXOs, - ), - onTap: () => _sendAllTapped(showCoinControl), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - ], - ), - const SizedBox( - height: 8, - ), - 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("amountInputFieldCryptoTextFieldKey"), - controller: cryptoAmountController, - focusNode: _cryptoFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - textAlign: TextAlign.right, - inputFormatters: [ - AmountInputFormatter( - decimals: coin.fractionDigits, - unit: ref.watch(pAmountUnit(coin)), - locale: locale, - ), - - // regex to validate a crypto amount with 8 decimal places - // TextInputFormatter.withFunction((oldValue, - // newValue) => - // // RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - // // RegExp(r'^\d{1,3}([,\.]\d+)?|[,\.\d]+$') - // getAmountRegex(locale, coin.fractionDigits) - // .hasMatch(newValue.text) - // ? newValue - // : oldValue), - ], - 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(coin)) - .unitForCoin(coin), - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + if (coin is! Ethereum && coin is! Tezos) + CustomTextButton( + text: _getSendAllTitle( + showCoinControl, + selectedUTXOs, ), + onTap: + () => _sendAllTapped(showCoinControl), ), - ), - ), - ), - ), - if (Prefs.instance.externalCalls) - const SizedBox( - height: 8, + ], ), - if (Prefs.instance.externalCalls) + const SizedBox(height: 8), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), - key: - const Key("amountInputFieldFiatTextFieldKey"), - controller: baseAmountController, - focusNode: _baseFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + key: const Key( + "amountInputFieldCryptoTextFieldKey", + ), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( - decimals: 2, + decimals: coin.fractionDigits, + unit: ref.watch(pAmountUnit(coin)), locale: locale, ), - // regex to validate a fiat amount with 2 decimal places + + // regex to validate a crypto amount with 8 decimal places // TextInputFormatter.withFunction((oldValue, // newValue) => - // // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') - // getAmountRegex(locale, 2) + // // RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + // // RegExp(r'^\d{1,3}([,\.]\d+)?|[,\.\d]+$') + // getAmountRegex(locale, coin.fractionDigits) // .hasMatch(newValue.text) // ? newValue // : oldValue), ], - onChanged: _fiatFieldChanged, decoration: InputDecoration( contentPadding: const EdgeInsets.only( top: 12, right: 12, ), hintText: "0", - hintStyle: - STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), + 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, + ref + .watch(pAmountUnit(coin)) + .unitForCoin(coin), + style: STextStyles.smallMed14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), ), ), ), - 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, + if (Prefs.instance.externalCalls) + const SizedBox(height: 8), + if (Prefs.instance.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: baseAmountController, + focusNode: _baseFocus, + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + AmountInputFormatter( + decimals: 2, + locale: locale, + ), + // regex to validate a fiat amount with 2 decimal places + // TextInputFormatter.withFunction((oldValue, + // newValue) => + // // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + // getAmountRegex(locale, 2) + // .hasMatch(newValue.text) + // ? newValue + // : oldValue), + ], + onChanged: _fiatFieldChanged, + 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, + ), + ), ), ), - 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 (context.mounted) { - final spendable = ref - .read(pWalletBalance(walletId)) - .spendable; - - Amount? amount; - if (ref.read(pSendAmount) != null) { - amount = ref.read(pSendAmount)!; - - if (spendable == amount) { - // this is now a send all - } else { - amount += _currentFee; - } + ), + ), + 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), + ); } - final result = - await Navigator.of(context) - .pushNamed( - CoinControlView.routeName, - arguments: Tuple4( - walletId, - CoinControlViewType.use, - amount, - selectedUTXOs, - ), - ); + if (context.mounted) { + final spendable = + ref + .read( + pWalletBalance(walletId), + ) + .spendable; + + Amount? amount; + if (ref.read(pSendAmount) != null) { + amount = ref.read(pSendAmount)!; + + if (spendable == amount) { + // this is now a send all + } else { + amount += _currentFee; + } + } - if (result is Set) { - setState(() { - selectedUTXOs = result; - }); + final result = await Navigator.of( + context, + ).pushNamed( + CoinControlView.routeName, + arguments: Tuple4( + walletId, + CoinControlViewType.use, + amount, + selectedUTXOs + .map((e) => e.utxo) + .toSet(), + ), + ); + + if (result is Set) { + setState(() { + selectedUTXOs = + result + .map( + (e) => StandardInput(e), + ) + .toSet(); + }); + } } - } - }, + }, + ), + ], + ), + ), + const SizedBox(height: 12), + if (coin is Epiccash) + Text( + "On chain Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + if (coin is Epiccash) const SizedBox(height: 8), + if (coin is Epiccash) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + maxLength: 256, + controller: onChainNoteController, + focusNode: _onChainNoteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _onChainNoteFocusNode, + context, + ).copyWith( + suffixIcon: + onChainNoteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, + ), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + onChainNoteController + .text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), - ], + ), ), - ), - const SizedBox( - height: 12, - ), - if (coin is Epiccash) + if (coin is Epiccash) const SizedBox(height: 12), Text( - "On chain Note (optional)", + (coin is Epiccash) + ? "Local Note (optional)" + : "Note (optional)", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin is Epiccash) - const SizedBox( - height: 8, - ), - if (coin is Epiccash) + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -2072,271 +2225,107 @@ class _SendViewState extends ConsumerState { autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - maxLength: 256, - controller: onChainNoteController, - focusNode: _onChainNoteFocusNode, + controller: noteController, + focusNode: _noteFocusNode, style: STextStyles.field(context), onChanged: (_) => setState(() {}), decoration: standardInputDecoration( "Type something...", - _onChainNoteFocusNode, + _noteFocusNode, context, ).copyWith( - suffixIcon: onChainNoteController - .text.isNotEmpty - ? Padding( - padding: - const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - onChainNoteController - .text = ""; - }); - }, - ), - ], + suffixIcon: + noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, ), - ), - ) - : null, + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = + ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), ), - if (coin is Epiccash) - const SizedBox( - height: 12, - ), - Text( - (coin is Epiccash) - ? "Local Note (optional)" - : "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), + if (hasFees) + Text( + "Transaction fee ${isEth + ? isCustomFee.value + ? "" + : "(max)" + : "(estimated)"}", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - ), - ), - const SizedBox( - height: 12, - ), - if (coin is! Epiccash && - coin is! NanoCurrency && - coin is! Tezos) - Text( - "Transaction fee (estimated)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - if (coin is! Epiccash && - coin is! NanoCurrency && - coin is! Tezos) - const SizedBox( - height: 8, - ), - if (coin is! Epiccash && - coin is! NanoCurrency && - coin is! Tezos) - Stack( - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: - Util.isDesktop ? false : true, - controller: feeController, - readOnly: true, - textInputAction: TextInputAction.none, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, + if (hasFees) const SizedBox(height: 8), + if (hasFees) + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: feeController, + readOnly: true, + textInputAction: TextInputAction.none, ), - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, ), - onPressed: isFiro && - ref - .watch( - publicPrivateBalanceStateProvider - .state, - ) - .state != - FiroType.public - ? null - : () { - showModalBottomSheet( - backgroundColor: - Colors.transparent, - context: context, - shape: - const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - TransactionFeeSelectionSheet( - walletId: walletId, - amount: (Decimal.tryParse( - cryptoAmountController - .text, - ) ?? - ref - .watch(pSendAmount) - ?.decimal ?? - Decimal.zero) - .toAmount( - fractionDigits: - coin.fractionDigits, - ), - updateChosen: (String fee) { - if (fee == "custom") { - if (!isCustomFee) { - setState(() { - isCustomFee = true; - }); - } - return; - } - - _setCurrentFee( - fee, - true, - ); - setState(() { - _calculateFeesFuture = - Future(() => fee); - if (isCustomFee) { - isCustomFee = false; - } - }); - }, - ), - ); - }, - child: (isFiro && - ref - .watch( - publicPrivateBalanceStateProvider - .state, - ) - .state != - FiroType.public) - ? Row( - children: [ - FutureBuilder( - future: _calculateFeesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - _setCurrentFee( - snapshot.data!, - false, - ); - return Text( - "~${snapshot.data!}", - style: STextStyles - .itemSubtitle( - context, - ), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: STextStyles - .itemSubtitle( - context, - ), - ); - } - }, - ), - ], - ) - : Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( + child: RawMaterialButton( + splashColor: + Theme.of( + context, + ).extension()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: + isFiro && + ref + .watch( + publicPrivateBalanceStateProvider + .state, + ) + .state != + BalanceType.public + ? null + : _onFeeSelectPressed, + child: + (isFiro && + ref + .watch( + publicPrivateBalanceStateProvider + .state, + ) + .state != + BalanceType.public) + ? Row( children: [ - Text( - ref - .watch( - feeRateTypeStateProvider - .state, - ) - .state - .prettyName, - style: STextStyles - .itemSubtitle12( - context, - ), - ), - const SizedBox( - width: 10, - ), FutureBuilder( future: _calculateFeesFuture, - builder: - (context, snapshot) { + builder: ( + context, + snapshot, + ) { if (snapshot.connectionState == ConnectionState .done && @@ -2346,90 +2335,155 @@ class _SendViewState extends ConsumerState { false, ); return Text( - isCustomFee - ? "" - : "~${snapshot.data!}", - style: STextStyles - .itemSubtitle( - context, - ), + "~${snapshot.data!}", + style: + STextStyles.itemSubtitle( + context, + ), ); } else { return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: STextStyles - .itemSubtitle( - context, - ), + stringsToLoopThrough: + stringsToLoopThrough, + style: + STextStyles.itemSubtitle( + context, + ), ); } }, ), ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state + .prettyName, + style: + STextStyles.itemSubtitle12( + context, + ), + ), + const SizedBox(width: 10), + FutureBuilder( + future: + _calculateFeesFuture, + builder: ( + context, + snapshot, + ) { + if (snapshot.connectionState == + ConnectionState + .done && + snapshot + .hasData) { + _setCurrentFee( + snapshot.data!, + false, + ); + return Text( + isCustomFee.value + ? "" + : "~${snapshot.data!}", + style: + STextStyles.itemSubtitle( + context, + ), + ); + } else { + return AnimatedText( + stringsToLoopThrough: + stringsToLoopThrough, + style: + STextStyles.itemSubtitle( + context, + ), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textSubtitle2, + ), + ], ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - ], - ), + ), ), + ], + ), + if (isCustomFee.value && !isEth) + Padding( + padding: const EdgeInsets.only( + bottom: 12, + top: 16, + ), + child: FeeSlider( + coin: coin, + onSatVByteChanged: (rate) { + customFeeRate = rate; + }, ), - ], - ), - if (isCustomFee) - Padding( - padding: const EdgeInsets.only( - bottom: 12, - top: 16, ), - child: FeeSlider( - coin: coin, - onSatVByteChanged: (rate) { - customFeeRate = rate; - }, + if (isCustomFee.value && isEth) + const SizedBox(height: 12), + if (isCustomFee.value && isEth) + EthFeeForm( + minGasLimit: kEthereumMinGasLimit, + stateChanged: (fee) => ethFee = fee, + ), + const Spacer(), + const SizedBox(height: 12), + TextButton( + onPressed: + ref.watch(pPreviewTxButtonEnabled(coin)) + ? _previewTransaction + : null, + style: + ref.watch(pPreviewTxButtonEnabled(coin)) + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle( + context, + ), + child: Text( + "Preview", + style: STextStyles.button(context), ), ), - const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: ref.watch(pPreviewTxButtonEnabled(coin)) - ? _previewTransaction - : null, - style: ref.watch(pPreviewTxButtonEnabled(coin)) - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Preview", - style: STextStyles.button(context), - ), - ), - const SizedBox( - height: 4, - ), - ], + const SizedBox(height: 16), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart new file mode 100644 index 000000000..fbd9ad996 --- /dev/null +++ b/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart @@ -0,0 +1,259 @@ +/* + * 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 '../../../providers/wallet/public_private_balance_state_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/amount/amount_formatter.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../wallets/crypto_currency/coins/firo.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; + +class DualBalanceSelectionSheet extends ConsumerStatefulWidget { + const DualBalanceSelectionSheet({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => + _FiroBalanceSelectionSheetState(); +} + +class _FiroBalanceSelectionSheetState + extends ConsumerState { + late final String walletId; + + @override + void initState() { + walletId = widget.walletId; + super.initState(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final coin = ref.watch(pWalletCoin(walletId)); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Padding( + padding: const EdgeInsets.only(left: 24, right: 24, top: 10, bottom: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + width: 60, + height: 4, + ), + ), + const SizedBox(height: 36), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Select balance", + style: STextStyles.pageTitleH2(context), + textAlign: TextAlign.left, + ), + const SizedBox(height: 16), + GestureDetector( + onTap: () { + final state = + ref.read(publicPrivateBalanceStateProvider.state).state; + if (state != BalanceType.private) { + ref.read(publicPrivateBalanceStateProvider.state).state = + BalanceType.private; + } + 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: BalanceType.private, + groupValue: + ref + .watch( + publicPrivateBalanceStateProvider + .state, + ) + .state, + onChanged: (x) { + ref + .read( + publicPrivateBalanceStateProvider.state, + ) + .state = BalanceType.private; + + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row( + // children: [ + Text( + "Private balance", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + const SizedBox(width: 2), + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + ref + .watch( + coin is Firo + ? pWalletBalanceTertiary( + walletId, + ) + : pWalletBalanceSecondary( + walletId, + ), + ) + .spendable, + ), + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + // ], + // ), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + GestureDetector( + onTap: () { + final state = + ref.read(publicPrivateBalanceStateProvider.state).state; + if (state != BalanceType.public) { + ref.read(publicPrivateBalanceStateProvider.state).state = + BalanceType.public; + } + Navigator.of(context).pop(); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: BalanceType.public, + groupValue: + ref + .watch( + publicPrivateBalanceStateProvider + .state, + ) + .state, + onChanged: (x) { + ref + .read( + publicPrivateBalanceStateProvider.state, + ) + .state = BalanceType.public; + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row( + // children: [ + Text( + "Public balance", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + const SizedBox(width: 2), + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + ref + .watch(pWalletBalance(walletId)) + .spendable, + ), + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + const SizedBox(height: 24), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart deleted file mode 100644 index 912fad545..000000000 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ /dev/null @@ -1,356 +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_riverpod/flutter_riverpod.dart'; - -import '../../../providers/providers.dart'; -import '../../../providers/wallet/public_private_balance_state_provider.dart'; -import '../../../themes/stack_colors.dart'; -import '../../../utilities/amount/amount_formatter.dart'; -import '../../../utilities/constants.dart'; -import '../../../utilities/text_styles.dart'; -import '../../../wallets/wallet/impl/firo_wallet.dart'; - -class FiroBalanceSelectionSheet extends ConsumerStatefulWidget { - const FiroBalanceSelectionSheet({ - super.key, - required this.walletId, - }); - - final String walletId; - - @override - ConsumerState createState() => - _FiroBalanceSelectionSheetState(); -} - -class _FiroBalanceSelectionSheetState - extends ConsumerState { - late final String walletId; - - @override - void initState() { - walletId = widget.walletId; - super.initState(); - } - - @override - Widget build(BuildContext context) { - debugPrint("BUILD: $runtimeType"); - - final wallet = - ref.watch(pWallets.select((value) => value.getWallet(walletId))); - final firoWallet = wallet as FiroWallet; - - final coin = wallet.info.coin; - - return Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - child: Padding( - padding: const EdgeInsets.only( - left: 24, - right: 24, - top: 10, - bottom: 0, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - width: 60, - height: 4, - ), - ), - const SizedBox( - height: 36, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Select balance", - style: STextStyles.pageTitleH2(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 16, - ), - GestureDetector( - onTap: () { - final state = - ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != FiroType.spark) { - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; - } - 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.spark, - groupValue: ref - .watch( - publicPrivateBalanceStateProvider.state, - ) - .state, - onChanged: (x) { - ref - .read( - publicPrivateBalanceStateProvider.state, - ) - .state = FiroType.spark; - - Navigator.of(context).pop(); - }, - ), - ), - ], - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Row( - // children: [ - Text( - "Spark balance", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - width: 2, - ), - Text( - ref.watch(pAmountFormatter(coin)).format( - firoWallet - .info.cachedBalanceTertiary.spendable, - ), - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - ], - ), - // ], - // ), - ), - ], - ), - ), - ), - 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: [ - 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, - ), - GestureDetector( - onTap: () { - final state = - ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != FiroType.public) { - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; - } - Navigator.of(context).pop(); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: FiroType.public, - groupValue: ref - .watch( - publicPrivateBalanceStateProvider.state, - ) - .state, - onChanged: (x) { - ref - .read( - publicPrivateBalanceStateProvider.state, - ) - .state = FiroType.public; - Navigator.of(context).pop(); - }, - ), - ), - ], - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Row( - // children: [ - Text( - "Public balance", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - width: 2, - ), - Text( - ref.watch(pAmountFormatter(coin)).format( - firoWallet.info.cachedBalance.spendable, - ), - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - ], - ), - ), - ], - ), - ), - ), - const SizedBox( - height: 16, - ), - const SizedBox( - height: 24, - ), - ], - ), - ], - ), - ), - ); - } -} 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 2586d88ee..3ec7048a3 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 @@ -8,8 +8,8 @@ * */ -import 'package:flutter/material.dart'; import 'package:cs_monero/cs_monero.dart' as lib_monero; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/paymint/fee_object_model.dart'; @@ -32,8 +32,8 @@ import '../../../widgets/animated_text.dart'; final feeSheetSessionCacheProvider = ChangeNotifierProvider((ref) { - return FeeSheetSessionCache(); -}); + return FeeSheetSessionCache(); + }); class FeeSheetSessionCache extends ChangeNotifier { final Map fast = {}; @@ -79,7 +79,7 @@ class _TransactionFeeSelectionSheetState Future feeFor({ required Amount amount, required FeeRateType feeRateType, - required int feeRate, + required BigInt feeRate, required CryptoCurrency coin, }) async { switch (feeRateType) { @@ -91,27 +91,27 @@ class _TransactionFeeSelectionSheetState if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.high.value, + BigInt.from(lib_monero.TransactionPriority.high.value), ); ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + case BalanceType.private: + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); + case BalanceType.public: + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - await wallet.estimateFeeFor(amount, feeRate); + ref.read(feeSheetSessionCacheProvider).fast[amount] = await wallet + .estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -128,21 +128,21 @@ class _TransactionFeeSelectionSheetState if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.medium.value, + BigInt.from(lib_monero.TransactionPriority.medium.value), ); ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + case BalanceType.private: + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); + case BalanceType.public: + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else { @@ -164,26 +164,26 @@ class _TransactionFeeSelectionSheetState if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.normal.value, + BigInt.from(lib_monero.TransactionPriority.normal.value), ); ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + case BalanceType.private: + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); + case BalanceType.public: + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - await wallet.estimateFeeFor(amount, feeRate); + ref.read(feeSheetSessionCacheProvider).slow[amount] = await wallet + .estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -243,17 +243,10 @@ class _TransactionFeeSelectionSheetState return Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Padding( - padding: const EdgeInsets.only( - left: 24, - right: 24, - top: 10, - bottom: 0, - ), + padding: const EdgeInsets.only(left: 24, right: 24, top: 10, bottom: 0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -261,9 +254,10 @@ class _TransactionFeeSelectionSheetState Center( child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -272,13 +266,12 @@ class _TransactionFeeSelectionSheetState height: 4, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), FutureBuilder( - future: widget.isToken - ? ref.read(pCurrentTokenWallet)!.fees - : wallet.fees, + future: + widget.isToken + ? ref.read(pCurrentTokenWallet)!.fees + : wallet.fees, builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -293,19 +286,21 @@ class _TransactionFeeSelectionSheetState style: STextStyles.pageTitleH2(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), GestureDetector( onTap: () { final state = - ref.read(feeRateTypeStateProvider.state).state; + ref + .read(feeRateTypeMobileStateProvider.state) + .state; if (state != FeeRateType.fast) { - ref.read(feeRateTypeStateProvider.state).state = + ref.read(feeRateTypeMobileStateProvider.state).state = FeeRateType.fast; } - final String? fee = - getAmount(FeeRateType.fast, wallet.info.coin); + final String? fee = getAmount( + FeeRateType.fast, + wallet.info.coin, + ); if (fee != null) { widget.updateChosen(fee); } @@ -323,16 +318,24 @@ class _TransactionFeeSelectionSheetState width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: FeeRateType.fast, - groupValue: ref - .watch(feeRateTypeStateProvider.state) - .state, + groupValue: + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state, onChanged: (x) { ref - .read(feeRateTypeStateProvider.state) + .read( + feeRateTypeMobileStateProvider + .state, + ) .state = FeeRateType.fast; Navigator.of(context).pop(); @@ -341,9 +344,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -355,15 +356,14 @@ class _TransactionFeeSelectionSheetState style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - const SizedBox( - width: 2, - ), + const SizedBox(width: 2), if (feeObject == null) AnimatedText( stringsToLoopThrough: stringsToLoopThrough, - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), if (feeObject != null) FutureBuilder( @@ -381,15 +381,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${ref.watch( - pAmountFormatter( - coin, - ), - ).format( - snapshot.data!, - indicatePrecisionLoss: - false, - )})", + "(~${ref.watch(pAmountFormatter(coin)).format(snapshot.data!, indicatePrecisionLoss: false)})", style: STextStyles.itemSubtitle( context, ), @@ -408,9 +400,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (feeObject == null && coin is! Ethereum) AnimatedText( stringsToLoopThrough: @@ -433,19 +423,21 @@ class _TransactionFeeSelectionSheetState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), GestureDetector( onTap: () { final state = - ref.read(feeRateTypeStateProvider.state).state; + ref + .read(feeRateTypeMobileStateProvider.state) + .state; if (state != FeeRateType.average) { - ref.read(feeRateTypeStateProvider.state).state = + ref.read(feeRateTypeMobileStateProvider.state).state = FeeRateType.average; } - final String? fee = - getAmount(FeeRateType.average, coin); + final String? fee = getAmount( + FeeRateType.average, + coin, + ); if (fee != null) { widget.updateChosen(fee); } @@ -462,16 +454,24 @@ class _TransactionFeeSelectionSheetState width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: FeeRateType.average, - groupValue: ref - .watch(feeRateTypeStateProvider.state) - .state, + groupValue: + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state, onChanged: (x) { ref - .read(feeRateTypeStateProvider.state) + .read( + feeRateTypeMobileStateProvider + .state, + ) .state = FeeRateType.average; Navigator.of(context).pop(); }, @@ -479,9 +479,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -493,15 +491,14 @@ class _TransactionFeeSelectionSheetState style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - const SizedBox( - width: 2, - ), + const SizedBox(width: 2), if (feeObject == null) AnimatedText( stringsToLoopThrough: stringsToLoopThrough, - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), if (feeObject != null) FutureBuilder( @@ -519,15 +516,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${ref.watch( - pAmountFormatter( - coin, - ), - ).format( - snapshot.data!, - indicatePrecisionLoss: - false, - )})", + "(~${ref.watch(pAmountFormatter(coin)).format(snapshot.data!, indicatePrecisionLoss: false)})", style: STextStyles.itemSubtitle( context, ), @@ -546,9 +535,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (feeObject == null && coin is! Ethereum) AnimatedText( stringsToLoopThrough: @@ -571,15 +558,15 @@ class _TransactionFeeSelectionSheetState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), GestureDetector( onTap: () { final state = - ref.read(feeRateTypeStateProvider.state).state; + ref + .read(feeRateTypeMobileStateProvider.state) + .state; if (state != FeeRateType.slow) { - ref.read(feeRateTypeStateProvider.state).state = + ref.read(feeRateTypeMobileStateProvider.state).state = FeeRateType.slow; } final String? fee = getAmount(FeeRateType.slow, coin); @@ -599,16 +586,24 @@ class _TransactionFeeSelectionSheetState width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: FeeRateType.slow, - groupValue: ref - .watch(feeRateTypeStateProvider.state) - .state, + groupValue: + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state, onChanged: (x) { ref - .read(feeRateTypeStateProvider.state) + .read( + feeRateTypeMobileStateProvider + .state, + ) .state = FeeRateType.slow; Navigator.of(context).pop(); }, @@ -616,9 +611,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -630,15 +623,14 @@ class _TransactionFeeSelectionSheetState style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - const SizedBox( - width: 2, - ), + const SizedBox(width: 2), if (feeObject == null) AnimatedText( stringsToLoopThrough: stringsToLoopThrough, - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), if (feeObject != null) FutureBuilder( @@ -656,15 +648,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${ref.watch( - pAmountFormatter( - coin, - ), - ).format( - snapshot.data!, - indicatePrecisionLoss: - false, - )})", + "(~${ref.watch(pAmountFormatter(coin)).format(snapshot.data!, indicatePrecisionLoss: false)})", style: STextStyles.itemSubtitle( context, ), @@ -683,9 +667,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (feeObject == null && coin is! Ethereum) AnimatedText( stringsToLoopThrough: @@ -708,17 +690,18 @@ class _TransactionFeeSelectionSheetState ), ), ), - const SizedBox( - height: 24, - ), - if (wallet is ElectrumXInterface) + const SizedBox(height: 24), + if (wallet is ElectrumXInterface || coin is Ethereum) GestureDetector( onTap: () { final state = - ref.read(feeRateTypeStateProvider.state).state; + ref + .read(feeRateTypeMobileStateProvider.state) + .state; if (state != FeeRateType.custom) { - ref.read(feeRateTypeStateProvider.state).state = - FeeRateType.custom; + ref + .read(feeRateTypeMobileStateProvider.state) + .state = FeeRateType.custom; } widget.updateChosen("custom"); @@ -735,17 +718,23 @@ class _TransactionFeeSelectionSheetState width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: FeeRateType.custom, - groupValue: ref - .watch(feeRateTypeStateProvider.state) - .state, + groupValue: + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state, onChanged: (x) { ref .read( - feeRateTypeStateProvider.state, + feeRateTypeMobileStateProvider + .state, ) .state = FeeRateType.custom; Navigator.of(context).pop(); @@ -754,9 +743,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -765,15 +752,14 @@ class _TransactionFeeSelectionSheetState children: [ Text( FeeRateType.custom.prettyName, - style: - STextStyles.titleBold12(context), + style: STextStyles.titleBold12( + context, + ), textAlign: TextAlign.left, ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), ], ), ), @@ -781,10 +767,8 @@ class _TransactionFeeSelectionSheetState ), ), ), - if (wallet is ElectrumXInterface) - const SizedBox( - height: 24, - ), + if (wallet is ElectrumXInterface || coin is Ethereum) + const SizedBox(height: 24), ], ); }, @@ -800,7 +784,9 @@ class _TransactionFeeSelectionSheetState switch (feeRateType) { case FeeRateType.fast: if (ref.read(feeSheetSessionCacheProvider).fast[amount] != null) { - return ref.read(pAmountFormatter(coin)).format( + return ref + .read(pAmountFormatter(coin)) + .format( ref.read(feeSheetSessionCacheProvider).fast[amount]!, indicatePrecisionLoss: false, withUnitName: false, @@ -810,7 +796,9 @@ class _TransactionFeeSelectionSheetState case FeeRateType.average: if (ref.read(feeSheetSessionCacheProvider).average[amount] != null) { - return ref.read(pAmountFormatter(coin)).format( + return ref + .read(pAmountFormatter(coin)) + .format( ref.read(feeSheetSessionCacheProvider).average[amount]!, indicatePrecisionLoss: false, withUnitName: false, @@ -820,7 +808,9 @@ class _TransactionFeeSelectionSheetState case FeeRateType.slow: if (ref.read(feeSheetSessionCacheProvider).slow[amount] != null) { - return ref.read(pAmountFormatter(coin)).format( + return ref + .read(pAmountFormatter(coin)) + .format( ref.read(feeSheetSessionCacheProvider).slow[amount]!, indicatePrecisionLoss: false, withUnitName: false, @@ -831,7 +821,7 @@ class _TransactionFeeSelectionSheetState return null; } } catch (e, s) { - Logging.instance.w("$e $s", error: e, stackTrace: s,); + Logging.instance.w("$e $s", error: e, stackTrace: s); return null; } } diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 01ae07406..fdf78136e 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -33,6 +33,7 @@ import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../utilities/eth_commons.dart'; import '../../utilities/logger.dart'; import '../../utilities/prefs.dart'; import '../../utilities/text_styles.dart'; @@ -45,6 +46,7 @@ import '../../wallets/models/tx_data.dart'; import '../../widgets/animated_text.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/eth_fee_form.dart'; import '../../widgets/icon_widgets/addressbook_icon.dart'; import '../../widgets/icon_widgets/clipboard_icon.dart'; import '../../widgets/icon_widgets/eth_token_icon.dart'; @@ -67,7 +69,6 @@ class TokenSendView extends ConsumerStatefulWidget { required this.tokenContract, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), }); static const String routeName = "/tokenSendView"; @@ -77,7 +78,6 @@ class TokenSendView extends ConsumerStatefulWidget { final EthContract tokenContract; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; @override ConsumerState createState() => _TokenSendViewState(); @@ -88,7 +88,6 @@ class _TokenSendViewState extends ConsumerState { late final CryptoCurrency coin; late final EthContract tokenContract; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late TextEditingController sendToController; late TextEditingController cryptoAmountController; @@ -119,6 +118,10 @@ class _TokenSendViewState extends ConsumerState { late Future _calculateFeesFuture; String cachedFees = ""; + final isCustomFee = ValueNotifier(false); + + EthEIP1559Fee? ethFee; + void _onTokenSendViewPasteAddressFieldButtonPressed() async { final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); if (data?.text != null && data!.text!.isNotEmpty) { @@ -148,7 +151,7 @@ class _TokenSendViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), @@ -183,10 +186,12 @@ class _TokenSendViewState extends ConsumerState { // autofill amount field if (paymentData.amount != null) { - final Amount amount = Decimal.parse(paymentData.amount!).toAmount( - fractionDigits: tokenContract.decimals, - ); - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( + final Amount amount = Decimal.parse( + paymentData.amount!, + ).toAmount(fractionDigits: tokenContract.decimals); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format( amount, withUnitName: false, indicatePrecisionLoss: false, @@ -215,13 +220,26 @@ class _TokenSendViewState extends ConsumerState { // shouldShowLockscreenOnResumeStateProvider // .state) // .state = true; - // here we ignore the exception caused by not giving permission - // to use the camera to scan a qr code - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code in SendView: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code in SendView: ", + error: e, + stackTrace: s, + ); + } } } @@ -231,22 +249,24 @@ class _TokenSendViewState extends ConsumerState { locale: ref.read(localeServiceChangeNotifierProvider).locale, ); if (baseAmount != null) { - final _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice(tokenContract.address) - .item1; + final _price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + ?.value; - if (_price == Decimal.zero) { + if (_price == null || _price == Decimal.zero) { _amountToSend = Amount.zero; } else { - _amountToSend = baseAmount <= Amount.zero - ? Amount.zero - : Amount.fromDecimal( - (baseAmount.decimal / _price).toDecimal( - scaleOnInfinitePrecision: tokenContract.decimals, - ), - fractionDigits: tokenContract.decimals, - ); + _amountToSend = + baseAmount <= Amount.zero + ? Amount.zero + : Amount.fromDecimal( + (baseAmount.decimal / _price).toDecimal( + scaleOnInfinitePrecision: tokenContract.decimals, + ), + fractionDigits: tokenContract.decimals, + ); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -254,10 +274,9 @@ class _TokenSendViewState extends ConsumerState { _cachedAmountToSend = _amountToSend; _cryptoAmountChangeLock = true; - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - _amountToSend!, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(_amountToSend!, withUnitName: false); _cryptoAmountChangeLock = false; } else { _amountToSend = Amount.zero; @@ -275,10 +294,9 @@ class _TokenSendViewState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( - cryptoAmountController.text, - ethContract: tokenContract, - ); + final cryptoAmount = ref + .read(pAmountFormatter(coin)) + .tryParse(cryptoAmountController.text, ethContract: tokenContract); if (cryptoAmount != null) { _amountToSend = cryptoAmount; if (_cachedAmountToSend != null && @@ -287,16 +305,15 @@ class _TokenSendViewState extends ConsumerState { } _cachedAmountToSend = _amountToSend; - final price = ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice(tokenContract.address) - .item1; + final price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + ?.value; - if (price > Decimal.zero) { + if (price != null && price > Decimal.zero) { baseAmountController.text = (_amountToSend!.decimal * price) - .toAmount( - fractionDigits: 2, - ) + .toAmount(fractionDigits: 2) .fiatString( locale: ref.read(localeServiceChangeNotifierProvider).locale, ); @@ -310,7 +327,7 @@ class _TokenSendViewState extends ConsumerState { _cryptoAmountChangedFeeUpdateTimer?.cancel(); _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { - if (coin is! Epiccash && !_baseFocus.hasFocus) { + if (mounted && coin is! Epiccash && !_baseFocus.hasFocus) { setState(() { _calculateFeesFuture = calculateFees(); }); @@ -322,7 +339,7 @@ class _TokenSendViewState extends ConsumerState { void _baseAmountChanged() { _baseAmountChangedFeeUpdateTimer?.cancel(); _baseAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { - if (coin is! Epiccash && !_cryptoFocus.hasFocus) { + if (mounted && coin is! Epiccash && !_cryptoFocus.hasFocus) { setState(() { _calculateFeesFuture = calculateFees(); }); @@ -331,7 +348,7 @@ class _TokenSendViewState extends ConsumerState { } String? _updateInvalidAddressText(String address) { - if (_data != null && _data!.contactLabel == address) { + if (_data != null && _data.contactLabel == address) { return null; } if (address.isNotEmpty && @@ -359,9 +376,9 @@ class _TokenSendViewState extends ConsumerState { final wallet = ref.read(pCurrentTokenWallet)!; final feeObject = await wallet.fees; - late final int feeRate; + late final BigInt feeRate; - switch (ref.read(feeRateTypeStateProvider.state).state) { + switch (ref.read(feeRateTypeMobileStateProvider.state).state) { case FeeRateType.fast: feeRate = feeObject.fast; break; @@ -372,15 +389,13 @@ class _TokenSendViewState extends ConsumerState { feeRate = feeObject.slow; break; default: - feeRate = -1; + feeRate = BigInt.from(-1); } final Amount fee = await wallet.estimateFeeFor(Amount.zero, feeRate); - cachedFees = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFees = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFees; } @@ -388,9 +403,7 @@ class _TokenSendViewState extends ConsumerState { Future _previewTransaction() async { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); final wallet = ref.read(pWallets).getWallet(walletId); final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -471,11 +484,7 @@ class _TokenSendViewState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); TxData txData; Future txDataFuture; @@ -483,21 +492,21 @@ class _TokenSendViewState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( txData: TxData( recipients: [ - ( + TxRecipient( address: _address!, amount: amount, isChange: false, + addressType: + tokenWallet.cryptoCurrency.getAddressType(_address!)!, ), ], - feeRateType: ref.read(feeRateTypeStateProvider), + feeRateType: ref.read(feeRateTypeMobileStateProvider), note: noteController.text, + ethEIP1559Fee: ethFee, ), ); - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; @@ -509,13 +518,14 @@ class _TokenSendViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: txData, - walletId: walletId, - isTokenTx: true, - onSuccess: clearSendForm, - routeOnSuccessName: TokenView.routeName, - ), + builder: + (_) => ConfirmTransactionView( + txData: txData, + walletId: walletId, + isTokenTx: true, + onSuccess: clearSendForm, + routeOnSuccessName: TokenView.routeName, + ), settings: const RouteSettings( name: ConfirmTransactionView.routeName, ), @@ -545,9 +555,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: () { @@ -578,6 +589,9 @@ class _TokenSendViewState extends ConsumerState { @override void initState() { ref.refresh(feeSheetSessionCacheProvider); + isCustomFee.addListener(() { + if (!isCustomFee.value) ethFee = null; + }); _calculateFeesFuture = calculateFees(); _data = widget.autoFillData; @@ -585,7 +599,6 @@ class _TokenSendViewState extends ConsumerState { coin = widget.coin; tokenContract = widget.tokenContract; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); @@ -598,11 +611,11 @@ class _TokenSendViewState extends ConsumerState { baseAmountController.addListener(_baseAmountChanged); if (_data != null) { - if (_data!.amount != null) { - cryptoAmountController.text = _data!.amount!.toString(); + if (_data.amount != null) { + cryptoAmountController.text = _data.amount!.toString(); } - sendToController.text = _data!.contactLabel; - _address = _data!.address.trim(); + sendToController.text = _data.contactLabel; + _address = _data.address.trim(); _addressToggleFlag = true; } @@ -627,6 +640,7 @@ class _TokenSendViewState extends ConsumerState { _addressFocusNode.dispose(); _cryptoFocus.dispose(); _baseFocus.dispose(); + isCustomFee.dispose(); super.dispose(); } @@ -637,6 +651,15 @@ class _TokenSendViewState extends ConsumerState { localeServiceChangeNotifierProvider.select((value) => value.locale), ); + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getTokenPrice(tokenContract.address)?.value, + ), + ); + } + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -657,209 +680,184 @@ class _TokenSendViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - // subtract top and bottom padding set in parent - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + 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: [ - EthTokenIcon( - contractAddress: tokenContract.address, - ), - 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, - ), - Text( - "Available balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), - ), - ], - ), - const Spacer(), - GestureDetector( - onTap: () { - cryptoAmountController.text = ref - .watch(pAmountFormatter(coin)) - .format( - ref - .read( - pTokenBalance( - ( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + EthTokenIcon( + contractAddress: tokenContract.address, + ), + 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, + ), + Text( + "Available balance", + style: STextStyles.label( + context, + ).copyWith(fontSize: 10), + ), + ], + ), + const Spacer(), + GestureDetector( + onTap: () { + cryptoAmountController.text = ref + .watch(pAmountFormatter(coin)) + .format( + ref + .read( + pTokenBalance(( walletId: widget.walletId, contractAddress: tokenContract.address, - ), - ), - ) - .spendable, - ethContract: tokenContract, - withUnitName: false, - indicatePrecisionLoss: true, - ); - }, - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - Text( - ref - .watch(pAmountFormatter(coin)) - .format( - ref - .watch( - pTokenBalance( - ( + )), + ) + .spendable, + ethContract: tokenContract, + withUnitName: false, + indicatePrecisionLoss: true, + ); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + ref + .watch( + pTokenBalance(( walletId: widget.walletId, contractAddress: tokenContract .address, - ), - ), - ) - .spendable, - ethContract: tokenContract, - ), - style: - STextStyles.titleBold12(context) - .copyWith( - fontSize: 10, - ), - textAlign: TextAlign.right, - ), - Text( - "${(ref.watch( - pTokenBalance( - ( - walletId: - widget.walletId, - contractAddress: - tokenContract - .address, - ), - ), - ).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", - style: STextStyles.subtitle(context) - .copyWith( - fontSize: 8, + )), + ) + .spendable, + ethContract: tokenContract, + ), + style: STextStyles.titleBold12( + context, + ).copyWith(fontSize: 10), + textAlign: TextAlign.right, ), - textAlign: TextAlign.right, - ), - ], + if (price != null) + Text( + "${(ref.watch(pTokenBalance((walletId: widget.walletId, contractAddress: tokenContract.address))).spendable.decimal * price).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, - ), - Text( - "Send to", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 16), + Text( + "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - child: TextField( - key: const Key("tokenSendViewAddressFieldKey"), - controller: sendToController, - readOnly: false, - autocorrect: false, - enableSuggestions: false, - toolbarOptions: const ToolbarOptions( - copy: false, - cut: false, - paste: true, - selectAll: false, + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onChanged: (newValue) { - _address = newValue.trim(); - _updatePreviewButtonState( - _address, - _amountToSend, - ); - - setState(() { - _addressToggleFlag = newValue.isNotEmpty; - }); - }, - focusNode: _addressFocusNode, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Enter ${tokenContract.symbol} address", - _addressFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, + child: TextField( + key: const Key("tokenSendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, ), - suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - _addressToggleFlag - ? TextFieldIconButton( + onChanged: (newValue) { + _address = newValue.trim(); + _updatePreviewButtonState( + _address, + _amountToSend, + ); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${tokenContract.symbol} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: + sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( key: const Key( "tokenSendViewClearAddressFieldButtonKey", ), @@ -876,455 +874,486 @@ class _TokenSendViewState extends ConsumerState { }, child: const XIcon(), ) - : TextFieldIconButton( + : TextFieldIconButton( key: const Key( "tokenSendViewPasteAddressFieldButtonKey", ), onTap: _onTokenSendViewPasteAddressFieldButtonPressed, - child: sendToController - .text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + child: + sendToController + .text + .isEmpty + ? const ClipboardIcon() + : const XIcon(), ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewAddressBookButtonKey", + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey", + ), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: widget.coin, + ); + }, + child: const AddressBookIcon(), ), - onTap: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: widget.coin, - ); - }, - child: const AddressBookIcon(), - ), - if (sendToController.text.isEmpty) - TextFieldIconButton( - key: const Key( - "sendViewScanQrButtonKey", + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: + _onTokenSendViewScanQrButtonPressed, + child: const QrCodeIcon(), ), - onTap: - _onTokenSendViewScanQrButtonPressed, - child: const QrCodeIcon(), - ), - ], + ], + ), ), ), ), ), ), - ), - Builder( - builder: (_) { - final error = _updateInvalidAddressText( - _address ?? "", - ); - - if (error == null || error.isEmpty) { - return Container(); - } else { - return Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - top: 4.0, - ), - child: Text( - error, - textAlign: TextAlign.left, - style: - STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .textError, + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ); + + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: STextStyles.label( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textError, + ), ), ), - ), - ); - } - }, - ), - const SizedBox( - height: 12, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - // CustomTextButton( - // text: "Send all ${tokenContract.symbol}", - // onTap: () async { - // cryptoAmountController.text = ref - // .read(tokenServiceProvider)! - // .balance - // .getSpendable() - // .toStringAsFixed(tokenContract.decimals); - // - // _cryptoAmountChanged(); - // }, - // ), - ], - ), - const SizedBox( - height: 8, - ), - 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("amountInputFieldCryptoTextFieldKey"), - controller: cryptoAmountController, - focusNode: _cryptoFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), - textAlign: TextAlign.right, - inputFormatters: [ - AmountInputFormatter( - decimals: tokenContract.decimals, - unit: ref.watch(pAmountUnit(coin)), - locale: locale, - ), - // // regex to validate a crypto amount with 8 decimal places - // TextInputFormatter.withFunction((oldValue, - // newValue) => - // RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - // .hasMatch(newValue.text) - // ? newValue - // : oldValue), - ], - 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(coin)) - .unitForContract(tokenContract), - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), - ), - ), - ), - if (Prefs.instance.externalCalls) - const SizedBox( - height: 8, + // CustomTextButton( + // text: "Send all ${tokenContract.symbol}", + // onTap: () async { + // cryptoAmountController.text = ref + // .read(tokenServiceProvider)! + // .balance + // .getSpendable() + // .toStringAsFixed(tokenContract.decimals); + // + // _cryptoAmountChanged(); + // }, + // ), + ], ), - if (Prefs.instance.externalCalls) + const SizedBox(height: 8), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), - key: - const Key("amountInputFieldFiatTextFieldKey"), - controller: baseAmountController, - focusNode: _baseFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + key: const Key( + "amountInputFieldCryptoTextFieldKey", + ), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( - decimals: 2, + decimals: tokenContract.decimals, + unit: ref.watch(pAmountUnit(coin)), locale: locale, ), - // // regex to validate a fiat amount with 2 decimal places + // // regex to validate a crypto amount with 8 decimal places // TextInputFormatter.withFunction((oldValue, // newValue) => - // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + // RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') // .hasMatch(newValue.text) // ? newValue // : oldValue), ], - onChanged: _onFiatAmountFieldChanged, decoration: InputDecoration( contentPadding: const EdgeInsets.only( top: 12, right: 12, ), hintText: "0", - hintStyle: - STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), + 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, + ref + .watch(pAmountUnit(coin)) + .unitForContract(tokenContract), + style: STextStyles.smallMed14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), ), ), ), - 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 (Prefs.instance.externalCalls) + const SizedBox(height: 8), + if (Prefs.instance.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: baseAmountController, + focusNode: _baseFocus, + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + AmountInputFormatter( + decimals: 2, + locale: locale, + ), + // // regex to validate a fiat amount with 2 decimal places + // TextInputFormatter.withFunction((oldValue, + // newValue) => + // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + // .hasMatch(newValue.text) + // ? newValue + // : oldValue), + ], + onChanged: _onFiatAmountFieldChanged, + 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, ), ), - ) - : null, + style: STextStyles.smallMed14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + ), + ), ), - ), - ), - const SizedBox( - height: 12, - ), - if (coin is! Epiccash) + const SizedBox(height: 12), Text( - "Transaction fee (estimated)", + "Note (optional)", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 8, - ), - Stack( - children: [ - TextField( + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - controller: feeController, - readOnly: true, - textInputAction: TextInputAction.none, + 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, + ), ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, + ), + const SizedBox(height: 12), + Text( + "Transaction fee ${isCustomFee.value ? "" : "(max)"}", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox(height: 8), + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: feeController, + readOnly: true, + textInputAction: TextInputAction.none, ), - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, ), - onPressed: () { - showModalBottomSheet( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - TransactionFeeSelectionSheet( - walletId: walletId, - isToken: true, - amount: (Decimal.tryParse( - cryptoAmountController.text, - ) ?? - Decimal.zero) - .toAmount( - fractionDigits: - tokenContract.decimals, - ), - updateChosen: (String fee) { - setState(() { - _calculateFeesFuture = - Future(() => fee); - }); - }, + child: RawMaterialButton( + splashColor: + Theme.of( + context, + ).extension()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - ref - .watch( - feeRateTypeStateProvider - .state, - ) - .state - .prettyName, - style: STextStyles.itemSubtitle12( - context, - ), - ), - const SizedBox( - width: 10, + ), + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), ), - FutureBuilder( - future: _calculateFeesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "~${snapshot.data!}", - style: - STextStyles.itemSubtitle( - context, - ), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: - STextStyles.itemSubtitle( - context, + ), + builder: + (_) => TransactionFeeSelectionSheet( + walletId: walletId, + isToken: true, + amount: (Decimal.tryParse( + cryptoAmountController + .text, + ) ?? + Decimal.zero) + .toAmount( + fractionDigits: + tokenContract.decimals, ), - ); - } - }, + updateChosen: (String fee) { + if (fee == "custom") { + if (!isCustomFee.value) { + setState(() { + isCustomFee.value = true; + }); + } + return; + } + + setState(() { + _calculateFeesFuture = Future( + () => fee, + ); + if (isCustomFee.value) { + isCustomFee.value = false; + } + }); + }, + ), + ); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state + .prettyName, + style: STextStyles.itemSubtitle12( + context, + ), + ), + const SizedBox(width: 10), + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + isCustomFee.value + ? "" + : "~${snapshot.data!}", + style: + STextStyles.itemSubtitle( + context, + ), + ); + } else { + return AnimatedText( + stringsToLoopThrough: + const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: + STextStyles.itemSubtitle( + context, + ), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + colorFilter: ColorFilter.mode( + Theme.of(context) + .extension()! + .textSubtitle2, + BlendMode.srcIn, ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - ], + ), + ], + ), ), ), + ], + ), + if (isCustomFee.value) const SizedBox(height: 12), + if (isCustomFee.value) + EthFeeForm( + minGasLimit: kEthereumTokenMinGasLimit, + stateChanged: (value) => ethFee = value, + ), + const Spacer(), + const SizedBox(height: 12), + TextButton( + onPressed: + ref + .watch( + previewTokenTxButtonStateProvider + .state, + ) + .state + ? _previewTransaction + : null, + style: + ref + .watch( + previewTokenTxButtonStateProvider + .state, + ) + .state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle( + context, + ), + child: Text( + "Preview", + style: STextStyles.button(context), ), - ], - ), - const Spacer(), - const SizedBox( - height: 12, - ), - TextButton( - onPressed: ref - .watch( - previewTokenTxButtonStateProvider.state, - ) - .state - ? _previewTransaction - : null, - style: ref - .watch( - previewTokenTxButtonStateProvider.state, - ) - .state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Preview", - style: STextStyles.button(context), ), - ), - const SizedBox( - height: 4, - ), - ], + const SizedBox(height: 16), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/about_view.dart b/lib/pages/settings_views/global_settings_view/about_view.dart index b72c3222c..06c3774a4 100644 --- a/lib/pages/settings_views/global_settings_view/about_view.dart +++ b/lib/pages/settings_views/global_settings_view/about_view.dart @@ -40,324 +40,198 @@ class AboutView extends ConsumerWidget { Navigator.of(context).pop(); }, ), - title: Text( - "About", - style: STextStyles.navBarTitle(context), - ), + title: Text("About", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: - (context, AsyncSnapshot snapshot) { - String version = ""; - String signature = ""; - String appName = ""; - String build = ""; - - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - version = snapshot.data!.version; - build = snapshot.data!.buildNumber; - signature = snapshot.data!.buildSignature; - appName = snapshot.data!.appName; - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - appName, - style: STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 24, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Version", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - version, - style: - STextStyles.itemSubtitle(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Build number", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - build, - style: - STextStyles.itemSubtitle(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Build commit", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - GitStatus.appCommitHash, - style: - STextStyles.itemSubtitle(context), - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Build signature", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - signature, - style: - STextStyles.itemSubtitle(context), - ), - ], - ), - ), - ], - ); - }, - ), - if (AppConfig.coins.whereType().isNotEmpty) - const SizedBox( - height: 12, - ), - if (AppConfig.coins.whereType().isNotEmpty) + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ FutureBuilder( - future: GitStatus.getFiroCommitStatus(), + future: PackageInfo.fromPlatform(), builder: ( context, - AsyncSnapshot snapshot, + AsyncSnapshot snapshot, ) { - CommitStatus stateOfCommit = - CommitStatus.notLoaded; + String version = ""; + String signature = ""; + String appName = ""; + String build = ""; if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { - stateOfCommit = snapshot.data!; + version = snapshot.data!.version; + build = snapshot.data!.buildNumber; + signature = snapshot.data!.buildSignature; + appName = snapshot.data!.appName; } - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Firo Build Commit", - style: STextStyles.titleBold12(context), + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + appName, + style: STextStyles.pageTitleH2(context), ), - const SizedBox( - height: 4, + ), + const SizedBox(height: 24), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Version", + style: STextStyles.titleBold12( + context, + ), + ), + const SizedBox(height: 4), + SelectableText( + version, + style: STextStyles.itemSubtitle( + context, + ), + ), + ], ), - SelectableText( - GitStatus.firoCommit, - style: GitStatus.styleForStatus( - stateOfCommit, - context, - ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Build number", + style: STextStyles.titleBold12( + context, + ), + ), + const SizedBox(height: 4), + SelectableText( + build, + style: STextStyles.itemSubtitle( + context, + ), + ), + ], ), - ], - ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Build commit", + style: STextStyles.titleBold12( + context, + ), + ), + const SizedBox(height: 4), + SelectableText( + GitStatus.appCommitHash, + style: STextStyles.itemSubtitle( + context, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Build signature", + style: STextStyles.titleBold12( + context, + ), + ), + const SizedBox(height: 4), + SelectableText( + signature, + style: STextStyles.itemSubtitle( + context, + ), + ), + ], + ), + ), + ], ); }, ), - if (AppConfig.coins.whereType().isNotEmpty) - const SizedBox( - height: 12, - ), - if (AppConfig.coins.whereType().isNotEmpty) - FutureBuilder( - future: GitStatus.getEpicCommitStatus(), - builder: ( - context, - AsyncSnapshot snapshot, - ) { - CommitStatus stateOfCommit = - CommitStatus.notLoaded; + if (AppConfig.coins.whereType().isNotEmpty) + const SizedBox(height: 12), + if (AppConfig.coins.whereType().isNotEmpty) + FutureBuilder( + future: GitStatus.getEpicCommitStatus(), + builder: ( + context, + AsyncSnapshot snapshot, + ) { + CommitStatus stateOfCommit = + CommitStatus.notLoaded; - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - stateOfCommit = snapshot.data!; - } + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + stateOfCommit = snapshot.data!; + } - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Text( - "Epic Cash Build Commit", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - SelectableText( - GitStatus.epicCashCommit, - style: GitStatus.styleForStatus( - stateOfCommit, - context, + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Text( + "Epic Cash Build Commit", + style: STextStyles.titleBold12(context), ), - ), - ], - ), - ); - }, - ), - if (AppConfig.coins.whereType().isNotEmpty) - const SizedBox( - height: 12, - ), - // if (AppConfig.coins.whereType().isNotEmpty) - // FutureBuilder( - // future: GitStatus.getMoneroCommitStatus(), - // builder: ( - // context, - // AsyncSnapshot snapshot, - // ) { - // CommitStatus stateOfCommit = - // CommitStatus.notLoaded; - // - // if (snapshot.connectionState == - // ConnectionState.done && - // snapshot.hasData) { - // stateOfCommit = snapshot.data!; - // } - // return RoundedWhiteContainer( - // child: Column( - // crossAxisAlignment: - // CrossAxisAlignment.stretch, - // children: [ - // Text( - // "Monero Build Commit", - // style: STextStyles.titleBold12(context), - // ), - // const SizedBox( - // height: 4, - // ), - // SelectableText( - // GitStatus.moneroCommit, - // style: GitStatus.styleForStatus( - // stateOfCommit, - // context, - // ), - // ), - // ], - // ), - // ); - // }, - // ), - // const SizedBox( - // height: 12, - // ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Website", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 4, - ), - CustomTextButton( - text: "https://stackwallet.com", - onTap: () { - launchUrl( - Uri.parse("https://stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], - ), - ), - if (AppConfig.coins.whereType().isNotEmpty) - const SizedBox( - height: 12, - ), - if (AppConfig.coins.whereType().isNotEmpty) + const SizedBox(height: 4), + SelectableText( + GitStatus.epicCashCommit, + style: GitStatus.styleForStatus( + stateOfCommit, + context, + ), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Tezos functionality", + "Website", style: STextStyles.titleBold12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), CustomTextButton( - text: "Powered by TzKT API", + text: "https://stackwallet.com", onTap: () { launchUrl( - Uri.parse("https://tzkt.io"), + Uri.parse("https://stackwallet.com"), mode: LaunchMode.externalApplication, ); }, @@ -365,55 +239,82 @@ class AboutView extends ConsumerWidget { ], ), ), - const SizedBox( - height: 12, - ), - const Spacer(), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: STextStyles.label(context), - children: [ - const TextSpan( - text: - "By using ${AppConfig.appName}, you agree to the ", - ), - TextSpan( - text: "Terms of service", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/terms-of-service.html", - ), - mode: LaunchMode.externalApplication, - ); - }, - ), - const TextSpan(text: " and "), - TextSpan( - text: "Privacy policy", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/privacy-policy.html", - ), - mode: LaunchMode.externalApplication, - ); - }, + if (AppConfig.coins.whereType().isNotEmpty) + const SizedBox(height: 12), + if (AppConfig.coins.whereType().isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Tezos functionality", + style: STextStyles.titleBold12(context), + ), + const SizedBox(height: 4), + CustomTextButton( + text: "Powered by TzKT API", + onTap: () { + launchUrl( + Uri.parse("https://tzkt.io"), + mode: LaunchMode.externalApplication, + ); + }, + ), + ], ), - ], + ), + const SizedBox(height: 12), + const Spacer(), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: STextStyles.label(context), + children: [ + const TextSpan( + text: + "By using ${AppConfig.appName}, you agree to the ", + ), + TextSpan( + text: "Terms of service", + style: STextStyles.richLink(context), + recognizer: + TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html", + ), + mode: + LaunchMode.externalApplication, + ); + }, + ), + const TextSpan(text: " and "), + TextSpan( + text: "Privacy policy", + style: STextStyles.richLink(context), + recognizer: + TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html", + ), + mode: + LaunchMode.externalApplication, + ); + }, + ), + ], + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart index d86f17526..20f880dd7 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart @@ -28,9 +28,7 @@ import 'manage_coin_units/manage_coin_units_view.dart'; import 'manage_explorer_view.dart'; class AdvancedSettingsView extends StatelessWidget { - const AdvancedSettingsView({ - super.key, - }); + const AdvancedSettingsView({super.key}); static const String routeName = "/advancedSettings"; @@ -47,149 +45,93 @@ class AdvancedSettingsView extends StatelessWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Advanced", - style: STextStyles.navBarTitle(context), - ), + title: Text("Advanced", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context) - .pushNamed(LoggingSettingsView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - child: Row( - children: [ - Text( - "Logging", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of( + context, + ).pushNamed(LoggingSettingsView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Logging", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Toggle testnet coins", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.showTestNetCoins, - ), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .showTestNetCoins = newValue; - }, + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Toggle testnet coins", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - ), - ], - ), - ), - ); - }, - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Enable coin control", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.enableCoinControl, + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.showTestNetCoins, + ), ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .showTestNetCoins = newValue; + }, ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .enableCoinControl = newValue; - }, ), - ), - ], + ], + ), ), - ), - ); - }, - ), - ), - // showExchange pref. - if (Constants.enableExchange) - const SizedBox( - height: 8, + ); + }, + ), ), - if (Constants.enableExchange) + const SizedBox(height: 8), RoundedWhiteContainer( child: Consumer( builder: (_, ref, __) { @@ -208,7 +150,7 @@ class AdvancedSettingsView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Enable exchange features", + "Enable coin control", style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), @@ -218,13 +160,13 @@ class AdvancedSettingsView extends StatelessWidget { child: DraggableSwitchButton( isOn: ref.watch( prefsChangeNotifierProvider.select( - (value) => value.enableExchange, + (value) => value.enableCoinControl, ), ), onValueChanged: (newValue) { ref .read(prefsChangeNotifierProvider) - .enableExchange = newValue; + .enableCoinControl = newValue; }, ), ), @@ -235,138 +177,185 @@ class AdvancedSettingsView extends StatelessWidget { }, ), ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Consumer( - builder: (_, ref, __) { - final externalCalls = ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - ); - return RawMaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context).pushNamed( - StackPrivacyCalls.routeName, - arguments: true, + // showExchange pref. + if (Constants.enableExchange) const SizedBox(height: 8), + if (Constants.enableExchange) + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable exchange features", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableExchange, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .enableExchange = newValue; + }, + ), + ), + ], + ), + ), ); }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, + ), + ), + const SizedBox(height: 8), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Consumer( + builder: (_, ref, __) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, ), - child: Row( - children: [ - RichText( - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: "${AppConfig.prefix} Experience", - style: STextStyles.titleBold12(context), - ), - TextSpan( - text: externalCalls - ? "\nEasy crypto" - : "\nIncognito", - style: STextStyles.label(context) - .copyWith(fontSize: 15.0), - ), - ], + ); + return RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + Navigator.of(context).pushNamed( + StackPrivacyCalls.routeName, + arguments: true, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + RichText( + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: "${AppConfig.prefix} Experience", + style: STextStyles.titleBold12(context), + ), + TextSpan( + text: + externalCalls + ? "\nEasy crypto" + : "\nIncognito", + style: STextStyles.label( + context, + ).copyWith(fontSize: 15.0), + ), + ], + ), ), - ), - ], + ], + ), ), - ), - ); - }, - ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + ); + }, ), - onPressed: () { - Navigator.of(context).pushNamed( - ChooseCoinView.routeName, - arguments: const Tuple3( - "Manage block explorers", - "block explorer", - ManageExplorerView.routeName, + ), + const SizedBox(height: 8), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, ), - child: Row( - children: [ - Text( - "Change block explorer", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, + onPressed: () { + Navigator.of(context).pushNamed( + ChooseCoinView.routeName, + arguments: const Tuple3( + "Manage block explorers", + "block explorer", + ManageExplorerView.routeName, ), - ], + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Change block explorer", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context).pushNamed( - ManageCoinUnitsView.routeName, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, + const SizedBox(height: 8), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - child: Row( - children: [ - Text( - "Units", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of( + context, + ).pushNamed(ManageCoinUnitsView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Units", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/logging_settings_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/logging_settings_view.dart index dc6c24013..eb4eb5536 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/logging_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/logging_settings_view.dart @@ -11,21 +11,28 @@ import 'dart:async'; import 'dart:io'; +import 'package:archive/archive_io.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; // import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; import '../../../../app_config.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/logger.dart'; +import '../../../../utilities/show_loading.dart'; +import '../../../../utilities/stack_file_system.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/log_level_preference_widget.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; @@ -45,15 +52,14 @@ class _LoggingSettingsViewState extends ConsumerState { bool _lock = false; Future _edit() async { - final currentPath = ref.read(prefsChangeNotifierProvider).logsPath ?? + final currentPath = + ref.read(prefsChangeNotifierProvider).logsPath ?? Logging.instance.logsDirPath; final newPath = await _pickDir(context, currentPath); // test if has permission to write if (newPath != null) { - final file = File( - "$newPath${Platform.pathSeparator}._test", - ); + final file = File("$newPath${Platform.pathSeparator}._test"); if (!file.existsSync()) { file.createSync(); file.deleteSync(); @@ -67,7 +73,7 @@ class _LoggingSettingsViewState extends ConsumerState { setState(() { fileLocationController.text = ref.read(prefsChangeNotifierProvider).logsPath ?? - Logging.instance.logsDirPath; + Logging.instance.logsDirPath; }); } } @@ -88,13 +94,82 @@ class _LoggingSettingsViewState extends ConsumerState { return chosenPath; } + Future _exportHelper() async { + final logsDir = await StackFileSystem.applicationLogsDirectory( + ref.read(prefsChangeNotifierProvider), + ); + + final files = logsDir + .listSync(recursive: false) + .whereType() + .where((f) => f.path.endsWith('.txt')); + + if (files.isEmpty) { + throw Exception("No logs found in ${logsDir.path}"); + } + + final archive = Archive(); + + for (final file in files) { + final bytes = await file.readAsBytes(); + final fileName = path.basename(file.path); + archive.addFile(ArchiveFile(fileName, bytes.length, bytes)); + } + + if (archive.isEmpty) { + throw Exception("Failed to add log files to archive"); + } + + // Write zip to a temp location + final tempDir = await getTemporaryDirectory(); + final zipPath = path.join(tempDir.path, 'logs.zip'); + final zipFile = File(zipPath); + await zipFile.writeAsBytes(ZipEncoder().encode(archive)!); + + await Share.shareXFiles([ + XFile(zipFile.path), + ], text: "${AppConfig.appName} logs"); + } + + bool _exportLock = false; + Future _androidExportLogs() async { + if (_exportLock) { + return; + } + _exportLock = true; + try { + await showLoading( + whileFuture: _exportHelper(), + context: context, + message: "Exporting logs...", + onException: (e) => throw e, + ); + } catch (e, s) { + Logging.instance.e("Failed to export logs", error: e, stackTrace: s); + if (mounted) { + unawaited( + showDialog( + context: context, + builder: + (context) => StackOkDialog( + title: "Failed to export logs", + message: e.toString(), + ), + ), + ); + } + } finally { + _exportLock = false; + } + } + @override void initState() { super.initState(); fileLocationController = TextEditingController(); fileLocationController.text = ref.read(prefsChangeNotifierProvider).logsPath ?? - Logging.instance.logsDirPath; + Logging.instance.logsDirPath; } @override @@ -114,18 +189,11 @@ class _LoggingSettingsViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Logging", - style: STextStyles.navBarTitle(context), - ), + title: Text("Logging", style: STextStyles.navBarTitle(context)), ), body: SafeArea( child: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), child: Column( children: [ Row( @@ -137,9 +205,7 @@ class _LoggingSettingsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), TextField( autocorrect: false, enableSuggestions: false, @@ -151,27 +217,22 @@ class _LoggingSettingsViewState extends ConsumerState { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), ), - key: const Key( - "logsDirPathLocationControllerKey", - ), + key: const Key("logsDirPathLocationControllerKey"), readOnly: true, toolbarOptions: const ToolbarOptions( copy: true, @@ -181,13 +242,9 @@ class _LoggingSettingsViewState extends ConsumerState { ), onChanged: (newValue) {}, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), const LogLevelPreferenceWidget(), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( @@ -201,10 +258,16 @@ class _LoggingSettingsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), const Spacer(), + + if (Platform.isAndroid) + SecondaryButton( + label: "Export logs", + onPressed: _androidExportLogs, + ), + if (Platform.isAndroid) const SizedBox(height: 16), + PrimaryButton( label: "Select log save location", onPressed: () async { @@ -222,9 +285,9 @@ class _LoggingSettingsViewState extends ConsumerState { ); if (context.mounted) { final String err; - if (e - .toString() - .contains("OS Error: Operation not permitted")) { + if (e.toString().contains( + "OS Error: Operation not permitted", + )) { err = "Cannot use chosen location"; } else { err = e.toString(); @@ -233,10 +296,11 @@ class _LoggingSettingsViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => StackOkDialog( - title: "Failed to change logs path", - message: err, - ), + builder: + (context) => StackOkDialog( + title: "Failed to change logs path", + message: err, + ), ), ); } @@ -245,9 +309,7 @@ class _LoggingSettingsViewState extends ConsumerState { } }, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/manage_coin_units/edit_coin_units_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/manage_coin_units/edit_coin_units_view.dart index 08d801cc4..661939aef 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/manage_coin_units/edit_coin_units_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/manage_coin_units/edit_coin_units_view.dart @@ -154,9 +154,11 @@ class _EditCoinUnitsViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/manage_coin_units/manage_coin_units_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/manage_coin_units/manage_coin_units_view.dart index 28ebe18a9..af5838eff 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/manage_coin_units/manage_coin_units_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/manage_coin_units/manage_coin_units_view.dart @@ -99,7 +99,7 @@ class ManageCoinUnitsView extends ConsumerWidget { style: STextStyles.navBarTitle(context), ), ), - body: child, + body: SafeArea(child: child), ), ), child: ListView.separated( diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart index 1260aa70d..486da8184 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart @@ -21,10 +21,7 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/rounded_white_container.dart'; class ManageExplorerView extends ConsumerStatefulWidget { - const ManageExplorerView({ - super.key, - required this.coin, - }); + const ManageExplorerView({super.key, required this.coin}); static const String routeName = "/manageExplorer"; @@ -41,9 +38,10 @@ class _ManageExplorerViewState extends ConsumerState { void initState() { super.initState(); textEditingController = TextEditingController( - text: getBlockExplorerTransactionUrlFor(coin: widget.coin, txid: "[TXID]") - .toString() - .replaceAll("%5BTXID%5D", "[TXID]"), + text: getBlockExplorerTransactionUrlFor( + coin: widget.coin, + txid: "[TXID]", + ).toString().replaceAll("%5BTXID%5D", "[TXID]"), ); } @@ -69,71 +67,66 @@ class _ManageExplorerViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Expanded( - child: Column( - children: [ - TextField( - controller: textEditingController, - decoration: const InputDecoration( - border: OutlineInputBorder(), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Expanded( + child: Column( + children: [ + TextField( + controller: textEditingController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Center( - child: Text( - "Edit your block explorer above. Keep in mind that " - "every block explorer has a slightly different URL " - "scheme.\n\nPaste in your block explorer of choice," - " then edit in [TXID] where the transaction ID " - "should go, and ${AppConfig.appName} will auto fill the " - "transaction ID in that place of URL.", - style: STextStyles.itemSubtitle(context), + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Center( + child: Text( + "Edit your block explorer above. Keep in mind that " + "every block explorer has a slightly different URL " + "scheme.\n\nPaste in your block explorer of choice," + " then edit in [TXID] where the transaction ID " + "should go, and ${AppConfig.appName} will auto fill the " + "transaction ID in that place of URL.", + style: STextStyles.itemSubtitle(context), + ), ), ), - ), - ], - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 480, - minHeight: 70, + ], ), - child: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () async { - textEditingController.text = - textEditingController.text.trim(); - await setBlockExplorerForCoin( - coin: widget.coin, - url: Uri.parse( - textEditingController.text, - ), - ); + ), + Align( + alignment: Alignment.bottomCenter, + child: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 480, + minHeight: 70, + ), + child: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () async { + textEditingController.text = + textEditingController.text.trim(); + await setBlockExplorerForCoin( + coin: widget.coin, + url: Uri.parse(textEditingController.text), + ); - if (mounted) { - Navigator.of(context).pop(); - } - }, - child: Text( - "Save", - style: STextStyles.button(context), + if (mounted) { + Navigator.of(context).pop(); + } + }, + child: Text("Save", style: STextStyles.button(context)), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/appearance_settings_view.dart index fe17f8ffc..aa10724a7 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/appearance_settings_view.dart @@ -39,125 +39,124 @@ class AppearanceSettingsView extends ConsumerWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Appearance", - style: STextStyles.navBarTitle(context), - ), + title: Text("Appearance", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - splashColor: Theme.of(context) - .extension()! - .highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + splashColor: + Theme.of( + context, + ).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Display favorite wallets", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.showFavoriteWallets, + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Display favorite wallets", + style: STextStyles.titleBold12( + context, + ), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.showFavoriteWallets, + ), ), + onValueChanged: (newValue) { + ref + .read( + prefsChangeNotifierProvider, + ) + .showFavoriteWallets = newValue; + }, ), - onValueChanged: (newValue) { - ref - .read( - prefsChangeNotifierProvider, - ) - .showFavoriteWallets = newValue; - }, ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - const SizedBox( - height: 10, - ), - RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Choose Theme", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 12, - ), - const Padding( - padding: EdgeInsets.all(4), - child: ThemeOptionsWidget(), - ), - ], - ), - ], - ), - const SizedBox( - height: 12, - ), - SecondaryButton( - label: "Add more themes", - onPressed: () { - Navigator.of(context).pushNamed( - ManageThemesView.routeName, - ); - }, - ), - ], + const SizedBox(height: 10), + RoundedWhiteContainer( + child: Column( + children: [ + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Choose Theme", + style: STextStyles.titleBold12( + context, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 12), + const Padding( + padding: EdgeInsets.all(4), + child: ThemeOptionsWidget(), + ), + ], + ), + ], + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Add more themes", + onPressed: () { + Navigator.of( + context, + ).pushNamed(ManageThemesView.routeName); + }, + ), + ], + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart index 4744d857f..40f654603 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart @@ -13,9 +13,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:tuple/tuple.dart'; + import '../../../../models/isar/stack_theme.dart'; -import 'sub_widgets/install_theme_from_file_dialog.dart'; -import 'sub_widgets/stack_theme_card.dart'; import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../themes/stack_colors.dart'; @@ -30,7 +30,8 @@ import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/loading_indicator.dart'; import '../../../../widgets/rounded_white_container.dart'; -import 'package:tuple/tuple.dart'; +import 'sub_widgets/install_theme_from_file_dialog.dart'; +import 'sub_widgets/stack_theme_card.dart'; class ManageThemesView extends ConsumerStatefulWidget { const ManageThemesView({super.key}); @@ -66,117 +67,117 @@ class _ManageThemesViewState extends ConsumerState { Widget build(BuildContext context) { return 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( - "Add more themes", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 2), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - icon: SvgPicture.asset( - Assets.svg.circlePlusFilled, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - height: 20, - width: 20, - ), - onPressed: _onInstallPressed, - ), + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - ), - ], - ), - body: _showThemes - ? Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: IntrinsicHeight( - child: child, - ), + title: Text( + "Add more themes", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 2), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, + height: 20, + width: 20, ), - ), - ), - Padding( - padding: const EdgeInsets.all(16), - child: SecondaryButton( - label: "Install theme file", onPressed: _onInstallPressed, ), ), - ], - ) - : SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RoundedWhiteContainer( - child: Text( - "You are using Incognito Mode. Please press the" - " button below to load available themes from our server" - " or install a theme file manually from your device.", - style: STextStyles.smallMed12(context), + ), + ], + ), + body: SafeArea( + child: + _showThemes + ? Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: IntrinsicHeight(child: child), + ), + ), ), - ), - const SizedBox( - height: 12, - ), - PrimaryButton( - label: "Load themes", - onPressed: () { - setState(() { - _showThemes = true; - future = ref.watch(pThemeService).fetchThemes; - }); - }, - ), - const SizedBox( - height: 12, - ), - SecondaryButton( - label: "Install theme file", - onPressed: _onInstallPressed, - ), - const SizedBox( - height: 16, - ), - Expanded( - child: IncognitoInstalledThemes( - cardWidth: - (MediaQuery.of(context).size.width - 48) / 2, + Padding( + padding: const EdgeInsets.all(16), + child: SecondaryButton( + label: "Install theme file", + onPressed: _onInstallPressed, + ), + ), + ], + ) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RoundedWhiteContainer( + child: Text( + "You are using Incognito Mode. Please press the" + " button below to load available themes from our server" + " or install a theme file manually from your device.", + style: STextStyles.smallMed12(context), + ), + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Load themes", + onPressed: () { + setState(() { + _showThemes = true; + future = + ref + .watch(pThemeService) + .fetchThemes; + }); + }, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Install theme file", + onPressed: _onInstallPressed, + ), + const SizedBox(height: 16), + Expanded( + child: IncognitoInstalledThemes( + cardWidth: + (MediaQuery.of(context).size.width - + 48) / + 2, + ), + ), + const SizedBox(height: 16), + ], + ), ), ), - const SizedBox( - height: 16, - ), - ], - ), - ), - ), - ), - ), - ), + ), + ), + ), + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -191,17 +192,17 @@ class _ManageThemesViewState extends ConsumerState { return Wrap( spacing: 16, runSpacing: 16, - children: snapshot.data! - .map( - (e) => SizedBox( - key: Key("ManageThemesView_card_${e.id}_key"), - width: (MediaQuery.of(context).size.width - 48) / 2, - child: StackThemeCard( - data: e, - ), - ), - ) - .toList(), + children: + snapshot.data! + .map( + (e) => SizedBox( + key: Key("ManageThemesView_card_${e.id}_key"), + width: + (MediaQuery.of(context).size.width - 48) / 2, + child: StackThemeCard(data: e), + ), + ) + .toList(), ); } else { return Center( @@ -219,10 +220,7 @@ class _ManageThemesViewState extends ConsumerState { } class IncognitoInstalledThemes extends ConsumerStatefulWidget { - const IncognitoInstalledThemes({ - super.key, - required this.cardWidth, - }); + const IncognitoInstalledThemes({super.key, required this.cardWidth}); final double cardWidth; @@ -238,28 +236,33 @@ class _IncognitoInstalledThemesState List> installedThemeIdNames = []; void _updateInstalledList() { - installedThemeIdNames = ref - .read(pThemeService) - .installedThemes - .where((e) => e.themeId != "light" && e.themeId != "dark") - .map((e) => Tuple3(e.themeId, e.name, e.version)) - .toList(); + installedThemeIdNames = + ref + .read(pThemeService) + .installedThemes + .where((e) => e.themeId != "light" && e.themeId != "dark") + .map((e) => Tuple3(e.themeId, e.name, e.version)) + .toList(); } @override void initState() { _updateInstalledList(); - _subscription = - ref.read(mainDBProvider).isar.stackThemes.watchLazy().listen((_) { - if (mounted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - _updateInstalledList(); - }); + _subscription = ref + .read(mainDBProvider) + .isar + .stackThemes + .watchLazy() + .listen((_) { + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _updateInstalledList(); + }); + }); + } }); - } - }); super.initState(); } @@ -275,24 +278,25 @@ class _IncognitoInstalledThemesState return Wrap( spacing: 16, runSpacing: 16, - children: installedThemeIdNames - .map( - (e) => SizedBox( - key: Key("IncognitoInstalledThemes_card_${e.item1}_key"), - width: widget.cardWidth, - child: StackThemeCard( - data: StackThemeMetaData( - name: e.item2, - id: e.item1, - version: e.item3 ?? 1, - sha256: "", - size: "", - previewImageUrl: "", + children: + installedThemeIdNames + .map( + (e) => SizedBox( + key: Key("IncognitoInstalledThemes_card_${e.item1}_key"), + width: widget.cardWidth, + child: StackThemeCard( + data: StackThemeMetaData( + name: e.item2, + id: e.item1, + version: e.item3 ?? 1, + sha256: "", + size: "", + previewImageUrl: "", + ), + ), ), - ), - ), - ) - .toList(), + ) + .toList(), ); } } diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/system_brightness_theme_selection_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/system_brightness_theme_selection_view.dart index becef5fb6..d78c6cb3f 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/system_brightness_theme_selection_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/system_brightness_theme_selection_view.dart @@ -64,11 +64,12 @@ class _SystemBrightnessThemeSelectionViewState @override void initState() { - installedThemeIdNames = ref - .read(pThemeService) - .installedThemes - .map((e) => Tuple2(e.themeId, e.name)) - .toList(); + installedThemeIdNames = + ref + .read(pThemeService) + .installedThemes + .map((e) => Tuple2(e.themeId, e.name)) + .toList(); super.initState(); } @@ -89,52 +90,45 @@ class _SystemBrightnessThemeSelectionViewState style: STextStyles.navBarTitle(context), ), ), - body: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 16, - ), - RoundedWhiteContainer( - child: Text( - "Select a light and dark theme that will be" - " activated automatically when your phone system" - " switches light and dark mode.", - style: STextStyles.smallMed12(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: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + RoundedWhiteContainer( + child: Text( + "Select a light and dark theme that will be" + " activated automatically when your phone system" + " switches light and dark mode.", + style: STextStyles.smallMed12(context), + ), ), - ), - const SizedBox( - height: 10, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Choose light mode theme", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 18, - ), - for (int i = 0; + const SizedBox(height: 10), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Choose light mode theme", + style: STextStyles.titleBold12(context), + ), + const SizedBox(height: 18), + for ( + int i = 0; i < (2 * installedThemeIdNames.length) - 1; - i++) - (i % 2 == 1) - ? const SizedBox( - height: 10, - ) - : ThemeOption( + i++ + ) + (i % 2 == 1) + ? const SizedBox(height: 10) + : ThemeOption( label: installedThemeIdNames[i ~/ 2].item2, onPressed: () { @@ -170,36 +164,33 @@ class _SystemBrightnessThemeSelectionViewState installedThemeIdNames[i ~/ 2].item1, groupValue: ref.watch( prefsChangeNotifierProvider.select( - (value) => value - .systemBrightnessLightThemeId, + (value) => + value + .systemBrightnessLightThemeId, ), ), ), - ], + ], + ), ), - ), - const SizedBox( - height: 10, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Choose dark mode theme", - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 18, - ), - for (int i = 0; + const SizedBox(height: 10), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Choose dark mode theme", + style: STextStyles.titleBold12(context), + ), + const SizedBox(height: 18), + for ( + int i = 0; i < (2 * installedThemeIdNames.length) - 1; - i++) - (i % 2 == 1) - ? const SizedBox( - height: 10, - ) - : ThemeOption( + i++ + ) + (i % 2 == 1) + ? const SizedBox(height: 10) + : ThemeOption( label: installedThemeIdNames[i ~/ 2].item2, onPressed: () { @@ -235,24 +226,24 @@ class _SystemBrightnessThemeSelectionViewState installedThemeIdNames[i ~/ 2].item1, groupValue: ref.watch( prefsChangeNotifierProvider.select( - (value) => value - .systemBrightnessDarkThemeId, + (value) => + value + .systemBrightnessDarkThemeId, ), ), ), - ], + ], + ), ), - ), - const SizedBox( - height: 16, - ), - ], + const SizedBox(height: 16), + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/currency_view.dart b/lib/pages/settings_views/global_settings_view/currency_view.dart index 2f85dc445..f49671b22 100644 --- a/lib/pages/settings_views/global_settings_view/currency_view.dart +++ b/lib/pages/settings_views/global_settings_view/currency_view.dart @@ -69,20 +69,14 @@ class _CurrencyViewState extends ConsumerState { BorderRadius? _borderRadius(int index) { if (index == 0 && currenciesWithoutSelected.length == 1) { - return BorderRadius.circular( - Constants.size.circularBorderRadius, - ); + return BorderRadius.circular(Constants.size.circularBorderRadius); } else if (index == 0) { return BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), + top: Radius.circular(Constants.size.circularBorderRadius), ); } else if (index == currenciesWithoutSelected.length - 1) { return BorderRadius.vertical( - bottom: Radius.circular( - Constants.size.circularBorderRadius, - ), + bottom: Radius.circular(Constants.size.circularBorderRadius), ); } return null; @@ -120,14 +114,16 @@ class _CurrencyViewState extends ConsumerState { final isDesktop = Util.isDesktop; if (!isDesktop) { - current = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + current = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); } - currenciesWithoutSelected = ref - .watch(baseCurrenciesProvider.select((value) => value.map)) - .keys - .toList(); + currenciesWithoutSelected = + ref + .watch(baseCurrenciesProvider.select((value) => value.map)) + .keys + .toList(); if (current.isNotEmpty) { currenciesWithoutSelected.remove(current); @@ -157,18 +153,13 @@ class _CurrencyViewState extends ConsumerState { } }, ), - title: Text( - "Currency", - style: STextStyles.navBarTitle(context), - ), + title: Text("Currency", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), + child: child, ), - child: child, ), ), ); @@ -194,9 +185,7 @@ class _CurrencyViewState extends ConsumerState { child: child, ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( @@ -206,9 +195,7 @@ class _CurrencyViewState extends ConsumerState { onPressed: Navigator.of(context).pop, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Save changes", @@ -240,8 +227,9 @@ class _CurrencyViewState extends ConsumerState { headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverOverlapAbsorber( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), sliver: SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(bottom: 16), @@ -274,26 +262,27 @@ class _CurrencyViewState extends ConsumerState { height: 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 = ""; - filter = ""; - }); - }, - ), - ], + 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 = ""; + filter = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), @@ -312,116 +301,110 @@ class _CurrencyViewState extends ConsumerState { ), ), SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, - borderRadius: _borderRadius(index), + delegate: SliverChildBuilderDelegate((context, index) { + return Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.popupBG, + borderRadius: _borderRadius(index), + ), + child: Padding( + padding: const EdgeInsets.all(4), + key: Key( + "currencySelect_${currenciesWithoutSelected[index]}", ), - child: Padding( - padding: const EdgeInsets.all(4), - key: Key( - "currencySelect_${currenciesWithoutSelected[index]}", - ), - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: currenciesWithoutSelected[index] == current - ? Theme.of(context) - .extension()! - .currencyListItemBG - : Theme.of(context) - .extension()! - .popupBG, - child: RawMaterialButton( - onPressed: () async { - onTap(index); - }, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: + currenciesWithoutSelected[index] == current + ? Theme.of(context) + .extension()! + .currencyListItemBG + : Theme.of( + context, + ).extension()!.popupBG, + child: RawMaterialButton( + onPressed: () async { + onTap(index); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - value: true, - groupValue: currenciesWithoutSelected[ - index] == - current, - onChanged: (_) { - onTap(index); - }, - ), - ), - const SizedBox( - width: 12, + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: true, + groupValue: + currenciesWithoutSelected[index] == + current, + onChanged: (_) { + onTap(index); + }, ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - currenciesWithoutSelected[index], - key: (currenciesWithoutSelected[ - index] == - current) - ? const Key( + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + currenciesWithoutSelected[index], + key: + (currenciesWithoutSelected[index] == + current) + ? const Key( "selectedCurrencySettingsCurrencyText", ) - : null, - style: STextStyles.largeMedium14( - context, - ), - ), - const SizedBox( - height: 2, + : null, + style: STextStyles.largeMedium14( + context, ), - Text( - ref.watch( - baseCurrenciesProvider.select( - (value) => value.map, - ), - )[currenciesWithoutSelected[ - index]] ?? - "", - key: (currenciesWithoutSelected[ - index] == - current) - ? const Key( + ), + const SizedBox(height: 2), + Text( + ref.watch( + baseCurrenciesProvider.select( + (value) => value.map, + ), + )[currenciesWithoutSelected[index]] ?? + "", + key: + (currenciesWithoutSelected[index] == + current) + ? const Key( "selectedCurrencySettingsCurrencyTextDescription", ) - : null, - style: STextStyles.itemSubtitle( - context, - ), + : null, + style: STextStyles.itemSubtitle( + context, ), - ], - ), - ], - ), + ), + ], + ), + ], ), ), ), ), - ); - }, - childCount: currenciesWithoutSelected.length, - ), + ), + ); + }, childCount: currenciesWithoutSelected.length), ), ], ); diff --git a/lib/pages/settings_views/global_settings_view/global_settings_view.dart b/lib/pages/settings_views/global_settings_view/global_settings_view.dart index 082d03429..5dc6d4101 100644 --- a/lib/pages/settings_views/global_settings_view/global_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/global_settings_view.dart @@ -38,9 +38,7 @@ import 'syncing_preferences_views/syncing_preferences_view.dart'; import 'tor_settings/tor_settings_view.dart'; class GlobalSettingsView extends StatelessWidget { - const GlobalSettingsView({ - super.key, - }); + const GlobalSettingsView({super.key}); static const String routeName = "/globalSettings"; @@ -56,271 +54,248 @@ class GlobalSettingsView extends StatelessWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Settings", - style: STextStyles.navBarTitle(context), - ), + title: Text("Settings", style: STextStyles.navBarTitle(context)), ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - SettingsListButton( - iconAssetName: Assets.svg.addressBook, - iconSize: 16, - title: "Address book", - onPressed: () { - Navigator.of(context) - .pushNamed(AddressBookView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.downloadFolder, - iconSize: 14, - title: "${AppConfig.prefix} backup & restore", - onPressed: () { - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: - StackBackupView.routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to access ${AppConfig.prefix} backup & restore settings", - biometricsAuthenticationTitle: - "${AppConfig.prefix} backup", - ), - settings: const RouteSettings( - name: "/swblockscreen", + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: Column( + children: [ + SettingsListButton( + iconAssetName: Assets.svg.addressBook, + iconSize: 16, + title: "Address book", + onPressed: () { + Navigator.of( + context, + ).pushNamed(AddressBookView.routeName); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.downloadFolder, + iconSize: 14, + title: + "${AppConfig.prefix} backup & restore", + onPressed: () { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: + StackBackupView.routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to access ${AppConfig.prefix} backup & restore settings", + biometricsAuthenticationTitle: + "${AppConfig.prefix} backup", + ), + settings: const RouteSettings( + name: "/swblockscreen", + ), ), - ), - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.lock, - iconSize: 16, - title: "Security", - onPressed: () { - Navigator.of(context) - .pushNamed(SecurityView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.dollarSign, - iconSize: 18, - title: "Currency", - onPressed: () { - Navigator.of(context).pushNamed( - BaseCurrencySettingsView.routeName, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.language, - iconSize: 18, - title: "Language", - onPressed: () { - Navigator.of(context).pushNamed( - LanguageSettingsView.routeName, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.tor, - iconSize: 18, - title: "Tor Settings", - onPressed: () { - Navigator.of(context) - .pushNamed(TorSettingsView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.node, - iconSize: 16, - title: "Manage nodes", - onPressed: () { - Navigator.of(context) - .pushNamed(ManageNodesView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowRotate, - iconSize: 18, - title: "Syncing preferences", - onPressed: () { - Navigator.of(context).pushNamed( - SyncingPreferencesView.routeName, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowUpRight, - iconSize: 16, - title: "Startup", - onPressed: () { - Navigator.of(context).pushNamed( - StartupPreferencesView.routeName, - ); - }, - ), - if (AppConfig.hasFeature( - AppFeature.themeSelection)) - const SizedBox( - height: 8, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.lock, + iconSize: 16, + title: "Security", + onPressed: () { + Navigator.of( + context, + ).pushNamed(SecurityView.routeName); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.dollarSign, + iconSize: 18, + title: "Currency", + onPressed: () { + Navigator.of(context).pushNamed( + BaseCurrencySettingsView.routeName, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.language, + iconSize: 18, + title: "Language", + onPressed: () { + Navigator.of(context).pushNamed( + LanguageSettingsView.routeName, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.tor, + iconSize: 18, + title: "Tor Settings", + onPressed: () { + Navigator.of( + context, + ).pushNamed(TorSettingsView.routeName); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.node, + iconSize: 16, + title: "Manage nodes", + onPressed: () { + Navigator.of( + context, + ).pushNamed(ManageNodesView.routeName); + }, ), - if (AppConfig.hasFeature( - AppFeature.themeSelection)) + const SizedBox(height: 8), SettingsListButton( - iconAssetName: Assets.svg.sun, + iconAssetName: Assets.svg.arrowRotate, iconSize: 18, - title: "Appearance", + title: "Syncing preferences", onPressed: () { Navigator.of(context).pushNamed( - AppearanceSettingsView.routeName, + SyncingPreferencesView.routeName, ); }, ), - if (Platform.isIOS) - const SizedBox( - height: 8, + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.arrowUpRight, + iconSize: 16, + title: "Startup", + onPressed: () { + Navigator.of(context).pushNamed( + StartupPreferencesView.routeName, + ); + }, + ), + if (AppConfig.hasFeature( + AppFeature.themeSelection, + )) + const SizedBox(height: 8), + if (AppConfig.hasFeature( + AppFeature.themeSelection, + )) + SettingsListButton( + iconAssetName: Assets.svg.sun, + iconSize: 18, + title: "Appearance", + onPressed: () { + Navigator.of(context).pushNamed( + AppearanceSettingsView.routeName, + ); + }, + ), + if (Platform.isIOS) const SizedBox(height: 8), + if (Platform.isIOS) + SettingsListButton( + iconAssetName: Assets.svg.circleAlert, + iconSize: 16, + title: "Delete account", + onPressed: () async { + await Navigator.of(context).pushNamed( + DeleteAccountView.routeName, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.ellipsis, + iconSize: 18, + title: "About", + onPressed: () { + Navigator.of( + context, + ).pushNamed(AboutView.routeName); + }, ), - if (Platform.isIOS) + const SizedBox(height: 8), SettingsListButton( - iconAssetName: Assets.svg.circleAlert, + iconAssetName: Assets.svg.solidSliders, iconSize: 16, - title: "Delete account", - onPressed: () async { - await Navigator.of(context).pushNamed( - DeleteAccountView.routeName, + title: "Advanced", + onPressed: () { + Navigator.of(context).pushNamed( + AdvancedSettingsView.routeName, ); }, ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.ellipsis, - iconSize: 18, - title: "About", - onPressed: () { - Navigator.of(context) - .pushNamed(AboutView.routeName); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.solidSliders, - iconSize: 16, - title: "Advanced", - onPressed: () { - Navigator.of(context).pushNamed( - AdvancedSettingsView.routeName, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.questionMessage, - iconSize: 16, - title: "Support", - onPressed: () { - Navigator.of(context) - .pushNamed(SupportView.routeName); - }, - ), - // TextButton( - // style: Theme.of(context) - // .textButtonTheme - // .style - // ?.copyWith( - // backgroundColor: - // MaterialStateProperty.all( - // Theme.of(context).extension()!.accentColorDark - // ), - // ), - // child: Text( - // "fire test notification", - // style: STextStyles.button(context), - // ), - // onPressed: () async { - // NotificationApi.showNotification2( - // title: "Test notification", - // body: "My doggy wallet", - // walletId: - // "3c5e2d70-fcc3-11ec-86a3-31a106a81c3b", - // iconAssetName: - // Assets.svg.iconFor(coin: Coin.dogecoin), - // date: DateTime.now(), - // ); - // }, - // ), - ], + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.questionMessage, + iconSize: 16, + title: "Support", + onPressed: () { + Navigator.of( + context, + ).pushNamed(SupportView.routeName); + }, + ), + // TextButton( + // style: Theme.of(context) + // .textButtonTheme + // .style + // ?.copyWith( + // backgroundColor: + // MaterialStateProperty.all( + // Theme.of(context).extension()!.accentColorDark + // ), + // ), + // child: Text( + // "fire test notification", + // style: STextStyles.button(context), + // ), + // onPressed: () async { + // NotificationApi.showNotification2( + // title: "Test notification", + // body: "My doggy wallet", + // walletId: + // "3c5e2d70-fcc3-11ec-86a3-31a106a81c3b", + // iconAssetName: + // Assets.svg.iconFor(coin: Coin.dogecoin), + // date: DateTime.now(), + // ); + // }, + // ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - ], + const SizedBox(height: 12), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); 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 99fb927cb..52b78cbcf 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -41,338 +41,344 @@ class HiddenSettings extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: AppBarIconButton( size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, width: 18, height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: Navigator.of(context).pop, ), ), - title: Text( - "Dev options", - style: STextStyles.navBarTitle(context), - ), + title: Text("Dev options", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - ref - .read(prefsChangeNotifierProvider) - .advancedFiroFeatures = - !ref - .read(prefsChangeNotifierProvider) - .advancedFiroFeatures; - }, - child: RoundedWhiteContainer( - child: Text( - ref.watch( - prefsChangeNotifierProvider.select( - (s) => s.advancedFiroFeatures, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + ref + .read(prefsChangeNotifierProvider) + .advancedFiroFeatures = !ref + .read(prefsChangeNotifierProvider) + .advancedFiroFeatures; + }, + child: RoundedWhiteContainer( + child: Text( + ref.watch( + prefsChangeNotifierProvider.select( + (s) => s.advancedFiroFeatures, + ), + ) + ? "Hide advanced Firo features" + : "Show advanced Firo features", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), - ) - ? "Hide advanced Firo features" - : "Show advanced Firo features", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, ), ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - final notifs = ref - .read(notificationsProvider) - .notifications; + ); + }, + ), + const SizedBox(height: 12), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + final notifs = + ref + .read(notificationsProvider) + .notifications; - for (final n in notifs) { + for (final n in notifs) { + await ref + .read(notificationsProvider) + .delete(n, false); + } await ref .read(notificationsProvider) - .delete(n, false); - } - await ref - .read(notificationsProvider) - .delete(notifs[0], true); + .delete(notifs[0], true); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Notification history deleted", - context: context, + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Notification history deleted", + context: context, + ), + ); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Delete notifications", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), - ); - } - }, - child: RoundedWhiteContainer( - child: Text( - "Delete notifications", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - ); - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - ref.read(prefsChangeNotifierProvider).logsPath = - null; - }, - child: RoundedWhiteContainer( - child: Text( - "Reset log location", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, ), ), - ), - ); - }, - ), - // const SizedBox( - // height: 12, - // ), - // Consumer(builder: (_, ref, __) { - // return GestureDetector( - // onTap: () async { - // final trades = - // ref.read(tradesServiceProvider).trades; - // - // for (final trade in trades) { - // ref.read(tradesServiceProvider).delete( - // trade: trade, shouldNotifyListeners: false); - // } - // ref.read(tradesServiceProvider).delete( - // trade: trades[0], shouldNotifyListeners: true); - // - // // ref.read(notificationsProvider).DELETE_EVERYTHING(); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Delete trade history", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context).extension()!.accentColorDark - // ), - // ), - // ), - // ); - // }), - // const SizedBox( - // height: 12, - // ), - // Consumer( - // builder: (_, ref, __) { - // return GestureDetector( - // onTap: () async { - // await ref - // .read(debugServiceProvider) - // .deleteAllLogs(); - // - // if (context.mounted) { - // unawaited( - // showFloatingFlushBar( - // type: FlushBarType.success, - // message: "Debug Logs deleted", - // context: context, - // ), - // ); - // } - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "Delete Debug Logs", - // 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 - .select((value) => value.familiarity), - ) < - 6) { + ); + }, + ), + const SizedBox(height: 12), + Consumer( + builder: (_, ref, __) { return GestureDetector( onTap: () async { - final familiarity = ref + ref .read(prefsChangeNotifierProvider) - .familiarity; - if (familiarity < 6) { - ref - .read(prefsChangeNotifierProvider) - .familiarity = 6; - - Constants.exchangeForExperiencedUsers(6); - } + .logsPath = null; }, child: RoundedWhiteContainer( child: Text( - "Enable exchange", + "Reset log location", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), ); - } else { - return Container(); - } - }, - ), - const SizedBox( - height: 12, - ), - Consumer( - builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - await showDialog( - context: context, - builder: (_) => TorWarningDialog( - coin: Stellar(CryptoCurrencyNetwork.main), + }, + ), + // const SizedBox( + // height: 12, + // ), + // Consumer(builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // final trades = + // ref.read(tradesServiceProvider).trades; + // + // for (final trade in trades) { + // ref.read(tradesServiceProvider).delete( + // trade: trade, shouldNotifyListeners: false); + // } + // ref.read(tradesServiceProvider).delete( + // trade: trades[0], shouldNotifyListeners: true); + // + // // ref.read(notificationsProvider).DELETE_EVERYTHING(); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Delete trade history", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context).extension()!.accentColorDark + // ), + // ), + // ), + // ); + // }), + // const SizedBox( + // height: 12, + // ), + // Consumer( + // builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // await ref + // .read(debugServiceProvider) + // .deleteAllLogs(); + // + // if (context.mounted) { + // unawaited( + // showFloatingFlushBar( + // type: FlushBarType.success, + // message: "Debug Logs deleted", + // context: context, + // ), + // ); + // } + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Delete Debug Logs", + // 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.select( + (value) => value.familiarity, + ), + ) < + 6) { + return GestureDetector( + onTap: () async { + final familiarity = + ref + .read(prefsChangeNotifierProvider) + .familiarity; + if (familiarity < 6) { + ref + .read(prefsChangeNotifierProvider) + .familiarity = 6; + + Constants.exchangeForExperiencedUsers(6); + } + }, + child: RoundedWhiteContainer( + child: Text( + "Enable exchange", + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), + ), ), ); - }, - child: RoundedWhiteContainer( - child: Text( - "Show Tor warning popup", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + } else { + return Container(); + } + }, + ), + const SizedBox(height: 12), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + await showDialog( + context: context, + builder: + (_) => TorWarningDialog( + coin: Stellar( + CryptoCurrencyNetwork.main, + ), + ), + ); + }, + child: RoundedWhiteContainer( + child: Text( + "Show Tor warning popup", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), - ), - ); - }, - ), - // const SizedBox( - // height: 12, - // ), - // Consumer( - // builder: (_, ref, __) { - // return GestureDetector( - // onTap: () async { - // await showLoading( - // whileFuture: FiroCache.init(), - // context: context, - // rootNavigator: true, - // message: "initializing firo cache", - // ); - // }, - // child: RoundedWhiteContainer( - // child: Text( - // "init firo_cache", - // style: STextStyles.button(context).copyWith( - // color: Theme.of(context) - // .extension()! - // .accentColorDark, - // ), - // ), - // ), - // ); - // }, - // ), - ], + ); + }, + ), + // const SizedBox( + // height: 12, + // ), + // Consumer( + // builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // await showLoading( + // whileFuture: FiroCache.init(), + // context: context, + // rootNavigator: true, + // message: "initializing firo cache", + // ); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "init firo_cache", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .accentColorDark, + // ), + // ), + // ), + // ); + // }, + // ), + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/language_view.dart b/lib/pages/settings_views/global_settings_view/language_view.dart index 8f1ca3012..9490530d5 100644 --- a/lib/pages/settings_views/global_settings_view/language_view.dart +++ b/lib/pages/settings_views/global_settings_view/language_view.dart @@ -59,20 +59,14 @@ class _LanguageViewState extends ConsumerState { BorderRadius? _borderRadius(int index) { if (index == 0 && listWithoutSelected.length == 1) { - return BorderRadius.circular( - Constants.size.circularBorderRadius, - ); + return BorderRadius.circular(Constants.size.circularBorderRadius); } else if (index == 0) { return BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), + top: Radius.circular(Constants.size.circularBorderRadius), ); } else if (index == listWithoutSelected.length - 1) { return BorderRadius.vertical( - bottom: Radius.circular( - Constants.size.circularBorderRadius, - ), + bottom: Radius.circular(Constants.size.circularBorderRadius), ); } return null; @@ -103,8 +97,9 @@ class _LanguageViewState extends ConsumerState { @override Widget build(BuildContext context) { - current = ref - .watch(prefsChangeNotifierProvider.select((value) => value.language)); + current = ref.watch( + prefsChangeNotifierProvider.select((value) => value.language), + ); listWithoutSelected = languages; if (current.isNotEmpty) { @@ -127,101 +122,99 @@ class _LanguageViewState extends ConsumerState { } }, ), - title: Text( - "Language", - style: STextStyles.navBarTitle(context), - ), + title: Text("Language", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchController, - focusNode: _searchFocusNode, - onChanged: (newString) { - setState(() => filter = newString); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), + sliver: SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (newString) { + setState(() => filter = newString); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; - filter = ""; - }); - }, + 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 = ""; + filter = ""; + }); + }, + ), + ], ), - ], - ), - ), - ) - : null, + ), + ) + : null, + ), ), ), ), ), ), - ), - ]; - }, - body: Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context, + ]; + }, + body: Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor( + context, + ), ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { return Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, + color: + Theme.of( + context, + ).extension()!.popupBG, borderRadius: _borderRadius(index), ), child: Padding( @@ -231,13 +224,14 @@ class _LanguageViewState extends ConsumerState { ), child: RoundedContainer( padding: const EdgeInsets.all(0), - color: index == 0 - ? Theme.of(context) - .extension()! - .currencyListItemBG - : Theme.of(context) - .extension()! - .popupBG, + color: + index == 0 + ? Theme.of(context) + .extension()! + .currencyListItemBG + : Theme.of( + context, + ).extension()!.popupBG, child: RawMaterialButton( onPressed: () async { onTap(index); @@ -257,9 +251,10 @@ class _LanguageViewState extends ConsumerState { width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: true, groupValue: index == 0, onChanged: (_) { @@ -267,34 +262,32 @@ class _LanguageViewState extends ConsumerState { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( listWithoutSelected[index], - key: (index == 0) - ? const Key( - "selectedLanguageSettingsLanguageText", - ) - : null, + key: + (index == 0) + ? const Key( + "selectedLanguageSettingsLanguageText", + ) + : null, style: STextStyles.largeMedium14( context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( listWithoutSelected[index], - key: (index == 0) - ? const Key( - "selectedLanguageSettingsLanguageTextDescription", - ) - : null, + key: + (index == 0) + ? const Key( + "selectedLanguageSettingsLanguageTextDescription", + ) + : null, style: STextStyles.itemSubtitle( context, ), @@ -308,13 +301,12 @@ class _LanguageViewState extends ConsumerState { ), ), ); - }, - childCount: listWithoutSelected.length, + }, childCount: listWithoutSelected.length), ), - ), - ], - ); - }, + ], + ); + }, + ), ), ), ), 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 0f00f75db..ca5b825ad 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 @@ -10,7 +10,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'; @@ -24,6 +23,7 @@ import '../../../../providers/global/secure_store_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; +import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/sync_type_enum.dart'; import '../../../../utilities/flutter_secure_storage_interface.dart'; @@ -36,6 +36,7 @@ import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -109,111 +110,107 @@ class _AddEditNodeViewState extends ConsumerState { context: context, useSafeArea: true, barrierDismissible: true, - builder: (_) => isDesktop - ? DesktopDialog( - maxWidth: 440, - maxHeight: 300, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 32, - ), - child: Row( + builder: + (_) => + isDesktop + ? DesktopDialog( + maxWidth: 440, + maxHeight: 300, + child: Column( children: [ - const SizedBox( - width: 32, - ), - Text( - "Server currently unreachable", - style: STextStyles.desktopH3(context), - ), - ], - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - top: 16, - bottom: 32, - ), - child: Column( - children: [ - const Spacer(), - Text( - "Would you like to save this node anyways?", - style: STextStyles.desktopTextMedium(context), - ), - const Spacer( - flex: 2, - ), - Row( + Padding( + padding: const EdgeInsets.only(top: 32), + child: Row( children: [ - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: - isDesktop ? ButtonHeight.l : null, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(false), - ), - ), - const SizedBox( - width: 16, + const SizedBox(width: 32), + Text( + "Server currently unreachable", + style: STextStyles.desktopH3(context), ), - Expanded( - child: PrimaryButton( - label: "Save", - buttonHeight: - isDesktop ? ButtonHeight.l : null, - onPressed: () => Navigator.of( + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, + ), + child: Column( + children: [ + const Spacer(), + Text( + "Would you like to save this node anyways?", + style: STextStyles.desktopTextMedium( context, - rootNavigator: true, - ).pop(true), + ), ), - ), - ], + const Spacer(flex: 2), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: + isDesktop ? ButtonHeight.l : null, + onPressed: + () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + label: "Save", + buttonHeight: + isDesktop ? ButtonHeight.l : null, + onPressed: + () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), + ), + ), + ], + ), + ], + ), ), - ], + ), + ], + ), + ) + : StackDialog( + title: "Server currently unreachable", + message: "Would you like to save this node anyways?", + leftButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(false); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), ), ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(true); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text("Save", style: STextStyles.button(context)), + ), ), - ], - ), - ) - : StackDialog( - title: "Server currently unreachable", - message: "Would you like to save this node anyways?", - leftButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(false); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(true); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), - ), - ), ).then((value) { if (value is bool && value) { shouldSave = true; @@ -232,18 +229,22 @@ class _AddEditNodeViewState extends ConsumerState { // strip unused path String address = formData.host!; - if (coin is LibMoneroWallet) { + if (coin is LibMoneroWallet || coin is LibSalviumWallet) { if (address.startsWith("http")) { final uri = Uri.parse(address); address = "${uri.scheme}://${uri.host}"; } } - final torEnabled = formData.netOption == TorPlainNetworkOption.tor || + final torEnabled = + formData.netOption == TorPlainNetworkOption.tor || formData.netOption == TorPlainNetworkOption.both; - final plainEnabled = formData.netOption == TorPlainNetworkOption.clear || + final plainEnabled = + formData.netOption == TorPlainNetworkOption.clear || formData.netOption == TorPlainNetworkOption.both; + final forceNoTor = formData.forceNoTor ?? false; + switch (viewType) { case AddEditNodeViewType.add: final NodeModel node = NodeModel( @@ -260,17 +261,18 @@ class _AddEditNodeViewState extends ConsumerState { isDown: false, torEnabled: torEnabled, clearnetEnabled: plainEnabled, + forceNoTor: forceNoTor, + isPrimary: false, ); - await ref.read(nodeServiceChangeNotifierProvider).add( - node, - formData.password, - true, - ); + await ref + .read(nodeServiceChangeNotifierProvider) + .save(node, formData.password, true); await _notifyWalletsOfUpdatedNode(); if (mounted) { - Navigator.of(context) - .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); } break; case AddEditNodeViewType.edit: @@ -288,25 +290,28 @@ class _AddEditNodeViewState extends ConsumerState { isDown: false, torEnabled: torEnabled, clearnetEnabled: plainEnabled, + forceNoTor: forceNoTor, + isPrimary: formData.isPrimary ?? false, ); - await ref.read(nodeServiceChangeNotifierProvider).add( - node, - formData.password, - true, - ); + await ref + .read(nodeServiceChangeNotifierProvider) + .save(node, formData.password, true); await _notifyWalletsOfUpdatedNode(); if (mounted) { - Navigator.of(context) - .popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(widget.routeOnSuccessOrDelete)); } break; } } Future _notifyWalletsOfUpdatedNode() async { - final wallets = - ref.read(pWallets).wallets.where((e) => e.info.coin == widget.coin); + final wallets = ref + .read(pWallets) + .wallets + .where((e) => e.info.coin == widget.coin); final prefs = ref.read(prefsChangeNotifierProvider); switch (prefs.syncType) { @@ -356,24 +361,41 @@ class _AddEditNodeViewState extends ConsumerState { try { await _processQrData(qrResult); } catch (e, s) { - Logging.instance.e("Error processing QR code data: ", - error: e, stackTrace: s); + Logging.instance.e( + "Error processing QR code data: ", + error: e, + stackTrace: s, + ); } } } catch (e, s) { - Logging.instance.e("Error opening QR code scanner dialog: ", - error: e, stackTrace: s); + Logging.instance.e( + "Error opening QR code scanner dialog: ", + error: e, + stackTrace: s, + ); } } else { try { - final result = await BarcodeScanner.scan(); + final result = await ref.read(pBarcodeScanner).scan(context: context); await _processQrData(result.rawContent); + } on PlatformException catch (e, s) { + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } } catch (e, s) { - Logging.instance.e( - "$runtimeType._scanQr()", - error: e, - stackTrace: s, - ); + Logging.instance.e("$runtimeType._scanQr()", error: e, stackTrace: s); } } } finally { @@ -400,17 +422,14 @@ class _AddEditNodeViewState extends ConsumerState { torEnabled: true, clearnetEnabled: !nodeQrData.host.endsWith(".onion"), loginName: (nodeQrData as LibMoneroNodeQrData?)?.user, + isPrimary: false, ), - (nodeQrData as LibMoneroNodeQrData?)?.password ?? "" + (nodeQrData as LibMoneroNodeQrData?)?.password ?? "", ); }); } } catch (e, s) { - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); } } @@ -446,201 +465,204 @@ class _AddEditNodeViewState extends ConsumerState { final NodeModel? node = viewType == AddEditNodeViewType.edit && nodeId != null ? ref.watch( - nodeServiceChangeNotifierProvider - .select((value) => value.getNodeById(id: nodeId!)), - ) + nodeServiceChangeNotifierProvider.select( + (value) => value.getNodeById(id: nodeId!), + ), + ) : null; return ConditionalParent( condition: !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: 75)); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - viewType == AddEditNodeViewType.edit ? "Edit node" : "Add node", - style: STextStyles.navBarTitle(context), - ), - actions: [ - if (viewType == AddEditNodeViewType.add && - coin - is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("qrNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: QrCodeIcon( - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + 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: 75), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + viewType == AddEditNodeViewType.edit + ? "Edit node" + : "Add node", + style: STextStyles.navBarTitle(context), + ), + actions: [ + if (viewType == AddEditNodeViewType.add && + coin + is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("qrNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: QrCodeIcon( + width: 20, + height: 20, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + onPressed: _scanQr, + ), ), - onPressed: _scanQr, ), - ), - ), - if (viewType == AddEditNodeViewType.edit && - ref - .watch( - nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin)), - ) - .length > - 1) - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("deleteNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: SvgPicture.asset( - Assets.svg.trash, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, + if (viewType == AddEditNodeViewType.edit && + ref + .watch( + nodeServiceChangeNotifierProvider.select( + (value) => value.getNodesFor(coin), + ), + ) + .length > + 1) + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, ), - onPressed: () async { - Navigator.popUntil( - context, - ModalRoute.withName(widget.routeOnSuccessOrDelete), - ); - - await ref - .read(nodeServiceChangeNotifierProvider) - .delete( - nodeId!, - true, + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("deleteNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.trash, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + width: 20, + height: 20, + ), + onPressed: () async { + Navigator.popUntil( + context, + ModalRoute.withName( + widget.routeOnSuccessOrDelete, + ), ); - }, - ), - ), - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, - bottom: 12, - ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight - 8), - child: IntrinsicHeight( - child: child, + + await ref + .read(nodeServiceChangeNotifierProvider) + .delete(nodeId!, true); + }, + ), ), ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + bottom: 12, ), - ); - }, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 8, + ), + child: IntrinsicHeight(child: child), + ), + ), + ); + }, + ), + ), + ), ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: 8, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ + const SizedBox(height: 8), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const SizedBox( - width: 8, - ), - const AppBarBackButton( - iconSize: 24, - size: 40, - ), - Text( - "Add new node", - style: STextStyles.desktopH3(context), + Row( + children: [ + const SizedBox(width: 8), + const AppBarBackButton(iconSize: 24, size: 40), + Text( + "Add new node", + style: STextStyles.desktopH3(context), + ), + ], ), + if (coin + is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future + Padding( + padding: const EdgeInsets.only(right: 32), + child: AppBarIconButton( + size: 40, + color: + isDesktop + ? Theme.of(context) + .extension()! + .textFieldDefaultBG + : Theme.of( + context, + ).extension()!.background, + icon: const QrCodeIcon(width: 21, height: 21), + onPressed: _scanQr, + ), + ), ], ), - if (coin - is CryptonoteCurrency) // TODO: [prio=low] do something other than `coin is CryptonoteCurrency` in the future - Padding( - padding: const EdgeInsets.only(right: 32), - child: AppBarIconButton( - size: 40, - color: isDesktop - ? Theme.of(context) - .extension()! - .textFieldDefaultBG - : Theme.of(context) - .extension()! - .background, - icon: const QrCodeIcon( - width: 21, - height: 21, - ), - onPressed: _scanQr, - ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, ), + child: child, + ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - top: 16, - bottom: 32, - ), - child: child, - ), - ], - ), - ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -670,10 +692,7 @@ class _AddEditNodeViewState extends ConsumerState { }, ), if (!isDesktop) const Spacer(), - if (isDesktop) - const SizedBox( - height: 78, - ), + if (isDesktop) const SizedBox(height: 78), Row( children: [ Expanded( @@ -681,42 +700,40 @@ class _AddEditNodeViewState extends ConsumerState { label: "Test connection", enabled: testConnectionEnabled, buttonHeight: isDesktop ? ButtonHeight.l : null, - onPressed: testConnectionEnabled - ? () async { - final testPassed = await testNodeConnection( - context: context, - onSuccess: _onTestSuccess, - cryptoCurrency: coin, - nodeFormData: ref.read(nodeFormDataProvider), - ref: ref, - ); - if (context.mounted) { - if (testPassed) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Server ping success", - context: context, - ), - ); - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Server unreachable", - context: context, - ), - ); + onPressed: + testConnectionEnabled + ? () async { + final testPassed = await testNodeConnection( + context: context, + onSuccess: _onTestSuccess, + cryptoCurrency: coin, + nodeFormData: ref.read(nodeFormDataProvider), + ref: ref, + ); + if (context.mounted) { + if (testPassed) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Server ping success", + context: context, + ), + ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Server unreachable", + context: context, + ), + ); + } } } - } - : null, + : null, ), ), - if (isDesktop) - const SizedBox( - width: 16, - ), + if (isDesktop) const SizedBox(width: 16), if (isDesktop) Expanded( child: PrimaryButton( @@ -728,24 +745,19 @@ class _AddEditNodeViewState extends ConsumerState { ), ], ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), if (!isDesktop) TextButton( - style: saveEnabled - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), + style: + saveEnabled + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), onPressed: saveEnabled ? attemptSave : null, - child: Text( - "Save", - style: STextStyles.button(context), - ), + child: Text("Save", style: STextStyles.button(context)), ), ], ), @@ -757,12 +769,12 @@ class _AddEditNodeViewState extends ConsumerState { class NodeFormData { String? name, host, login, password; int? port; - bool? useSSL, isFailover, trusted; + bool? useSSL, isFailover, trusted, forceNoTor, isPrimary; TorPlainNetworkOption? netOption; @override String toString() { - return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, trusted: $trusted, netOption: $netOption }"; + return "{ name: $name, host: $host, port: $port, useSSL: $useSSL, trusted: $trusted, netOption: $netOption, isPrimary: $isPrimary }"; } } @@ -806,9 +818,11 @@ class _NodeFormState extends ConsumerState { bool _useSSL = false; bool _isFailover = false; bool _trusted = false; + bool _forceNoTor = false; int? port; late bool enableSSLCheckbox; late TorPlainNetworkOption netOption; + bool _isPrimary = false; late final bool enableAuthFields; @@ -864,6 +878,9 @@ class _NodeFormState extends ConsumerState { ref.read(nodeFormDataProvider).isFailover = _isFailover; ref.read(nodeFormDataProvider).trusted = _trusted; ref.read(nodeFormDataProvider).netOption = netOption; + ref.read(nodeFormDataProvider).forceNoTor = _forceNoTor; + ref.read(nodeFormDataProvider).host; + ref.read(nodeFormDataProvider).isPrimary = _isPrimary; } @override @@ -898,6 +915,8 @@ class _NodeFormState extends ConsumerState { _useSSL = node.useSSL; _isFailover = node.isFailover; _trusted = node.trusted ?? false; + _forceNoTor = node.forceNoTor ?? false; + _isPrimary = node.isPrimary ?? false; if (node.torEnabled && !node.clearnetEnabled) { netOption = TorPlainNetworkOption.tor; @@ -965,24 +984,25 @@ class _NodeFormState extends ConsumerState { _nameFocusNode, context, ).copyWith( - suffixIcon: !shouldBeReadOnly && _nameController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _nameController.text = ""; - _updateState(); - }, - ), - ], + suffixIcon: + !shouldBeReadOnly && _nameController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _nameController.text = ""; + _updateState(); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), onChanged: (newValue) { _updateState(); @@ -990,9 +1010,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1011,26 +1029,59 @@ class _NodeFormState extends ConsumerState { _hostFocusNode, context, ).copyWith( - suffixIcon: !shouldBeReadOnly && _hostController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _hostController.text = ""; - _updateState(); - }, - ), - ], + suffixIcon: + !shouldBeReadOnly && _hostController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _hostController.text = ""; + _updateState(); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), onChanged: (newValue) { + // parse port hack + try { + final uri = Uri.parse(newValue); + final port = uri.hasPort ? uri.port : 0; + if (port != 0) { + _portController.text = port.toString(); + final noPortUri = Uri( + scheme: uri.scheme, + userInfo: uri.userInfo, + host: uri.host, + path: uri.path, + query: uri.hasQuery ? uri.query : null, + fragment: uri.fragment.isNotEmpty ? uri.fragment : null, + ); + _hostController.text = noPortUri.toString(); + } + } catch (_) { + if (newValue.contains(":")) { + final parts = newValue.split(":"); + if (parts.isNotEmpty) { + final maybePort = int.tryParse(parts.last); + if (maybePort != null) { + _portController.text = maybePort.toString(); + _hostController.text = newValue.substring( + 0, + newValue.lastIndexOf(":"), + ); + } + } + } + } + if (widget.coin is Epiccash) { if (newValue.startsWith("https://")) { _useSSL = true; @@ -1041,7 +1092,7 @@ class _NodeFormState extends ConsumerState { } else { enableSSLCheckbox = true; } - } else if (widget.coin is LibMoneroWallet) { + } else if (widget.coin is LibMoneroWallet || widget.coin is LibSalviumWallet) { if (newValue.startsWith("https://")) { _useSSL = true; } else if (newValue.startsWith("http://")) { @@ -1055,9 +1106,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1078,24 +1127,25 @@ class _NodeFormState extends ConsumerState { _portFocusNode, context, ).copyWith( - suffixIcon: !shouldBeReadOnly && _portController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _portController.text = ""; - _updateState(); - }, - ), - ], + suffixIcon: + !shouldBeReadOnly && _portController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _portController.text = ""; + _updateState(); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), onChanged: (newValue) { _updateState(); @@ -1103,9 +1153,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (enableAuthFields) ClipRRect( borderRadius: BorderRadius.circular( @@ -1127,21 +1175,21 @@ class _NodeFormState extends ConsumerState { suffixIcon: !shouldBeReadOnly && _usernameController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _usernameController.text = ""; - _updateState(); - }, - ), - ], - ), + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _usernameController.text = ""; + _updateState(); + }, + ), + ], ), - ) + ), + ) : null, ), onChanged: (newValue) { @@ -1150,10 +1198,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - if (enableAuthFields) - const SizedBox( - height: 8, - ), + if (enableAuthFields) const SizedBox(height: 8), if (enableAuthFields) ClipRRect( borderRadius: BorderRadius.circular( @@ -1176,21 +1221,21 @@ class _NodeFormState extends ConsumerState { suffixIcon: !shouldBeReadOnly && _passwordController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - _passwordController.text = ""; - _updateState(); - }, - ), - ], - ), + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + _passwordController.text = ""; + _updateState(); + }, + ), + ], ), - ) + ), + ) : null, ), onChanged: (newValue) { @@ -1199,22 +1244,20 @@ class _NodeFormState extends ConsumerState { }, ), ), - if (enableAuthFields) - const SizedBox( - height: 8, - ), + if (enableAuthFields) const SizedBox(height: 8), if (widget.coin is! CryptonoteCurrency) Row( children: [ GestureDetector( - onTap: !shouldBeReadOnly && enableSSLCheckbox - ? () { - setState(() { - _useSSL = !_useSSL; - }); - _updateState(); - } - : null, + onTap: + !shouldBeReadOnly && enableSSLCheckbox + ? () { + setState(() { + _useSSL = !_useSSL; + }); + _updateState(); + } + : null, child: Container( color: Colors.transparent, child: Row( @@ -1223,29 +1266,29 @@ class _NodeFormState extends ConsumerState { width: 20, height: 20, child: Checkbox( - fillColor: !shouldBeReadOnly && enableSSLCheckbox - ? null - : MaterialStateProperty.all( - Theme.of(context) - .extension()! - .checkboxBGDisabled, - ), + fillColor: + !shouldBeReadOnly && enableSSLCheckbox + ? null + : MaterialStateProperty.all( + Theme.of(context) + .extension()! + .checkboxBGDisabled, + ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _useSSL, - onChanged: !shouldBeReadOnly && enableSSLCheckbox - ? (newValue) { - setState(() { - _useSSL = newValue!; - }); - _updateState(); - } - : null, + onChanged: + !shouldBeReadOnly && enableSSLCheckbox + ? (newValue) { + setState(() { + _useSSL = newValue!; + }); + _updateState(); + } + : null, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( "Use SSL", style: STextStyles.itemSubtitle12(context), @@ -1256,18 +1299,19 @@ class _NodeFormState extends ConsumerState { ), ], ), - if (widget.coin is LibMoneroWallet) + if (widget.coin is LibMoneroWallet || widget.coin is LibSalviumWallet) Row( children: [ GestureDetector( - onTap: !widget.readOnly /*&& trustedCheckbox*/ - ? () { - setState(() { - _trusted = !_trusted; - }); - _updateState(); - } - : null, + onTap: + !widget.readOnly /*&& trustedCheckbox*/ + ? () { + setState(() { + _trusted = !_trusted; + }); + _updateState(); + } + : null, child: Container( color: Colors.transparent, child: Row( @@ -1276,29 +1320,29 @@ class _NodeFormState extends ConsumerState { width: 20, height: 20, child: Checkbox( - fillColor: !widget.readOnly - ? null - : MaterialStateProperty.all( - Theme.of(context) - .extension()! - .checkboxBGDisabled, - ), + fillColor: + !widget.readOnly + ? null + : MaterialStateProperty.all( + Theme.of(context) + .extension()! + .checkboxBGDisabled, + ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, value: _trusted, - onChanged: !widget.readOnly - ? (newValue) { - setState(() { - _trusted = newValue!; - }); - _updateState(); - } - : null, + onChanged: + !widget.readOnly + ? (newValue) { + setState(() { + _trusted = newValue!; + }); + _updateState(); + } + : null, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( "Trusted", style: STextStyles.itemSubtitle12(context), @@ -1310,9 +1354,7 @@ class _NodeFormState extends ConsumerState { ], ), if (widget.coin is! CryptonoteCurrency && widget.coin is! Epiccash) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (widget.coin is! CryptonoteCurrency && widget.coin is! Epiccash) Row( children: [ @@ -1322,7 +1364,9 @@ class _NodeFormState extends ConsumerState { _isFailover = !_isFailover; }); if (widget.readOnly) { - ref.read(nodeServiceChangeNotifierProvider).edit( + ref + .read(nodeServiceChangeNotifierProvider) + .save( widget.node!.copyWith( isFailover: _isFailover, loginName: widget.node!.loginName, @@ -1351,7 +1395,9 @@ class _NodeFormState extends ConsumerState { _isFailover = newValue!; }); if (widget.readOnly) { - ref.read(nodeServiceChangeNotifierProvider).edit( + ref + .read(nodeServiceChangeNotifierProvider) + .save( widget.node!.copyWith( isFailover: _isFailover, loginName: widget.node!.loginName, @@ -1366,9 +1412,7 @@ class _NodeFormState extends ConsumerState { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( "Use as failover", style: STextStyles.itemSubtitle12(context), @@ -1379,10 +1423,7 @@ class _NodeFormState extends ConsumerState { ), ], ), - if (widget.coin is! Ethereum) - const SizedBox( - height: 16, - ), + if (widget.coin is! Ethereum) const SizedBox(height: 16), if (widget.coin is! Ethereum) Row( children: [ @@ -1393,19 +1434,14 @@ class _NodeFormState extends ConsumerState { groupValue: netOption, onChanged: (value) { if (!widget.readOnly) { - setState( - () => netOption = TorPlainNetworkOption.tor, - ); + setState(() => netOption = TorPlainNetworkOption.tor); _updateState(); } }, ), ], ), - if (widget.coin is! Ethereum) - const SizedBox( - height: 8, - ), + if (widget.coin is! Ethereum) const SizedBox(height: 8), if (widget.coin is! Ethereum) Row( children: [ @@ -1416,19 +1452,14 @@ class _NodeFormState extends ConsumerState { groupValue: netOption, onChanged: (value) { if (!widget.readOnly) { - setState( - () => netOption = TorPlainNetworkOption.clear, - ); + setState(() => netOption = TorPlainNetworkOption.clear); _updateState(); } }, ), ], ), - if (widget.coin is! Ethereum) - const SizedBox( - height: 8, - ), + if (widget.coin is! Ethereum) const SizedBox(height: 8), if (widget.coin is! Ethereum) Row( children: [ @@ -1439,18 +1470,55 @@ class _NodeFormState extends ConsumerState { groupValue: netOption, onChanged: (value) { if (!widget.readOnly) { - setState( - () => netOption = TorPlainNetworkOption.both, - ); + setState(() => netOption = TorPlainNetworkOption.both); _updateState(); } }, ), ], ), + if (widget.coin is CryptonoteCurrency && _isLocalNode()) + const SizedBox(height: 8), + if (widget.coin is CryptonoteCurrency && _isLocalNode()) + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Checkbox( + fillColor: + !widget.readOnly + ? null + : MaterialStateProperty.all( + Theme.of( + context, + ).extension()!.checkboxBGDisabled, + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _forceNoTor, + onChanged: + !widget.readOnly + ? (newValue) { + setState(() { + _forceNoTor = newValue!; + }); + _updateState(); + } + : null, + ), + ), + const SizedBox(width: 12), + Text("Bypass TOR", style: STextStyles.itemSubtitle12(context)), + ], + ), ], ); } + + bool _isLocalNode() { + final host = _hostController.text.toLowerCase(); + return host.contains("127.0.0.1") || host.contains("localhost"); + } } class RadioTextButton extends StatelessWidget { @@ -1473,10 +1541,9 @@ class RadioTextButton extends StatelessWidget { Widget build(BuildContext context) { return ConditionalParent( condition: Util.isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), child: GestureDetector( onTap: () { if (value != groupValue) { @@ -1493,27 +1560,24 @@ class RadioTextButton extends StatelessWidget { width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of( + context, + ).extension()!.radioButtonIconEnabled, value: value, groupValue: groupValue, - onChanged: !enabled - ? null - : (_) { - if (value != groupValue) { - onChanged.call(value); - } - }, + onChanged: + !enabled + ? null + : (_) { + if (value != groupValue) { + onChanged.call(value); + } + }, ), ), - const SizedBox( - width: 14, - ), - Text( - label, - style: STextStyles.w500_14(context), - ), + const SizedBox(width: 14), + Text(label, style: STextStyles.w500_14(context)), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart index a7756f662..8d8c1bc7b 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart @@ -27,9 +27,7 @@ import '../../../../widgets/rounded_white_container.dart'; import 'coin_nodes_view.dart'; class ManageNodesView extends ConsumerStatefulWidget { - const ManageNodesView({ - super.key, - }); + const ManageNodesView({super.key}); static const String routeName = "/manageNodes"; @@ -43,9 +41,7 @@ class _ManageNodesViewState extends ConsumerState { @override void initState() { _coins = _coins.toList(); - _coins.removeWhere( - (e) => e is Firo && e.network.isTestNet, - ); + _coins.removeWhere((e) => e is Firo && e.network.isTestNet); super.initState(); } @@ -60,9 +56,10 @@ class _ManageNodesViewState extends ConsumerState { prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), ); - final coins = showTestNet - ? _coins - : _coins.where((e) => !e.network.isTestNet).toList(); + final coins = + showTestNet + ? _coins + : _coins.where((e) => !e.network.isTestNet).toList(); return Background( child: Scaffold( @@ -73,29 +70,24 @@ class _ManageNodesViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Manage nodes", - style: STextStyles.navBarTitle(context), - ), + title: Text("Manage nodes", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ...coins.map( - (coin) { - final count = ref - .watch( - nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin)), - ) - .length; + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 12, right: 12), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...coins.map((coin) { + final count = + ref + .watch( + nodeServiceChangeNotifierProvider.select( + (value) => value.getNodesFor(coin), + ), + ) + .length; return Padding( padding: const EdgeInsets.all(4), @@ -121,17 +113,11 @@ class _ManageNodesViewState extends ConsumerState { child: Row( children: [ SvgPicture.file( - File( - ref.watch( - coinIconProvider(coin), - ), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -151,9 +137,9 @@ class _ManageNodesViewState extends ConsumerState { ), ), ); - }, - ), - ], + }), + ], + ), ), ), ), 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 56c87e5c4..481c3906d 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 @@ -64,8 +64,10 @@ class _NodeDetailsViewState extends ConsumerState { bool _desktopReadOnly = true; Future _notifyWalletsOfUpdatedNode() async { - final wallets = - ref.read(pWallets).wallets.where((e) => e.info.coin == widget.coin); + final wallets = ref + .read(pWallets) + .wallets + .where((e) => e.info.coin == widget.coin); final prefs = ref.read(prefsChangeNotifierProvider); switch (prefs.syncType) { @@ -110,139 +112,140 @@ class _NodeDetailsViewState extends ConsumerState { final isDesktop = Util.isDesktop; final node = ref.watch( - nodeServiceChangeNotifierProvider - .select((value) => value.getNodeById(id: nodeId)), + nodeServiceChangeNotifierProvider.select( + (value) => value.getNodeById(id: nodeId), + ), ); final nodesForCoin = ref.watch( - nodeServiceChangeNotifierProvider - .select((value) => value.getNodesFor(coin)), + nodeServiceChangeNotifierProvider.select( + (value) => value.getNodesFor(coin), + ), ); final canDelete = nodesForCoin.length > 1; return ConditionalParent( condition: !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: 75)); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Node details", - style: STextStyles.navBarTitle(context), - ), - actions: [ - // if (!nodeId.startsWith(DefaultNodes.defaultNodeIdPrefix)) - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, + 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: 75), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Node details", + style: STextStyles.navBarTitle(context), ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("nodeDetailsEditNodeAppBarButtonKey"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension()!.background, - icon: SvgPicture.asset( - Assets.svg.pencil, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, + actions: [ + // if (!nodeId.startsWith(DefaultNodes.defaultNodeIdPrefix)) + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, ), - onPressed: () { - Navigator.of(context).pushNamed( - AddEditNodeView.routeName, - arguments: Tuple4( - AddEditNodeViewType.edit, - coin, - nodeId, - popRouteName, + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("nodeDetailsEditNodeAppBarButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.pencil, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AddEditNodeView.routeName, + arguments: Tuple4( + AddEditNodeViewType.edit, + coin, + nodeId, + popRouteName, + ), + ); + }, + ), + ), + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 12, right: 12), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 8, + ), + child: IntrinsicHeight(child: child), + ), ), ); }, ), ), ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 12, - right: 12, - ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight - 8), - child: IntrinsicHeight( - child: child, - ), - ), - ), - ); - }, ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( + builder: + (child) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - width: 8, - ), - const AppBarBackButton( - iconSize: 24, - size: 40, + Row( + children: [ + const SizedBox(width: 8), + const AppBarBackButton(iconSize: 24, size: 40), + Text( + "Node details", + style: STextStyles.desktopH3(context), + ), + ], ), - Text( - "Node details", - style: STextStyles.desktopH3(context), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + top: 16, + bottom: 32, + ), + child: child, ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - top: 16, - bottom: 32, - ), - child: child, - ), - ], - ), - ), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -253,44 +256,35 @@ class _NodeDetailsViewState extends ConsumerState { coin: coin, ), if (!isDesktop) const Spacer(), - if (isDesktop) - const SizedBox( - height: 22, - ), + if (isDesktop) const SizedBox(height: 22), if (isDesktop && canDelete) SizedBox( height: 56, - child: _desktopReadOnly - ? null - : Row( - children: [ - Expanded( - child: DeleteButton( - label: "Delete node", - desktopMed: true, - onPressed: () async { - Navigator.of(context).pop(); + child: + _desktopReadOnly + ? null + : Row( + children: [ + Expanded( + child: DeleteButton( + label: "Delete node", + desktopMed: true, + onPressed: () async { + Navigator.of(context).pop(); - await ref - .read(nodeServiceChangeNotifierProvider) - .delete( - node!.id, - true, - ); - }, + await ref + .read(nodeServiceChangeNotifierProvider) + .delete(node!.id, true); + }, + ), ), - ), - const SizedBox( - width: 16, - ), - const Spacer(), - ], - ), + const SizedBox(width: 16), + const Spacer(), + ], + ), ), if (isDesktop && !_desktopReadOnly && canDelete) - const SizedBox( - height: 45, - ), + const SizedBox(height: 45), Row( children: [ Expanded( @@ -298,9 +292,10 @@ class _NodeDetailsViewState extends ConsumerState { label: "Test connection", buttonHeight: isDesktop ? ButtonHeight.l : null, onPressed: () async { - final node = ref - .read(nodeServiceChangeNotifierProvider) - .getNodeById(id: nodeId)!; + final node = + ref + .read(nodeServiceChangeNotifierProvider) + .getNodeById(id: nodeId)!; final TorPlainNetworkOption netOption; if (ref.read(nodeFormDataProvider).netOption != null) { @@ -312,15 +307,17 @@ class _NodeDetailsViewState extends ConsumerState { ); } - final nodeFormData = NodeFormData() - ..useSSL = node.useSSL - ..trusted = node.trusted - ..name = node.name - ..host = node.host - ..login = node.loginName - ..port = node.port - ..isFailover = node.isFailover - ..netOption = netOption; + final nodeFormData = + NodeFormData() + ..useSSL = node.useSSL + ..trusted = node.trusted + ..name = node.name + ..host = node.host + ..login = node.loginName + ..port = node.port + ..isFailover = node.isFailover + ..netOption = netOption + ..forceNoTor = node.forceNoTor; nodeFormData.password = await node.getPassword( ref.read(secureStoreProvider), ); @@ -358,16 +355,13 @@ class _NodeDetailsViewState extends ConsumerState { }, ), ), - if (isDesktop) - const SizedBox( - width: 16, - ), + if (isDesktop) const SizedBox(width: 16), if (isDesktop) Expanded( child: - // !nodeId.startsWith(DefaultNodes.defaultNodeIdPrefix) - // ? - PrimaryButton( + // !nodeId.startsWith(DefaultNodes.defaultNodeIdPrefix) + // ? + PrimaryButton( label: _desktopReadOnly ? "Edit" : "Save", buttonHeight: ButtonHeight.l, onPressed: () async { @@ -388,19 +382,21 @@ class _NodeDetailsViewState extends ConsumerState { ref.read(nodeFormDataProvider).isFailover, torEnabled: ref.read(nodeFormDataProvider).netOption == - TorPlainNetworkOption.tor || - ref.read(nodeFormDataProvider).netOption == - TorPlainNetworkOption.both, + TorPlainNetworkOption.tor || + ref.read(nodeFormDataProvider).netOption == + TorPlainNetworkOption.both, clearnetEnabled: ref.read(nodeFormDataProvider).netOption == - TorPlainNetworkOption.clear || - ref.read(nodeFormDataProvider).netOption == - TorPlainNetworkOption.both, + TorPlainNetworkOption.clear || + ref.read(nodeFormDataProvider).netOption == + TorPlainNetworkOption.both, + forceNoTor: + ref.read(nodeFormDataProvider).forceNoTor, ); await ref .read(nodeServiceChangeNotifierProvider) - .edit( + .save( editedNode, ref.read(nodeFormDataProvider).password, true, @@ -408,16 +404,12 @@ class _NodeDetailsViewState extends ConsumerState { await _notifyWalletsOfUpdatedNode(); } }, - ) + ), // : Container() - , ), ], ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart b/lib/pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart new file mode 100644 index 000000000..924b39189 --- /dev/null +++ b/lib/pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../providers/providers.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/util.dart'; +import '../../../../widgets/background.dart'; +import '../../../../widgets/conditional_parent.dart'; +import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/rounded_white_container.dart'; + +class AutoLockTimeoutSettingsView extends ConsumerStatefulWidget { + const AutoLockTimeoutSettingsView({super.key}); + + static const routeName = "/autoLockTimeoutSettingsView"; + + @override + ConsumerState createState() => + _AutoLockTimeoutSettingsViewState(); +} + +class _AutoLockTimeoutSettingsViewState + extends ConsumerState { + final isDesktop = Util.isDesktop; + final TextEditingController _timeController = TextEditingController(); + late bool _enabled; + bool _lock = false; + + Future _save() async { + if (_lock) return; + _lock = true; + + try { + final minutes = int.tryParse(_timeController.text); + + if (minutes == null) { + // this should not hit unless logic in validating text field input is + // wrong + return; + } + + ref.read(prefsChangeNotifierProvider).autoLockInfo = ( + enabled: _enabled, + minutes: minutes, + ); + + Navigator.of(context, rootNavigator: isDesktop).pop(); + } finally { + _lock = false; + } + } + + int _minutesCache = 1; + + int _clampMinutes(int input) { + if (input > 60) return 60; + if (input < 1) return 1; + return input; + } + + @override + void initState() { + super.initState(); + _enabled = ref.read(prefsChangeNotifierProvider).autoLockInfo.enabled; + _minutesCache = _clampMinutes( + ref.read(prefsChangeNotifierProvider).autoLockInfo.minutes, + ); + _timeController.text = _minutesCache.toString(); + } + + @override + void dispose() { + _timeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !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: 70), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + 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.stretch, + children: [ + RoundedWhiteContainer( + child: RawMaterialButton( + splashColor: + Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Toggle auto lock", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: _enabled, + onValueChanged: (newValue) { + _enabled = newValue; + }, + ), + ), + ], + ), + ), + ), + ), + SizedBox(height: isDesktop ? 24 : 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Text("Minutes", style: STextStyles.titleBold12(context)), + const SizedBox(width: 16), + Flexible( + child: TextField( + controller: _timeController, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + inputFormatters: [ + TextInputFormatter.withFunction( + (oldValue, newValue) => + RegExp(r'^([0-9]*)$').hasMatch(newValue.text) + ? newValue + : oldValue, + ), + ], + onChanged: (value) { + final number = int.tryParse(value); + if (number == null || number < 1 || number > 60) { + _timeController.text = _minutesCache.toString(); + return; + } + + _minutesCache = _clampMinutes(number); + }, + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + decoration: InputDecoration( + hintText: "Minutes", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + ), + ], + ), + ), + SizedBox(height: isDesktop ? 40 : 16), + if (!isDesktop) const Spacer(), + ConditionalParent( + condition: isDesktop, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [child], + ), + child: PrimaryButton( + buttonHeight: isDesktop ? ButtonHeight.l : null, + width: isDesktop ? 200 : null, + label: "Save", + onPressed: _save, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index 57fa710ad..0afaecb7c 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -8,12 +8,14 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../../notifications/show_flush_bar.dart'; -import '../../../../../providers/global/prefs_provider.dart'; import '../../../../../providers/global/secure_store_provider.dart'; +import '../../../../../providers/providers.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/assets.dart'; import '../../../../../utilities/flutter_secure_storage_interface.dart'; @@ -21,12 +23,11 @@ import '../../../../../utilities/text_styles.dart'; import '../../../../../widgets/background.dart'; import '../../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../../widgets/custom_pin_put/custom_pin_put.dart'; +import '../../../../pinpad_views/lock_screen_view.dart'; import '../security_view.dart'; class ChangePinView extends ConsumerStatefulWidget { - const ChangePinView({ - super.key, - }); + const ChangePinView({super.key}); static const String routeName = "/changePin"; @@ -46,8 +47,10 @@ class _ChangePinViewState extends ConsumerState { ); } - final PageController _pageController = - PageController(initialPage: 0, keepPage: true); + final PageController _pageController = PageController( + initialPage: 0, + keepPage: true, + ); // Attributes for Page 1 of the page view final TextEditingController _pinPutController1 = TextEditingController(); @@ -110,16 +113,12 @@ class _ChangePinViewState extends ConsumerState { style: STextStyles.pageTitleH1(context), ), ), - const SizedBox( - height: 52, - ), + const SizedBox(height: 52), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, - textStyle: STextStyles.label(context).copyWith( - fontSize: 1, - ), + textStyle: STextStyles.label(context).copyWith(fontSize: 1), focusNode: _pinPutFocusNode1, controller: _pinPutController1, useNativeKeyboard: false, @@ -131,9 +130,10 @@ class _ChangePinViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, + fillColor: + Theme.of( + context, + ).extension()!.background, counterText: "", ), isRandom: @@ -161,7 +161,6 @@ class _ChangePinViewState extends ConsumerState { ), // page 2 - Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -171,17 +170,16 @@ class _ChangePinViewState extends ConsumerState { style: STextStyles.pageTitleH1(context), ), ), - const SizedBox( - height: 52, - ), + const SizedBox(height: 52), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, textStyle: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, + color: + Theme.of( + context, + ).extension()!.textSubtitle3, fontSize: 1, ), focusNode: _pinPutFocusNode2, @@ -195,9 +193,10 @@ class _ChangePinViewState extends ConsumerState { disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, - fillColor: Theme.of(context) - .extension()! - .background, + fillColor: + Theme.of( + context, + ).extension()!.background, counterText: "", ), isRandom: @@ -207,40 +206,58 @@ class _ChangePinViewState extends ConsumerState { followingFieldDecoration: _pinPutDecoration, onSubmit: (String pin) async { if (_pinPutController1.text == _pinPutController2.text) { - // This should never fail as we are overwriting the existing pin - assert( - (await _secureStore.read(key: "stack_pin")) != null, - ); - await _secureStore.write(key: "stack_pin", value: pin); + final isDuress = ref.read(pDuress); - showFloatingFlushBar( - type: FlushBarType.success, - message: "New PIN is set up", - context: context, - iconAsset: Assets.svg.check, - ); + if (isDuress) { + await _secureStore.write( + key: kDuressPinKey, + value: pin, + ); + } else { + // This should never fail as we are overwriting the existing pin + assert( + (await _secureStore.read(key: kPinKey)) != null, + ); + await _secureStore.write(key: kPinKey, value: pin); + } - await Future.delayed( - const Duration(milliseconds: 1200), - ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: + "New${isDuress ? " duress" : ""} PIN is set up", + context: context, + iconAsset: Assets.svg.check, + ), + ); - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(SecurityView.routeName), + await Future.delayed( + const Duration(milliseconds: 1200), ); + + if (context.mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(SecurityView.routeName), + ); + } } } else { - _pageController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.linear, + unawaited( + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ), ); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "PIN codes do not match. Try again.", - context: context, - iconAsset: Assets.svg.alertCircle, + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ), ); _pinPutController1.text = ''; diff --git a/lib/pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart new file mode 100644 index 000000000..7db6dbd42 --- /dev/null +++ b/lib/pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../../notifications/show_flush_bar.dart'; +import '../../../../../providers/global/prefs_provider.dart'; +import '../../../../../providers/global/secure_store_provider.dart'; +import '../../../../../themes/stack_colors.dart'; +import '../../../../../utilities/assets.dart'; +import '../../../../../utilities/flutter_secure_storage_interface.dart'; +import '../../../../../utilities/text_styles.dart'; +import '../../../../../widgets/background.dart'; +import '../../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../../widgets/custom_pin_put/custom_pin_put.dart'; +import '../../../pinpad_views/lock_screen_view.dart'; +import 'security_view.dart'; + +class CreateDuressPinView extends ConsumerStatefulWidget { + const CreateDuressPinView({super.key}); + + static const String routeName = "/createDuressPinView"; + + @override + ConsumerState createState() => + _CreateDuressPinViewState(); +} + +class _CreateDuressPinViewState extends ConsumerState { + BoxDecoration get _pinPutDecoration { + return BoxDecoration( + color: Theme.of(context).extension()!.infoItemIcons, + border: Border.all( + width: 1, + color: Theme.of(context).extension()!.infoItemIcons, + ), + borderRadius: BorderRadius.circular(6), + ); + } + + final PageController _pageController = PageController( + initialPage: 0, + keepPage: true, + ); + + // Attributes for Page 1 of the page view + final TextEditingController _pinPutController1 = TextEditingController(); + final FocusNode _pinPutFocusNode1 = FocusNode(); + + // Attributes for Page 2 of the page view + final TextEditingController _pinPutController2 = TextEditingController(); + final FocusNode _pinPutFocusNode2 = FocusNode(); + + late final SecureStorageInterface _secureStore; + + int pinCount = 1; + + @override + void initState() { + super.initState(); + _secureStore = ref.read(secureStoreProvider); + } + + @override + void dispose() { + _pageController.dispose(); + _pinPutController1.dispose(); + _pinPutController2.dispose(); + _pinPutFocusNode1.dispose(); + _pinPutFocusNode2.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return 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: 70)); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + body: SafeArea( + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + // page 1 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Create duress PIN", + style: STextStyles.pageTitleH1(context), + ), + ), + const SizedBox(height: 52), + CustomPinPut( + fieldsCount: pinCount, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.label(context).copyWith(fontSize: 1), + focusNode: _pinPutFocusNode1, + controller: _pinPutController1, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: + Theme.of( + context, + ).extension()!.background, + counterText: "", + ), + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, + submittedFieldDecoration: _pinPutDecoration, + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) { + if (pin.length < 4) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN not long enough!", + iconAsset: Assets.svg.alertCircle, + context: context, + ); + } else { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); + } + }, + ), + ], + ), + + // page 2 + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + "Confirm duress PIN", + style: STextStyles.pageTitleH1(context), + ), + ), + const SizedBox(height: 52), + CustomPinPut( + fieldsCount: pinCount, + eachFieldHeight: 12, + eachFieldWidth: 12, + textStyle: STextStyles.infoSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle3, + fontSize: 1, + ), + focusNode: _pinPutFocusNode2, + controller: _pinPutController2, + useNativeKeyboard: false, + obscureText: "", + inputDecoration: InputDecoration( + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: + Theme.of( + context, + ).extension()!.background, + counterText: "", + ), + isRandom: + ref.read(prefsChangeNotifierProvider).randomizePIN, + submittedFieldDecoration: _pinPutDecoration, + selectedFieldDecoration: _pinPutDecoration, + followingFieldDecoration: _pinPutDecoration, + onSubmit: (String pin) async { + if (_pinPutController1.text == _pinPutController2.text) { + await _secureStore.write( + key: kDuressPinKey, + value: pin, + ); + ref.read(prefsChangeNotifierProvider).hasDuressPin = + true; + + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Duress PIN is set up", + context: context, + iconAsset: Assets.svg.check, + ), + ); + } + + await Future.delayed( + const Duration(milliseconds: 1200), + ); + + if (context.mounted) { + Navigator.of(context).popUntil( + ModalRoute.withName(SecurityView.routeName), + ); + } + } else { + unawaited( + _pageController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "PIN codes do not match. Try again.", + context: context, + iconAsset: Assets.svg.alertCircle, + ), + ); + + _pinPutController1.text = ''; + _pinPutController2.text = ''; + } + }, + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index e80374320..76331b0bb 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -11,25 +11,193 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../providers/global/duress_provider.dart'; import '../../../../providers/global/prefs_provider.dart'; +import '../../../../providers/global/secure_store_provider.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/constants.dart'; +import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/rounded_white_container.dart'; +import '../../../../widgets/stack_dialog.dart'; import '../../../pinpad_views/lock_screen_view.dart'; +import 'auto_lock_timeout_settings_view.dart'; import 'change_pin_view/change_pin_view.dart'; +import 'create_duress_pin_view.dart'; -class SecurityView extends StatelessWidget { - const SecurityView({ - super.key, - }); +class SecurityView extends ConsumerStatefulWidget { + const SecurityView({super.key}); static const String routeName = "/security"; + @override + ConsumerState createState() => _SecurityViewState(); +} + +class _SecurityViewState extends ConsumerState { + bool _lock = false; + + Future _duressToggled(bool newValue) async { + if (_lock) return; + try { + _lock = true; + + if (newValue) { + await _createDuressPin(); + } else { + await _deleteDuressPin(); + } + } finally { + _lock = false; + } + } + + Future _createDuressPin() async { + final result = await showDialog( + context: context, + builder: + (context) => StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enable duress PIN", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox(height: 8), + Row( + children: [ + Flexible( + child: Text( + "When unlocking the app with a duress PIN, only wallets" + " marked as visible in duress mode will be loaded and" + " shown. Be aware that providing a duress PIN instead" + " of your real PIN to law enforcement, border agents," + " or other authorities may be considered deception and" + " could carry legal consequences depending on your" + " jurisdiction. Use with care and according to your" + " threat model.", + style: STextStyles.smallMed14(context), + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: () => Navigator.of(context).pop(false), + ), + ), + const SizedBox(width: 8), + Expanded( + child: PrimaryButton( + label: "Ok", + onPressed: () => Navigator.of(context).pop(true), + ), + ), + ], + ), + ], + ), + ), + ); + + if (result == true && mounted) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: CreateDuressPinView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: "Authenticate to create duress PIN", + biometricsAuthenticationTitle: "Create duress PIN", + ), + settings: const RouteSettings(name: "/createDuressPinLockscreen"), + ), + ); + } + } + + Future _deleteDuressPin() async { + await showDialog( + context: context, + builder: + (context) => StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Disable duress PIN", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox(height: 8), + Row( + children: [ + Flexible( + child: Text( + "Your duress pin will be deleted. " + "You will be asked to create a PIN when you enable this again. " + "Are you sure you want to continue?", + + style: STextStyles.smallMed14(context), + ), + ), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + const SizedBox(width: 8), + Expanded( + child: PrimaryButton( + label: "Ok", + onPressed: () async { + try { + await ref + .read(secureStoreProvider) + .delete(key: kDuressPinKey); + } catch (e, s) { + Logging.instance.f( + "dpin delete failed!!", + error: e, + stackTrace: s, + ); + } + + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + ], + ), + ], + ), + ), + ); + + ref.read(prefsChangeNotifierProvider).hasDuressPin = false; + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -43,216 +211,367 @@ class SecurityView extends StatelessWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Security", - style: STextStyles.navBarTitle(context), - ), + title: Text("Security", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => const LockscreenView( - showBackButton: true, - routeOnSuccess: ChangePinView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to change PIN", - biometricsAuthenticationTitle: "Change PIN", - ), - settings: - const RouteSettings(name: "/changepinlockscreen"), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, ), - child: Row( - children: [ - Text( - "Change PIN", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, + onPressed: () { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: ChangePinView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to change PIN", + biometricsAuthenticationTitle: "Change PIN", + ), + settings: const RouteSettings( + name: "/changepinlockscreen", + ), ), - ], + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Change PIN", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - // () { - // final useBio = - // ref.read(prefsChangeNotifierProvider).useBiometrics; - // - // debugPrint("useBio: $useBio"); - // ref.read(prefsChangeNotifierProvider).useBiometrics = - // !useBio; - // - // debugPrint( - // "useBio set to: ${ref.read(prefsChangeNotifierProvider).useBiometrics}"); - // }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Enable biometric authentication", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.useBiometrics), + onPressed: null, + // () { + // final useBio = + // ref.read(prefsChangeNotifierProvider).useBiometrics; + // + // debugPrint("useBio: $useBio"); + // ref.read(prefsChangeNotifierProvider).useBiometrics = + // !useBio; + // + // debugPrint( + // "useBio set to: ${ref.read(prefsChangeNotifierProvider).useBiometrics}"); + // }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable biometric authentication", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.useBiometrics, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .useBiometrics = newValue; + }, ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .useBiometrics = newValue; - }, ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Randomize PIN Pad", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.randomizePIN), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Randomize PIN Pad", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.randomizePIN, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .randomizePIN = newValue; + }, ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .randomizePIN = newValue; - }, ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - // The "autoPin" preference (whether to automatically accept a correct PIN). - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + // The "autoPin" preference (whether to automatically accept a correct PIN). + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Auto-accept correct PIN", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.autoPin, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .autoPin = newValue; + }, + ), + ), + ], + ), ), + ); + }, + ), + ), + if (!ref.watch(pDuress)) const SizedBox(height: 8), + if (!ref.watch(pDuress)) + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Duress PIN", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitch( + value: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.hasDuressPin, + ), + ), + onChanged: _duressToggled, + ), + ), + ], + ), + ), + ); + }, + ), + ), + if (!ref.watch(pDuress) && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.hasDuressPin, + ), + )) + const SizedBox(height: 8), + if (!ref.watch(pDuress) && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.hasDuressPin, ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Auto-accept correct PIN", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, + )) + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.autoPin), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Biometrics opens duress", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .autoPin = newValue; - }, - ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitch( + value: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.biometricsDuress, + ), + ), + onChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .biometricsDuress = newValue; + }, + ), + ), + ], ), - ], + ), + ); + }, + ), + ), + const SizedBox(height: 8), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: true, + routeOnSuccess: + AutoLockTimeoutSettingsView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to change auto lock settings", + biometricsAuthenticationTitle: + "Auto lock settings", + ), + settings: const RouteSettings( + name: "/autoLockTimeoutSettingsLockScreen", + ), ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Auto lock settings", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], ), - ); - }, + ), + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart index f41891bb6..57785f5f4 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart @@ -95,9 +95,9 @@ class _AutoBackupViewState extends ConsumerState { title: "Enable Auto Backup", message: "To enable Auto Backup, you need to create a backup file.", leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), + style: Theme.of( + context, + ).extension()!.getSecondaryEnabledButtonStyle(context), child: Text( "Back", style: STextStyles.button(context).copyWith( @@ -110,13 +110,10 @@ class _AutoBackupViewState extends ConsumerState { }, ), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), + child: Text("Continue", style: STextStyles.button(context)), onPressed: () { Navigator.of(context).pop(true); }, @@ -126,9 +123,9 @@ class _AutoBackupViewState extends ConsumerState { ); if (mounted) { if (result is bool && result) { - Navigator.of(context) - .pushNamed(CreateAutoBackupView.routeName) - .then((_) { + Navigator.of(context).pushNamed(CreateAutoBackupView.routeName).then(( + _, + ) { // set toggle to correct state if (_toggle != ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) { @@ -152,9 +149,9 @@ class _AutoBackupViewState extends ConsumerState { message: "You are turning off Auto Backup. You can turn it back on at any time. Your previous Auto Backup file will not be deleted. Remember to backup your wallets manually so you don't lose important information.", leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), + style: Theme.of( + context, + ).extension()!.getSecondaryEnabledButtonStyle(context), child: Text( "Back", style: STextStyles.button(context).copyWith( @@ -167,13 +164,10 @@ class _AutoBackupViewState extends ConsumerState { }, ), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Disable", - style: STextStyles.button(context), - ), + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), + child: Text("Disable", style: STextStyles.button(context)), onPressed: () { Navigator.of(context).pop(true); }, @@ -184,8 +178,9 @@ class _AutoBackupViewState extends ConsumerState { if (mounted) { if (result is bool && result) { ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled = false; - Navigator.of(context) - .popUntil(ModalRoute.withName(AutoBackupView.routeName)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(AutoBackupView.routeName)); } else { toggleController.activate?.call(); } @@ -233,11 +228,11 @@ class _AutoBackupViewState extends ConsumerState { ); ref.listen( - prefsChangeNotifierProvider - .select((value) => value.backupFrequencyType), - (previous, BackupFrequencyType next) { - frequencyController.text = Format.prettyFrequencyType(next); - }); + prefsChangeNotifierProvider.select((value) => value.backupFrequencyType), + (previous, BackupFrequencyType next) { + frequencyController.text = Format.prettyFrequencyType(next); + }, + ); return Background( child: Scaffold( @@ -248,232 +243,220 @@ class _AutoBackupViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Auto Backup", - style: STextStyles.navBarTitle(context), - ), + title: Text("Auto Backup", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Auto Backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - key: const Key("autoBackupToggleButtonKey"), - isOn: _toggle, - controller: toggleController, - onValueChanged: (newValue) async { - _toggle = newValue; + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Auto Backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + key: const Key("autoBackupToggleButtonKey"), + isOn: _toggle, + controller: toggleController, + onValueChanged: (newValue) async { + _toggle = newValue; - if (_toggle) { - attemptEnable(); - } else { - attemptDisable(); - } - }, + if (_toggle) { + attemptEnable(); + } else { + attemptDisable(); + } + }, + ), ), - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - if (!isEnabledAutoBackup) - RoundedWhiteContainer( - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: STextStyles.label(context), - children: [ - const TextSpan( - text: - "Auto Backup is a custom ${AppConfig.appName} feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website ", - ), - TextSpan( - text: "stackwallet.com.", - style: STextStyles.richLink(context), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse("https://stackwallet.com"), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], + ], + ), ), ), ), - if (isEnabledAutoBackup) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RoundedWhiteContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + const SizedBox(height: 8), + if (!isEnabledAutoBackup) + RoundedWhiteContainer( + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: STextStyles.label(context), children: [ - CustomTextButton( - text: "Back up now", - onTap: () { - ref.read(autoSWBServiceProvider).doBackup(); - }, + const TextSpan( + text: + "Auto Backup is a custom ${AppConfig.appName} feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website ", ), - Text( - "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", - style: STextStyles.itemSubtitle(context), + TextSpan( + text: "stackwallet.com.", + style: STextStyles.richLink(context), + recognizer: + TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse("https://stackwallet.com"), + mode: LaunchMode.externalApplication, + ); + }, ), ], ), ), - const SizedBox( - height: 32, - ), - Text( - "Auto Backup file", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + ), + if (isEnabledAutoBackup) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomTextButton( + text: "Back up now", + onTap: () { + ref.read(autoSWBServiceProvider).doBackup(); + }, + ), + Text( + "Backed up ${prettySinceLastBackupString(ref.watch(prefsChangeNotifierProvider.select((value) => value.lastAutoBackup)))}", + style: STextStyles.itemSubtitle(context), + ), + ], + ), ), - child: TextField( - key: const Key("backupSavedToFileLocationTextFieldKey"), - focusNode: fileLocationFocusNode, - controller: fileLocationController, - enabled: false, - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark - .withOpacity(0.5), + const SizedBox(height: 32), + Text( + "Auto Backup file", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - readOnly: true, - enableSuggestions: false, - autocorrect: false, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: true, + child: TextField( + key: const Key( + "backupSavedToFileLocationTextFieldKey", + ), + focusNode: fileLocationFocusNode, + controller: fileLocationController, + enabled: false, + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark + .withOpacity(0.5), + ), + readOnly: true, + enableSuggestions: false, + autocorrect: false, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: true, + ), + decoration: standardInputDecoration( + "Saved to", + fileLocationFocusNode, + context, + ), ), - decoration: standardInputDecoration( - "Saved to", - fileLocationFocusNode, - context, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("backupPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + enabled: false, + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark + .withOpacity(0.5), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: true, + ), + decoration: standardInputDecoration( + "Passphrase", + passwordFocusNode, + context, + ), ), ), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 12), + Text( + "Auto Backup frequency", + style: STextStyles.smallMed12(context), ), - child: TextField( - key: const Key("backupPasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, + const SizedBox(height: 10), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("backupFrequencyFieldKey"), + controller: frequencyController, enabled: false, style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.textDark.withOpacity(0.5), ), - obscureText: true, - enableSuggestions: false, - autocorrect: false, toolbarOptions: const ToolbarOptions( copy: true, cut: false, paste: false, selectAll: true, ), - decoration: standardInputDecoration( - "Passphrase", - passwordFocusNode, - context, - ), ), - ), - const SizedBox( - height: 12, - ), - Text( - "Auto Backup frequency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - key: const Key("backupFrequencyFieldKey"), - controller: frequencyController, - enabled: false, - style: STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark - .withOpacity(0.5), - ), - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: true, - ), - ), - const SizedBox( - height: 20, - ), - Center( - child: CustomTextButton( - text: "Edit Auto Backup", - onTap: () async { - Navigator.of(context) - .pushNamed(EditAutoBackupView.routeName); - }, + const SizedBox(height: 20), + Center( + child: CustomTextButton( + text: "Edit Auto Backup", + onTap: () async { + Navigator.of( + context, + ).pushNamed(EditAutoBackupView.routeName); + }, + ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ), 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 c75ac4a83..5616ccd9d 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 @@ -41,9 +41,7 @@ import 'helpers/swb_file_system.dart'; import 'sub_views/backup_frequency_type_select_sheet.dart'; class CreateAutoBackupView extends ConsumerStatefulWidget { - const CreateAutoBackupView({ - super.key, - }); + const CreateAutoBackupView({super.key}); static const String routeName = "/createAutoBackup"; @@ -134,567 +132,587 @@ class _EnableAutoBackupViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Create your backup file", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - if (!Platform.isAndroid && !Platform.isIOS) - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid || Platform.isIOS - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir(context); - } - - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance - .e("$e\n$s", error: e, stackTrace: s); - } - }, - controller: fileLocationController, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Save to...", - hintStyle: STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - SvgPicture.asset( - Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), - const SizedBox( - width: 12, - ), - ], - ), - ), - ), - key: const Key( - "createBackupSaveToFileLocationTextFieldKey", - ), - readOnly: true, - toolbarOptions: const ToolbarOptions( - copy: true, - cut: false, - paste: false, - selectAll: false, - ), - onChanged: (newValue) {}, - ), - if (!Platform.isAndroid && !Platform.isIOS) - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Create your backup file", + style: STextStyles.smallMed12(context), ), - child: TextField( - key: const Key("createBackupPasswordFieldKey1"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Create passphrase", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); + const SizedBox(height: 10), + if (!Platform.isAndroid && !Platform.isIOS) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + onTap: + Platform.isAndroid || Platform.isIOS + ? null + : () async { + try { + await stackFileSystem + .prepareStorage(); + + if (mounted) { + await stackFileSystem.pickDir( + context, + ); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance.e( + "$e\n$s", + error: e, + stackTrace: s, + ); + } }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + controller: fileLocationController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Save to...", + hintStyle: STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + SvgPicture.asset( + Assets.svg.folder, + color: + Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), - ), - const SizedBox( - width: 12, - ), - ], + const SizedBox(width: 12), + ], + ), ), ), + key: const Key( + "createBackupSaveToFileLocationTextFieldKey", + ), + readOnly: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: false, + paste: false, + selectAll: false, + ), + onChanged: (newValue) {}, + ), + if (!Platform.isAndroid && !Platform.isIOS) + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - onChanged: (newValue) { - if (newValue.isEmpty) { + child: TextField( + key: const Key("createBackupPasswordFieldKey1"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Create passphrase", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox(width: 12), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (final sug + in result.feedback.suggestions!.toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", + "phrases\nNo need", + ); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring( + 0, + feedback.length - 2, + ); + } + setState(() { - passwordFeedback = ""; + passwordFeedback = feedback; }); - return; - } - final result = zxcvbn.evaluate(newValue); - String suggestionsAndTips = ""; - for (final sug - in result.feedback.suggestions!.toSet()) { - suggestionsAndTips += "$sug\n"; - } - suggestionsAndTips += result.feedback.warning!; - String feedback = - // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" - suggestionsAndTips; - - passwordStrength = result.score! / 4; - - // hack fix to format back string returned from zxcvbn - if (feedback.contains("phrasesNo need")) { - feedback = feedback.replaceFirst( - "phrasesNo need", - "phrases\nNo need", - ); - } - - if (feedback.endsWith("\n")) { - feedback = - feedback.substring(0, feedback.length - 2); - } - - setState(() { - passwordFeedback = feedback; - }); - }, - ), - ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: EdgeInsets.only( - left: 12, - right: 12, - top: passwordFeedback.isNotEmpty ? 4 : 0, + }, ), - child: passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), - child: ProgressBar( - key: const Key("createStackBackUpProgressBar"), - width: - MediaQuery.of(context).size.width - 32 - 24, - height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension()! - .buttonBackSecondary, - percent: passwordStrength < 0.25 - ? 0.03 - : passwordStrength, + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: + passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, ), - ), - const SizedBox( - height: 10, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("createBackupPasswordFieldKey2"), - focusNode: passwordRepeatFocusNode, - controller: passwordRepeatController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm passphrase", - passwordRepeatFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "createBackupPasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, + ), + child: ProgressBar( + key: const Key("createStackBackUpProgressBar"), + width: + MediaQuery.of(context).size.width - 32 - 24, + height: 5, + fillColor: + passwordStrength < 0.51 + ? Theme.of(context) .extension()! - .textDark3, - width: 16, - height: 16, - ), - ), - const SizedBox( - width: 12, - ), - ], - ), + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + backgroundColor: + Theme.of(context) + .extension()! + .buttonBackSecondary, + percent: + passwordStrength < 0.25 + ? 0.03 + : passwordStrength, ), ), - onChanged: (newValue) { - setState(() {}); - // TODO: ? check if passwords match? - }, - ), - ), - const SizedBox( - height: 32, - ), - Text( - "Auto Backup frequency", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, - ), - Stack( - children: [ - TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - readOnly: true, - textInputAction: TextInputAction.none, + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - Positioned.fill( - child: RawMaterialButton( - splashColor: Theme.of(context) - .extension()! - .highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - showModalBottomSheet( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - const BackupFrequencyTypeSelectSheet(), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - ), + child: TextField( + key: const Key("createBackupPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm passphrase", + passwordRepeatFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, children: [ - Text( - Format.prettyFrequencyType( - ref.watch( - prefsChangeNotifierProvider.select( - (value) => - value.backupFrequencyType, - ), - ), + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "createBackupPasswordFieldShowPasswordButtonKey", ), - style: - STextStyles.itemSubtitle12(context), - ), - Padding( - padding: - const EdgeInsets.only(right: 4.0), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, child: SvgPicture.asset( - Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .textSubtitle2, - width: 12, - height: 6, + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension()! + .textDark3, + width: 16, + height: 16, ), ), + const SizedBox(width: 12), ], ), ), ), + onChanged: (newValue) { + setState(() {}); + // TODO: ? check if passwords match? + }, ), - ], - ), - const Spacer(), - const SizedBox( - height: 10, - ), - TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - onPressed: !shouldEnableCreate - ? null - : () async { - final String pathToSave = - fileLocationController.text; - final String passphrase = - passwordController.text; - final String repeatPassphrase = - passwordRepeatController.text; - - if (pathToSave.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory not chosen", - context: context, - ); - return; - } - if (!(await Directory(pathToSave).exists())) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Directory does not exist", - context: context, - ); - return; - } - if (passphrase.isEmpty) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "A passphrase is required", - context: context, - ); - return; - } - if (passphrase != repeatPassphrase) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Passphrase does not match", - context: context, - ); - return; - } - - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting initial backup", - message: "This shouldn't take long", + ), + const SizedBox(height: 32), + Text( + "Auto Backup frequency", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 10), + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Positioned.fill( + child: RawMaterialButton( + splashColor: + Theme.of( + context, + ).extension()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - - // make sure the dialog is able to be displayed for at least some time - final fut = Future.delayed( - const Duration(milliseconds: 300), - ); - - String adkString; - int adkVersion; - try { - final adk = - await compute(generateAdk, passphrase); - adkString = - Format.uint8listToString(adk.item2); - adkVersion = adk.item1; - } on Exception catch (e, s) { - final String err = - getErrorMessageFromSWBException(e); - Logging.instance - .e("$err\n$s", error: e, stackTrace: s); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: err, - context: context, - ); - return; - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - // pop encryption progress dialog - Navigator.of(context).pop(); - showFloatingFlushBar( - type: FlushBarType.warning, - message: "$e", + ), + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: + (_) => + const BackupFrequencyTypeSelectSheet(), ); - return; - } - - await secureStore.write( - key: "auto_adk_string", - value: adkString, - ); - await secureStore.write( - key: "auto_adk_version_string", - value: adkVersion.toString(), - ); - - final DateTime now = DateTime.now(); - final String fileToSave = - createAutoBackupFilename(pathToSave, now); - - final backup = - await SWB.createStackWalletJSON( - secureStorage: secureStore, - ); - - final bool result = - await SWB.encryptStackWalletWithADK( - fileToSave, - adkString, - jsonEncode(backup), - adkVersion, - ); - - // this future should already be complete unless there was an error encrypting - await Future.wait([fut]); - - if (mounted) { - // pop encryption progress dialog - Navigator.of(context).pop(); - - if (result) { - ref - .read(prefsChangeNotifierProvider) - .autoBackupLocation = pathToSave; - ref - .read(prefsChangeNotifierProvider) - .lastAutoBackup = now; - - ref - .read(prefsChangeNotifierProvider) - .isAutoBackupEnabled = true; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + Format.prettyFrequencyType( + ref.watch( + prefsChangeNotifierProvider.select( + (value) => + value.backupFrequencyType, + ), + ), + ), + style: STextStyles.itemSubtitle12( + context, + ), + ), + Padding( + padding: const EdgeInsets.only( + right: 4.0, + ), + child: SvgPicture.asset( + Assets.svg.chevronDown, + color: + Theme.of(context) + .extension()! + .textSubtitle2, + width: 12, + height: 6, + ), + ), + ], + ), + ), + ), + ), + ], + ), + const Spacer(), + const SizedBox(height: 10), + TextButton( + style: + shouldEnableCreate + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + onPressed: + !shouldEnableCreate + ? null + : () async { + final String pathToSave = + fileLocationController.text; + final String passphrase = + passwordController.text; + final String repeatPassphrase = + passwordRepeatController.text; + + if (pathToSave.isEmpty) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory not chosen", + context: context, + ); + return; + } + if (!(await Directory( + pathToSave, + ).exists())) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Directory does not exist", + context: context, + ); + return; + } + if (passphrase.isEmpty) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "A passphrase is required", + context: context, + ); + return; + } + if (passphrase != repeatPassphrase) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Passphrase does not match", + context: context, + ); + return; + } - await showDialog( + showDialog( context: context, barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: - "${AppConfig.prefix} Auto Backup enabled and saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "${AppConfig.prefix} Auto Backup enabled!", - ), + builder: + (_) => const StackDialog( + title: + "Encrypting initial backup", + message: + "This shouldn't take long", + ), ); - if (mounted) { - passwordController.text = ""; - passwordRepeatController.text = ""; - Navigator.of(context).popUntil( - ModalRoute.withName( - AutoBackupView.routeName, - ), + // make sure the dialog is able to be displayed for at least some time + final fut = Future.delayed( + const Duration(milliseconds: 300), + ); + + String adkString; + int adkVersion; + try { + final adk = await compute( + generateAdk, + passphrase, + ); + adkString = Format.uint8listToString( + adk.item2, + ); + adkVersion = adk.item1; + } on Exception catch (e, s) { + final String err = + getErrorMessageFromSWBException(e); + Logging.instance.e( + "$err\n$s", + error: e, + stackTrace: s, + ); + // pop encryption progress dialog + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.warning, + message: err, + context: context, ); + return; + } catch (e, s) { + Logging.instance.e( + "", + error: e, + stackTrace: s, + ); + // pop encryption progress dialog + Navigator.of(context).pop(); + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$e", + context: context, + ); + return; } - } else { - await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Failed to enable Auto Backup", - ), + + await secureStore.write( + key: "auto_adk_string", + value: adkString, + ); + await secureStore.write( + key: "auto_adk_version_string", + value: adkVersion.toString(), ); - } - } - }, - child: Text( - "Enable Auto Backup", - style: STextStyles.button(context), + + final DateTime now = DateTime.now(); + final String fileToSave = + createAutoBackupFilename( + pathToSave, + now, + ); + + final backup = await SWB + .createStackWalletJSON( + secureStorage: secureStore, + ); + + final bool result = await SWB + .encryptStackWalletWithADK( + fileToSave, + adkString, + jsonEncode(backup), + adkVersion, + ); + + // this future should already be complete unless there was an error encrypting + await Future.wait([fut]); + + if (mounted) { + // pop encryption progress dialog + Navigator.of(context).pop(); + + if (result) { + ref + .read(prefsChangeNotifierProvider) + .autoBackupLocation = pathToSave; + ref + .read(prefsChangeNotifierProvider) + .lastAutoBackup = now; + + ref + .read(prefsChangeNotifierProvider) + .isAutoBackupEnabled = true; + + await showDialog( + context: context, + barrierDismissible: false, + builder: + (_) => + Platform.isAndroid + ? StackOkDialog( + title: + "${AppConfig.prefix} Auto Backup enabled and saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: + "${AppConfig.prefix} Auto Backup enabled!", + ), + ); + if (mounted) { + passwordController.text = ""; + passwordRepeatController.text = ""; + + Navigator.of(context).popUntil( + ModalRoute.withName( + AutoBackupView.routeName, + ), + ); + } + } else { + await showDialog( + context: context, + barrierDismissible: false, + builder: + (_) => const StackOkDialog( + title: + "Failed to enable Auto Backup", + ), + ); + } + } + }, + child: Text( + "Enable Auto Backup", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart index 8b1c72ebd..3088b69ac 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_information_view.dart @@ -9,12 +9,13 @@ */ import 'package:flutter/material.dart'; -import 'create_backup_view.dart'; + import '../../../../themes/stack_colors.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/rounded_white_container.dart'; +import 'create_backup_view.dart'; class CreateBackupInfoView extends StatelessWidget { const CreateBackupInfoView({super.key}); @@ -36,63 +37,59 @@ class CreateBackupInfoView extends StatelessWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Create backup", - style: STextStyles.navBarTitle(context), - ), + title: Text("Create backup", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Info", - style: STextStyles.pageTitleH2(context), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Info", + style: STextStyles.pageTitleH2(context), + ), ), - ), - const SizedBox( - height: 16, - ), - RoundedWhiteContainer( - child: Text( - // TODO: need info - "{lorem ipsum}", - style: STextStyles.baseXS(context), + const SizedBox(height: 16), + RoundedWhiteContainer( + child: Text( + // TODO: need info + "{lorem ipsum}", + style: STextStyles.baseXS(context), + ), ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.of(context) - .pushNamed(CreateBackupView.routeName); - }, - child: Text( - "Next", - style: STextStyles.button(context), + const SizedBox(height: 16), + const Spacer(), + TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + Navigator.of( + context, + ).pushNamed(CreateBackupView.routeName); + }, + child: Text( + "Next", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), 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 944847a42..a04f0de24 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 @@ -138,21 +138,21 @@ class _RestoreFromFileViewState extends State { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), ), - ), - ); - }, + ); + }, + ), ), ), ), @@ -168,8 +168,9 @@ class _RestoreFromFileViewState extends State { padding: const EdgeInsets.only(bottom: 10), child: Text( "Choose file location", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark3, ), @@ -190,30 +191,31 @@ class _RestoreFromFileViewState extends State { child: TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid || Platform.isIOS - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir(context); - } + onTap: + Platform.isAndroid || Platform.isIOS + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + + if (mounted) { + await stackFileSystem.pickDir(context); + } - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance.e( + "", + error: e, + stackTrace: s, + ); } - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - } - }, + }, controller: fileLocationController, style: STextStyles.field(context), decoration: InputDecoration( @@ -222,26 +224,24 @@ class _RestoreFromFileViewState extends State { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), ), key: const Key( - "createBackupSaveToFileLocationTextFieldKey"), + "createBackupSaveToFileLocationTextFieldKey", + ), readOnly: true, toolbarOptions: const ToolbarOptions( copy: true, @@ -257,16 +257,15 @@ class _RestoreFromFileViewState extends State { }, ), if (!Platform.isAndroid && !Platform.isIOS) - SizedBox( - height: !isDesktop ? 8 : 24, - ), + SizedBox(height: !isDesktop ? 8 : 24), if (isDesktop) Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Create a passphrase", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark3, ), @@ -295,9 +294,7 @@ class _RestoreFromFileViewState extends State { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), GestureDetector( key: const Key( "createBackupPasswordFieldShowPasswordButtonKey", @@ -309,16 +306,15 @@ class _RestoreFromFileViewState extends State { }, child: SvgPicture.asset( hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -369,46 +365,43 @@ class _RestoreFromFileViewState extends State { right: 12, top: passwordFeedback.isNotEmpty ? 4 : 0, ), - child: passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, + child: + passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, ), if (passwordFocusNode.hasFocus || passwordRepeatFocusNode.hasFocus || passwordController.text.isNotEmpty) Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), + padding: const EdgeInsets.only(left: 12, right: 12, top: 10), child: ProgressBar( key: const Key("createStackBackUpProgressBar"), width: MediaQuery.of(context).size.width - 32 - 24, height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension()! - .buttonBackSecondary, + fillColor: + passwordStrength < 0.51 + ? Theme.of( + context, + ).extension()!.accentColorRed + : passwordStrength < 1 + ? Theme.of( + context, + ).extension()!.accentColorYellow + : Theme.of( + context, + ).extension()!.accentColorGreen, + backgroundColor: + Theme.of( + context, + ).extension()!.buttonBackSecondary, percent: passwordStrength < 0.25 ? 0.03 : passwordStrength, ), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -431,9 +424,7 @@ class _RestoreFromFileViewState extends State { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), GestureDetector( key: const Key( "createBackupPasswordFieldShowPasswordButtonKey", @@ -445,16 +436,15 @@ class _RestoreFromFileViewState extends State { }, child: SvgPicture.asset( hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -465,24 +455,24 @@ class _RestoreFromFileViewState extends State { }, ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), if (!isDesktop) const Spacer(), !isDesktop ? Consumer( - builder: (context, ref, __) { - return TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - onPressed: !shouldEnableCreate - ? null - : () async { + builder: (context, ref, __) { + return TextButton( + style: + shouldEnableCreate + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + onPressed: + !shouldEnableCreate + ? null + : () async { final String pathToSave = fileLocationController.text; final String passphrase = @@ -535,10 +525,11 @@ class _RestoreFromFileViewState extends State { showDialog( context: context, barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Encrypting backup", - message: "This shouldn't take long", - ), + builder: + (_) => const StackDialog( + title: "Encrypting backup", + message: "This shouldn't take long", + ), ), ); // make sure the dialog is able to be displayed for at least 1 second @@ -554,12 +545,12 @@ class _RestoreFromFileViewState extends State { secureStorage: ref.read(secureStoreProvider), ); - final bool result = - await SWB.encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); + final bool result = await SWB + .encryptStackWalletWithPassphrase( + fileToSave, + passphrase, + jsonEncode(backup), + ); if (mounted) { // pop encryption progress dialog @@ -569,15 +560,17 @@ class _RestoreFromFileViewState extends State { await showDialog( context: context, barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: "Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: - "Backup creation succeeded", - ), + builder: + (_) => + Platform.isAndroid + ? StackOkDialog( + title: "Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: + "Backup creation succeeded", + ), ); passwordController.text = ""; passwordRepeatController.text = ""; @@ -586,32 +579,34 @@ class _RestoreFromFileViewState extends State { await showDialog( context: context, barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed", - ), + builder: + (_) => const StackOkDialog( + title: "Backup creation failed", + ), ); } } }, - child: Text( - "Create backup", - style: STextStyles.button(context), - ), - ); - }, - ) + child: Text( + "Create backup", + style: STextStyles.button(context), + ), + ); + }, + ) : Row( - children: [ - Consumer( - builder: (context, ref, __) { - return PrimaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Create backup", - enabled: shouldEnableCreate, - onPressed: !shouldEnableCreate - ? null - : () async { + children: [ + Consumer( + builder: (context, ref, __) { + return PrimaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Create backup", + enabled: shouldEnableCreate, + onPressed: + !shouldEnableCreate + ? null + : () async { final String pathToSave = fileLocationController.text; final String passphrase = @@ -629,8 +624,9 @@ class _RestoreFromFileViewState extends State { ); return; } - if (!(await Directory(pathToSave) - .exists())) { + if (!(await Directory( + pathToSave, + ).exists())) { unawaited( showFloatingFlushBar( type: FlushBarType.warning, @@ -684,18 +680,16 @@ class _RestoreFromFileViewState extends State { "Encrypting initial backup", style: STextStyles.desktopH3( - context, - ), - ), - const SizedBox( - height: 40, + context, + ), ), + const SizedBox(height: 40), Text( "This shouldn't take long", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ], ), @@ -726,18 +720,19 @@ class _RestoreFromFileViewState extends State { final String fileToSave = "$pathToSave/stackbackup_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.swb"; - final backup = - await SWB.createStackWalletJSON( - secureStorage: - ref.read(secureStoreProvider), - ); + final backup = await SWB + .createStackWalletJSON( + secureStorage: ref.read( + secureStoreProvider, + ), + ); final bool result = await SWB .encryptStackWalletWithPassphrase( - fileToSave, - passphrase, - jsonEncode(backup), - ); + fileToSave, + passphrase, + jsonEncode(backup), + ); await Future.wait([fut]); @@ -763,10 +758,10 @@ class _RestoreFromFileViewState extends State { child: Padding( padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), + left: 32, + right: 32, + bottom: 32, + ), child: Column( mainAxisSize: MainAxisSize.min, @@ -779,15 +774,17 @@ class _RestoreFromFileViewState extends State { ), Text( "${AppConfig.prefix} backup saved to: \n", - style: STextStyles - .desktopH3(context), + style: + STextStyles.desktopH3( + context, + ), ), Text( fileToSave, - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), const SizedBox( height: 40, @@ -796,8 +793,7 @@ class _RestoreFromFileViewState extends State { children: [ // const Spacer(), Expanded( - child: - PrimaryButton( + child: PrimaryButton( label: "Ok", buttonHeight: ButtonHeight @@ -835,27 +831,26 @@ class _RestoreFromFileViewState extends State { await showDialog( context: context, barrierDismissible: false, - builder: (_) => const StackOkDialog( - title: "Backup creation failed", - ), + builder: + (_) => const StackOkDialog( + title: "Backup creation failed", + ), ); } } }, - ); - }, - ), - const SizedBox( - width: 16, - ), - SecondaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Cancel", - onPressed: () {}, - ), - ], - ), + ); + }, + ), + const SizedBox(width: 16), + SecondaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), ), 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 53669d9f0..5ad7eab46 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 @@ -47,9 +47,7 @@ import 'helpers/swb_file_system.dart'; import 'sub_views/backup_frequency_type_select_sheet.dart'; class EditAutoBackupView extends ConsumerStatefulWidget { - const EditAutoBackupView({ - super.key, - }); + const EditAutoBackupView({super.key}); static const String routeName = "/editAutoBackup"; @@ -142,10 +140,11 @@ class _EditAutoBackupViewState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => const StackDialog( - title: "Updating Auto Backup", - message: "This shouldn't take long", - ), + builder: + (_) => const StackDialog( + title: "Updating Auto Backup", + message: "This shouldn't take long", + ), ), ); // make sure the dialog is able to be displayed for at least 1 second @@ -220,29 +219,33 @@ class _EditAutoBackupViewState extends ConsumerState { await showDialog( context: context, barrierDismissible: false, - builder: (_) => Platform.isAndroid - ? StackOkDialog( - title: "${AppConfig.prefix} Auto Backup saved to:", - message: fileToSave, - ) - : const StackOkDialog( - title: "${AppConfig.prefix} Auto Backup saved"), + builder: + (_) => + Platform.isAndroid + ? StackOkDialog( + title: "${AppConfig.prefix} Auto Backup saved to:", + message: fileToSave, + ) + : const StackOkDialog( + title: "${AppConfig.prefix} Auto Backup saved", + ), ); if (mounted) { passwordController.text = ""; passwordRepeatController.text = ""; if (!Util.isDesktop) { - Navigator.of(context) - .popUntil(ModalRoute.withName(AutoBackupView.routeName)); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(AutoBackupView.routeName)); } } } else { await showDialog( context: context, barrierDismissible: false, - builder: (_) => - const StackOkDialog(title: "Failed to update Auto Backup"), + builder: + (_) => const StackOkDialog(title: "Failed to update Auto Backup"), ); } } @@ -299,49 +302,47 @@ class _EditAutoBackupViewState extends ConsumerState { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Edit Auto Backup", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, - ), + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Edit Auto Backup", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), + ); + }, ), - ); - }, + ), + ), ), ), - ), - ), child: Column( crossAxisAlignment: isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.stretch, children: [ if (!isDesktop) - Text( - "Create your backup", - style: STextStyles.smallMed12(context), - ), + Text("Create your backup", style: STextStyles.smallMed12(context)), if (isDesktop) Text( "Choose file location", @@ -350,33 +351,32 @@ class _EditAutoBackupViewState extends ConsumerState { ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), if (!Platform.isAndroid && !Platform.isIOS) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid || Platform.isIOS - ? null - : () async { - try { - await stackFileSystem.prepareStorage(); - - if (mounted) { - await stackFileSystem.pickDir(context); + onTap: + Platform.isAndroid || Platform.isIOS + ? null + : () async { + try { + await stackFileSystem.prepareStorage(); + + if (mounted) { + await stackFileSystem.pickDir(context); + } + + if (mounted) { + setState(() { + fileLocationController.text = + stackFileSystem.dirPath ?? ""; + }); + } + } catch (e, s) { + Logging.instance.e("$e\n$s", error: e, stackTrace: s); } - - if (mounted) { - setState(() { - fileLocationController.text = - stackFileSystem.dirPath ?? ""; - }); - } - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); - } - }, + }, controller: fileLocationController, style: STextStyles.field(context), decoration: InputDecoration( @@ -385,20 +385,17 @@ class _EditAutoBackupViewState extends ConsumerState { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -413,10 +410,7 @@ class _EditAutoBackupViewState extends ConsumerState { ), onChanged: (newValue) {}, ), - if (isDesktop) - const SizedBox( - height: 24, - ), + if (isDesktop) const SizedBox(height: 24), if (isDesktop) Text( "Create a passphrase", @@ -426,9 +420,7 @@ class _EditAutoBackupViewState extends ConsumerState { textAlign: TextAlign.left, ), if (!Platform.isAndroid && !Platform.isIOS) - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -450,9 +442,7 @@ class _EditAutoBackupViewState extends ConsumerState { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), GestureDetector( key: const Key( "createBackupPasswordFieldShowPasswordButtonKey", @@ -464,16 +454,15 @@ class _EditAutoBackupViewState extends ConsumerState { }, child: SvgPicture.asset( hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -524,46 +513,46 @@ class _EditAutoBackupViewState extends ConsumerState { right: 12, top: passwordFeedback.isNotEmpty ? 4 : 0, ), - child: passwordFeedback.isNotEmpty - ? Text( - passwordFeedback, - style: STextStyles.infoSmall(context), - ) - : null, + child: + passwordFeedback.isNotEmpty + ? Text( + passwordFeedback, + style: STextStyles.infoSmall(context), + ) + : null, ), if (passwordFocusNode.hasFocus || passwordRepeatFocusNode.hasFocus || passwordController.text.isNotEmpty) Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), + padding: const EdgeInsets.only(left: 12, right: 12, top: 10), child: ProgressBar( key: const Key("createStackBackUpProgressBar"), - width: isDesktop - ? 492 - : MediaQuery.of(context).size.width - 32 - 24, + width: + isDesktop + ? 492 + : MediaQuery.of(context).size.width - 32 - 24, height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context).extension()!.accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension()! - .buttonBackSecondary, + fillColor: + passwordStrength < 0.51 + ? Theme.of( + context, + ).extension()!.accentColorRed + : passwordStrength < 1 + ? Theme.of( + context, + ).extension()!.accentColorYellow + : Theme.of( + context, + ).extension()!.accentColorGreen, + backgroundColor: + Theme.of( + context, + ).extension()!.buttonBackSecondary, percent: passwordStrength < 0.25 ? 0.03 : passwordStrength, ), ), - SizedBox( - height: isDesktop ? 16 : 10, - ), + SizedBox(height: isDesktop ? 16 : 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -585,9 +574,7 @@ class _EditAutoBackupViewState extends ConsumerState { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), GestureDetector( key: const Key( "createBackupPasswordFieldShowPasswordButtonKey", @@ -599,16 +586,15 @@ class _EditAutoBackupViewState extends ConsumerState { }, child: SvgPicture.asset( hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -619,52 +605,46 @@ class _EditAutoBackupViewState extends ConsumerState { }, ), ), - SizedBox( - height: isDesktop ? 24 : 32, - ), + SizedBox(height: isDesktop ? 24 : 32), Text( "Auto Backup frequency", - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark3, - ) - : STextStyles.smallMed12(context), - ), - const SizedBox( - height: 10, + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ) + : STextStyles.smallMed12(context), ), + const SizedBox(height: 10), if (isDesktop) DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, value: _currentDropDownValue, items: [ - ..._dropDownItems.map( - (e) { - String message = ""; - switch (e) { - case BackupFrequencyType.everyTenMinutes: - message = "Every 10 minutes"; - break; - case BackupFrequencyType.everyAppStart: - message = "Every app startup"; - break; - case BackupFrequencyType.afterClosingAWallet: - message = "After closing a cryptocurrency wallet"; - break; - } - - return DropdownMenuItem( - value: e, - child: Text( - message, - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - ); - }, - ), + ..._dropDownItems.map((e) { + String message = ""; + switch (e) { + case BackupFrequencyType.everyTenMinutes: + message = "Every 10 minutes"; + break; + case BackupFrequencyType.everyAppStart: + message = "Every app startup"; + break; + case BackupFrequencyType.afterClosingAWallet: + message = "After closing a cryptocurrency wallet"; + break; + } + + return DropdownMenuItem( + value: e, + child: Text( + message, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ); + }), ], onChanged: (value) { if (value is BackupFrequencyType) { @@ -694,19 +674,17 @@ class _EditAutoBackupViewState extends ConsumerState { offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ), @@ -759,9 +737,10 @@ class _EditAutoBackupViewState extends ConsumerState { padding: const EdgeInsets.only(right: 4.0), child: SvgPicture.asset( Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .textSubtitle2, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, width: 12, height: 6, ), @@ -774,9 +753,7 @@ class _EditAutoBackupViewState extends ConsumerState { ], ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 24 : 10, - ), + SizedBox(height: isDesktop ? 24 : 10), if (isDesktop) Row( children: [ @@ -787,9 +764,7 @@ class _EditAutoBackupViewState extends ConsumerState { onPressed: Navigator.of(context).pop, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Save", @@ -802,18 +777,16 @@ class _EditAutoBackupViewState extends ConsumerState { ), if (!isDesktop) TextButton( - style: shouldEnableCreate - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), + style: + shouldEnableCreate + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), onPressed: !shouldEnableCreate ? null : onSavePressed, - child: Text( - "Save", - style: STextStyles.button(context), - ), + child: Text("Save", style: STextStyles.button(context)), ), ], ), 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 b6e872bd4..1d8d9c90f 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 @@ -47,7 +47,6 @@ import '../../../../../utilities/format.dart'; import '../../../../../utilities/logger.dart'; import '../../../../../utilities/prefs.dart'; import '../../../../../utilities/util.dart'; -import '../../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../../wallets/crypto_currency/intermediate/frost_currency.dart'; import '../../../../../wallets/isar/models/frost_wallet_info.dart'; import '../../../../../wallets/isar/models/wallet_info.dart'; @@ -57,6 +56,7 @@ import '../../../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../../../wallets/wallet/impl/xelis_wallet.dart'; import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../../wallets/wallet/wallet.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart'; @@ -141,10 +141,13 @@ abstract class SWB { if (!backupFile.existsSync()) { final String jsonBackup = plaintext; final Uint8List content = Uint8List.fromList(utf8.encode(jsonBackup)); - final Uint8List encryptedContent = - await encryptWithPassphrase(passphrase, content); - backupFile - .writeAsStringSync(Format.uint8listToString(encryptedContent)); + final Uint8List encryptedContent = await encryptWithPassphrase( + passphrase, + content, + ); + backupFile.writeAsStringSync( + Format.uint8listToString(encryptedContent), + ); } Logging.instance.d(backupFile.absolute); return true; @@ -170,8 +173,9 @@ abstract class SWB { content, version: adkVersion, ); - backupFile - .writeAsStringSync(Format.uint8listToString(encryptedContent)); + backupFile.writeAsStringSync( + Format.uint8listToString(encryptedContent), + ); } Logging.instance.d(backupFile.absolute); return true; @@ -207,8 +211,10 @@ abstract class SWB { final encryptedBytes = Format.stringToUint8List(encryptedText); - final decryptedContent = - await decryptWithPassphrase(passphrase, encryptedBytes); + final decryptedContent = await decryptWithPassphrase( + passphrase, + encryptedBytes, + ); final jsonBackup = utf8.decode(decryptedContent); return jsonBackup; @@ -225,50 +231,30 @@ abstract class SWB { Logging.instance.d("Starting createStackWalletJSON..."); final _wallets = Wallets.sharedInstance; final Map backupJson = {}; - final NodeService nodeService = - NodeService(secureStorageInterface: secureStorage); + final NodeService nodeService = NodeService( + secureStorageInterface: secureStorage, + ); final _secureStore = secureStorage; - Logging.instance.d( - "createStackWalletJSON awaiting DB.instance.mutex...", - ); + Logging.instance.d("createStackWalletJSON awaiting DB.instance.mutex..."); // prevent modification of data await DB.instance.mutex.protect(() async { - Logging.instance.i( - "...createStackWalletJSON DB.instance.mutex acquired", - ); - Logging.instance.i( - "SWB backing up nodes", - ); - try { - final primaryNodes = nodeService.primaryNodes.map((e) async { - final map = e.toMap(); - map["password"] = await e.getPassword(_secureStore); - return map; - }).toList(); - backupJson['primaryNodes'] = await Future.wait(primaryNodes); - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - } + Logging.instance.i("...createStackWalletJSON DB.instance.mutex acquired"); + Logging.instance.i("SWB backing up nodes"); try { - final nodesFuture = nodeService.nodes.map((e) async { - final map = e.toMap(); - map["password"] = await e.getPassword(_secureStore); - return map; - }).toList(); + final nodesFuture = + nodeService.nodes.map((e) async { + final map = e.toMap(); + map["password"] = await e.getPassword(_secureStore); + return map; + }).toList(); final nodes = await Future.wait(nodesFuture); backupJson['nodes'] = nodes; } catch (e, s) { Logging.instance.e("", error: e, stackTrace: s); } - Logging.instance.d( - "SWB backing up prefs", - ); + Logging.instance.d("SWB backing up prefs"); final Map prefs = {}; final _prefs = Prefs.instance; @@ -289,18 +275,14 @@ abstract class SWB { backupJson['prefs'] = prefs; - Logging.instance.d( - "SWB backing up addressbook", - ); + Logging.instance.d("SWB backing up addressbook"); final AddressBookService addressBookService = AddressBookService(); final addresses = addressBookService.contacts; backupJson['addressBookEntries'] = addresses.map((e) => e.toMap()).toList(); - Logging.instance.d( - "SWB backing up wallets", - ); + Logging.instance.d("SWB backing up wallets"); final List backupWallets = []; for (final wallet in _wallets.wallets) { @@ -349,14 +331,15 @@ abstract class SWB { backupWallet['restoreHeight'] = wallet.info.restoreHeight; - final isarNotes = await MainDB.instance.isar.transactionNotes - .where() - .walletIdEqualTo(wallet.walletId) - .findAll(); + final isarNotes = + await MainDB.instance.isar.transactionNotes + .where() + .walletIdEqualTo(wallet.walletId) + .findAll(); - final notes = isarNotes - .asMap() - .map((key, value) => MapEntry(value.txid, value.value)); + final notes = isarNotes.asMap().map( + (key, value) => MapEntry(value.txid, value.value), + ); backupWallet['notes'] = notes; @@ -364,14 +347,13 @@ abstract class SWB { } backupJson['wallets'] = backupWallets; - Logging.instance.d( - "SWB backing up trades", - ); + Logging.instance.d("SWB backing up trades"); // back up trade history final tradesService = TradesService(); - final trades = - tradesService.trades.map((e) => e.toMap()).toList(growable: false); + final trades = tradesService.trades + .map((e) => e.toMap()) + .toList(growable: false); backupJson["tradeHistory"] = trades; // back up trade history lookup data for trades send from stack wallet @@ -380,18 +362,14 @@ abstract class SWB { tradeTxidLookupDataService.all.map((e) => e.toMap()).toList(); backupJson["tradeTxidLookupData"] = lookupData; - Logging.instance.d( - "SWB backing up trade notes", - ); + Logging.instance.d("SWB backing up trade notes"); // back up trade notes final tradeNotesService = TradeNotesService(); final tradeNotes = tradeNotesService.all; backupJson["tradeNotes"] = tradeNotes; }); - Logging.instance.d( - "createStackWalletJSON DB.instance.mutex released", - ); + Logging.instance.d("createStackWalletJSON DB.instance.mutex released"); // // back up notifications data // final notificationsService = NotificationsService(); @@ -473,9 +451,7 @@ abstract class SWB { knownSalts: [], participants: participants, myName: myName, - threshold: frost.multisigThreshold( - multisigConfig: multisigConfig, - ), + threshold: frost.multisigThreshold(multisigConfig: multisigConfig), ); await MainDB.instance.isar.writeTxn(() async { @@ -507,7 +483,7 @@ abstract class SWB { case const (WowneroWallet): await (wallet as WowneroWallet).init(isRestore: true); break; - + case const (XelisWallet): await (wallet as XelisWallet).init(isRestore: true); break; @@ -518,7 +494,9 @@ abstract class SWB { int restoreHeight = walletbackup['restoreHeight'] as int? ?? 0; if (restoreHeight <= 0) { - if (wallet is EpiccashWallet || wallet is LibMoneroWallet) { + if (wallet is EpiccashWallet || + wallet is LibMoneroWallet || + wallet is LibSalviumWallet) { restoreHeight = 0; } else { restoreHeight = walletbackup['storedChainHeight'] as int? ?? 0; @@ -547,8 +525,9 @@ abstract class SWB { } // restore notes - final notesMap = - Map.from(walletbackup["notes"] as Map? ?? {}); + final notesMap = Map.from( + walletbackup["notes"] as Map? ?? {}, + ); final List notes = []; for (final key in notesMap.keys) { @@ -601,11 +580,7 @@ abstract class SWB { mnemonicPassphrase: mnemonicPassphrase, ); } catch (e, s) { - Logging.instance.i( - "", - error: e, - stackTrace: s, - ); + Logging.instance.i("", error: e, stackTrace: s); uiState?.update( walletId: info.walletId, restoringStatus: StackRestoringStatus.failed, @@ -639,17 +614,13 @@ abstract class SWB { uiState?.preferences = StackRestoringStatus.restoring; - Logging.instance.d( - "SWB restoring prefs", - ); + Logging.instance.d("SWB restoring prefs"); await _restorePrefs(prefs); uiState?.preferences = StackRestoringStatus.success; uiState?.addressBook = StackRestoringStatus.restoring; - Logging.instance.d( - "SWB restoring addressbook", - ); + Logging.instance.d("SWB restoring addressbook"); if (addressBookEntries != null) { await _restoreAddressBook(addressBookEntries); } @@ -657,40 +628,28 @@ abstract class SWB { uiState?.addressBook = StackRestoringStatus.success; uiState?.nodes = StackRestoringStatus.restoring; - Logging.instance.d( - "SWB restoring nodes", - ); - await _restoreNodes( - nodes, - primaryNodes, - secureStorageInterface, - ); + Logging.instance.d("SWB restoring nodes"); + await _restoreNodes(nodes, primaryNodes, secureStorageInterface); uiState?.nodes = StackRestoringStatus.success; uiState?.trades = StackRestoringStatus.restoring; // restore trade history if (trades != null) { - Logging.instance.d( - "SWB restoring trades", - ); + Logging.instance.d("SWB restoring trades"); await _restoreTrades(trades); } // restore trade history lookup data for trades send from stack wallet if (tradeTxidLookupData != null) { - Logging.instance.d( - "SWB restoring trade look up data", - ); + Logging.instance.d("SWB restoring trade look up data"); await _restoreTradesLookUpData(tradeTxidLookupData, oldToNewWalletIdMap); } // restore trade notes if (tradeNotes != null) { - Logging.instance.d( - "SWB restoring trade notes", - ); + Logging.instance.d("SWB restoring trade notes"); await _restoreTradesNotes(tradeNotes); } @@ -722,22 +681,22 @@ abstract class SWB { ) async { if (!Platform.isLinux) await WakelockPlus.enable(); - Logging.instance.d( - "SWB creating temp backup", - ); - final preRestoreJSON = - await createStackWalletJSON(secureStorage: secureStorageInterface); - Logging.instance.d( - "SWB temp backup created", + Logging.instance.d("SWB creating temp backup"); + final preRestoreJSON = await createStackWalletJSON( + secureStorage: secureStorageInterface, ); + Logging.instance.d("SWB temp backup created"); - final List _currentWalletIds = await MainDB.instance.isar.walletInfo - .where() - .walletIdProperty() - .findAll(); + final List _currentWalletIds = + await MainDB.instance.isar.walletInfo + .where() + .walletIdProperty() + .findAll(); - final preRestoreState = - PreRestoreState(_currentWalletIds.toSet(), preRestoreJSON); + final preRestoreState = PreRestoreState( + _currentWalletIds.toSet(), + preRestoreJSON, + ); final Map oldToNewWalletIdMap = {}; @@ -759,10 +718,7 @@ abstract class SWB { // basic cancel check here // no reverting required yet as nothing has been written to store - if (_checkShouldCancel( - null, - secureStorageInterface, - )) { + if (_checkShouldCancel(null, secureStorageInterface)) { return false; } @@ -774,10 +730,7 @@ abstract class SWB { ); // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } @@ -794,10 +747,7 @@ abstract class SWB { for (final walletbackup in wallets) { // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } @@ -818,8 +768,9 @@ abstract class SWB { Map? otherData; try { if (walletbackup["otherDataJsonString"] is String) { - final data = - jsonDecode(walletbackup["otherDataJsonString"] as String); + final data = jsonDecode( + walletbackup["otherDataJsonString"] as String, + ); otherData = Map.from(data as Map); } } catch (e, s) { @@ -830,13 +781,6 @@ abstract class SWB { ); } - if (coin is Firo) { - otherData ??= {}; - // swb will do a restore so this flag should be set to false so another - // rescan/restore isn't done when opening the wallet - otherData[WalletInfoKeys.lelantusCoinIsarRescanRequired] = false; - } - final info = WalletInfo( coinName: coin.identifier, walletId: walletId, @@ -850,8 +794,8 @@ abstract class SWB { var node = nodeService.getPrimaryNodeFor(currency: coin); if (node == null) { - node = coin.defaultNode; - await nodeService.setPrimaryNodeFor(coin: coin, node: node); + node = coin.defaultNode(isPrimary: true); + await nodeService.save(node, null, false); } // final txTracker = TransactionNotificationTracker(walletId: walletId); @@ -859,19 +803,13 @@ abstract class SWB { // final failovers = nodeService.failoverNodesFor(coin: coin); // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } managers.add(Tuple2(walletbackup, info)); // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } @@ -884,10 +822,7 @@ abstract class SWB { } // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } @@ -898,10 +833,7 @@ abstract class SWB { // start restoring wallets for (final tuple in managers) { // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } final bools = await _asyncRestore( @@ -915,19 +847,13 @@ abstract class SWB { } // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } for (final Future status in restoreStatuses) { // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } await status; @@ -935,19 +861,17 @@ abstract class SWB { if (!Platform.isLinux) await WakelockPlus.disable(); // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } - Logging.instance.d( - "done with SWB restore", - ); + Logging.instance.d("done with SWB restore"); - await Wallets.sharedInstance - .loadAfterStackRestore(_prefs, uiState?.wallets ?? [], Util.isDesktop); + await Wallets.sharedInstance.loadAfterStackRestore( + _prefs, + uiState?.wallets ?? [], + Util.isDesktop, + ); return true; } @@ -960,8 +884,6 @@ abstract class SWB { revertToState.validJSON["prefs"] as Map; final List? addressBookEntries = revertToState.validJSON["addressBookEntries"] as List?; - final List? primaryNodes = - revertToState.validJSON["primaryNodes"] as List?; final List? nodes = revertToState.validJSON["nodes"] as List?; final List? trades = revertToState.validJSON["tradeHistory"] as List?; @@ -998,11 +920,12 @@ abstract class SWB { // ensure this contact's data matches the pre restore state final List addresses = []; for (final address in (contact['addresses'] as List)) { - final entry = ContactAddressEntry() - ..coinName = address['coin'] as String - ..address = address['address'] as String - ..label = address['label'] as String - ..other = address['other'] as String?; + final entry = + ContactAddressEntry() + ..coinName = address['coin'] as String + ..address = address['address'] as String + ..label = address['label'] as String + ..other = address['other'] as String?; try { entry.coin; @@ -1011,9 +934,7 @@ abstract class SWB { continue; } - addresses.add( - entry, - ); + addresses.add(entry); } await addressBookService.editContact( ContactEntry( @@ -1054,7 +975,7 @@ abstract class SWB { if (nodeData != null) { // node existed before restore attempt // revert to pre restore node - await nodeService.edit( + await nodeService.save( node.copyWith( host: nodeData['host'] as String, port: nodeData['port'] as int, @@ -1066,6 +987,7 @@ abstract class SWB { isFailover: nodeData['isFailover'] as bool, isDown: nodeData['isDown'] as bool, trusted: nodeData['trusted'] as bool?, + isPrimary: nodeData["isPrimary"] as bool? ?? false, ), nodeData['password'] as String?, true, @@ -1076,28 +998,6 @@ abstract class SWB { } } - // primary nodes - if (primaryNodes != null) { - for (final node in primaryNodes) { - try { - final CryptoCurrency coin; - try { - coin = AppConfig.getCryptoCurrencyByPrettyName( - node['coinName'] as String, - ); - } catch (_) { - continue; - } - - await nodeService.setPrimaryNodeFor( - coin: coin, - node: nodeService.getNodeById(id: node['id'] as String)!, - ); - } catch (e, s) { - Logging.instance.e("", error: e, stackTrace: s); - } - } - } await nodeService.updateDefaults(); // trades @@ -1174,22 +1074,22 @@ abstract class SWB { } // finally remove any added wallets - final allWalletIds = (await MainDB.instance.isar.walletInfo - .where() - .walletIdProperty() - .findAll()) - .toSet(); + final allWalletIds = + (await MainDB.instance.isar.walletInfo + .where() + .walletIdProperty() + .findAll()) + .toSet(); final walletIdsToDelete = allWalletIds.difference(revertToState.walletIds); await MainDB.instance.isar.writeTxn(() async { - await MainDB.instance.isar.walletInfo - .deleteAllByWalletId(walletIdsToDelete.toList()); + await MainDB.instance.isar.walletInfo.deleteAllByWalletId( + walletIdsToDelete.toList(), + ); }); _cancelCompleter!.complete(); _shouldCancelRestore = false; - Logging.instance.d( - "Revert SWB complete", - ); + Logging.instance.d("Revert SWB complete"); } static Future _restorePrefs(Map prefs) async { @@ -1201,9 +1101,10 @@ abstract class SWB { _prefs.language = prefs['language'] as String; _prefs.showFavoriteWallets = prefs['showFavoriteWallets'] as bool; _prefs.wifiOnly = prefs['wifiOnly'] as bool; - _prefs.syncType = prefs['syncType'] == "currentWalletOnly" - ? SyncingType.currentWalletOnly - : prefs['syncType'] == "selectedWalletsAtStartup" + _prefs.syncType = + prefs['syncType'] == "currentWalletOnly" + ? SyncingType.currentWalletOnly + : prefs['syncType'] == "selectedWalletsAtStartup" ? SyncingType.currentWalletOnly : SyncingType.allWalletsOnStartup; // _prefs.walletIdsSyncOnStartup = @@ -1217,8 +1118,9 @@ abstract class SWB { (e) => e.name == (prefs['backupFrequencyType'] as String?), orElse: () => BackupFrequencyType.everyAppStart, ); - _prefs.lastAutoBackup = - DateTime.tryParse(prefs['lastAutoBackup'] as String? ?? ""); + _prefs.lastAutoBackup = DateTime.tryParse( + prefs['lastAutoBackup'] as String? ?? "", + ); } static Future _restoreAddressBook( @@ -1228,11 +1130,12 @@ abstract class SWB { for (final contact in addressBookEntries) { final List addresses = []; for (final address in (contact['addresses'] as List)) { - final entry = ContactAddressEntry() - ..coinName = address['coin'] as String - ..address = address['address'] as String - ..label = address['label'] as String - ..other = address['other'] as String?; + final entry = + ContactAddressEntry() + ..coinName = address['coin'] as String + ..address = address['address'] as String + ..label = address['label'] as String + ..other = address['other'] as String?; try { entry.coin; @@ -1241,9 +1144,7 @@ abstract class SWB { continue; } - addresses.add( - entry, - ); + addresses.add(entry); } if (addresses.isNotEmpty) { await addressBookService.addContact( @@ -1268,13 +1169,20 @@ abstract class SWB { secureStorageInterface: secureStorageInterface, ); if (nodes != null) { + final primaryIds = + primaryNodes + ?.map((e) => e["id"] as String?) + .whereType() + .toSet(); + for (final node in nodes) { - await nodeService.add( + final id = node['id'] as String; + await nodeService.save( NodeModel( host: node['host'] as String, port: node['port'] as int, name: node['name'] as String, - id: node['id'] as String, + id: id, useSSL: node['useSSL'] == "false" ? false : true, enabled: node['enabled'] == "false" ? false : true, coinName: node['coinName'] as String, @@ -1283,83 +1191,47 @@ abstract class SWB { isDown: node['isDown'] as bool, torEnabled: node['torEnabled'] as bool? ?? true, clearnetEnabled: node['plainEnabled'] as bool? ?? true, + isPrimary: + node["isPrimary"] as bool? ?? primaryIds?.contains(id) ?? false, ), node["password"] as String?, true, ); } } - if (primaryNodes != null) { - for (final node in primaryNodes) { - final CryptoCurrency coin; - try { - coin = AppConfig.getCryptoCurrencyByPrettyName( - node['coinName'] as String, - ); - } catch (_) { - continue; - } - try { - await nodeService.setPrimaryNodeFor( - coin: coin, - node: nodeService.getNodeById(id: node['id'] as String)!, - ); - } catch (e, s) { - Logging.instance.e("", error: e, stackTrace: s); - } - } - } await nodeService.updateDefaults(); } - static Future _restoreTrades( - List trades, - ) async { - final tradesService = TradesService(); - for (int i = 0; i < trades.length - 1; i++) { - ExchangeTransaction? exTx; - try { - exTx = ExchangeTransaction.fromJson(trades[i] as Map); - } catch (e) { - // unneeded log - // Logging.instance.log("$e\n$s", error: e, stackTrace: s,); - } - - Trade trade; - if (exTx != null) { - trade = Trade.fromExchangeTransaction(exTx, false); - } else { - trade = Trade.fromMap(trades[i] as Map); - } - - await tradesService.add( - trade: trade, - shouldNotifyListeners: false, - ); - } - // only call notifyListeners on last one added + static Future _restoreTrades(List trades) async { if (trades.isNotEmpty) { - ExchangeTransaction? exTx; - try { - exTx = - ExchangeTransaction.fromJson(trades.last as Map); - } catch (e) { - // unneeded log - // Logging.instance.log("$e\n$s", level: LogLevel.Warning); - } + final tradesService = TradesService(); + for (int i = 0; i < trades.length; i++) { + // First check for old old database entries + ExchangeTransaction? exTx; + try { + exTx = ExchangeTransaction.fromJson( + trades[i] as Map, + ); + } catch (e) { + // unneeded log + // Logging.instance.log("$e\n$s", error: e, stackTrace: s,); + } - Trade trade; - if (exTx != null) { - trade = Trade.fromExchangeTransaction(exTx, false); - } else { - trade = Trade.fromMap(trades.last as Map); - } + Trade trade; + if (exTx != null) { + trade = Trade.fromExchangeTransaction(exTx, false); + } else { + trade = Trade.fromMap(trades[i] as Map); + } - await tradesService.add( - trade: trade, - shouldNotifyListeners: true, - ); + await tradesService.add( + trade: trade, + shouldNotifyListeners: + i == + trades.length - 1, // only call notifyListeners on last one added + ); + } } } 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 001279117..9954cb0b7 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 @@ -12,10 +12,11 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../../../../app_config.dart'; +import '../../../../../utilities/stack_file_system.dart'; import '../../../../../utilities/util.dart'; class SWBFileSystem { @@ -29,13 +30,9 @@ class SWBFileSystem { Future prepareStorage() async { if (Platform.isAndroid) { - await Permission.storage.request(); - } - rootPath = (await getApplicationDocumentsDirectory()); - //todo: check if print needed - // debugPrint(rootPath!.absolute.toString()); - if (Platform.isAndroid) { - rootPath = Directory("/storage/emulated/0/"); + rootPath = await StackFileSystem.wtfAndroidDocumentsPath(); + } else { + rootPath = await getApplicationDocumentsDirectory(); } //todo: check if print needed // debugPrint(rootPath!.absolute.toString()); @@ -45,14 +42,11 @@ class SWBFileSystem { if (Platform.isIOS) { sampleFolder = Directory(rootPath!.path); - } else if (Platform.isAndroid) { - sampleFolder = Directory('${rootPath!.path}Documents/$dirName'); - } else if (Platform.isLinux) { - sampleFolder = Directory('${rootPath!.path}/$dirName'); - } else if (Platform.isWindows) { - sampleFolder = Directory('${rootPath!.path}/$dirName'); - } else if (Platform.isMacOS) { - sampleFolder = Directory('${rootPath!.path}/$dirName'); + } else if (Platform.isAndroid || + Platform.isLinux || + Platform.isWindows || + Platform.isMacOS) { + sampleFolder = Directory(path.join(rootPath!.path, dirName)); } try { @@ -86,9 +80,10 @@ class SWBFileSystem { if (Platform.isIOS) { chosenPath = startPath?.path; } else { - final String path = Platform.isWindows - ? startPath!.path.replaceAll("/", "\\") - : startPath!.path; + final String path = + Platform.isWindows + ? startPath!.path.replaceAll("/", "\\") + : startPath!.path; chosenPath = await FilePicker.platform.getDirectoryPath( dialogTitle: "Choose Backup location", initialDirectory: path, diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart index 9eff26a7e..5aded8ec6 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart @@ -30,10 +30,7 @@ import 'helpers/restore_create_backup.dart'; import 'sub_views/stack_restore_progress_view.dart'; class RestoreFromEncryptedStringView extends ConsumerStatefulWidget { - const RestoreFromEncryptedStringView({ - super.key, - required this.encrypted, - }); + const RestoreFromEncryptedStringView({super.key, required this.encrypted}); static const String routeName = "/restoreFromEncryptedString"; @@ -95,189 +92,197 @@ class _RestoreFromEncryptedStringViewState style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - key: const Key("restoreFromFilePasswordFieldKey"), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter password", - passwordFocusNode, - context, - ).copyWith( - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "restoreFromFilePasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "restoreFromFilePasswordFieldKey", + ), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "restoreFromFilePasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension()! + .textDark3, + width: 16, + height: 16, + ), ), - ), - const SizedBox( - width: 12, - ), - ], + const SizedBox(width: 12), + ], + ), ), ), + onChanged: (newValue) { + setState(() {}); + }, ), - onChanged: (newValue) { - setState(() {}); - }, ), - ), - const SizedBox( - height: 16, - ), - const Spacer(), - TextButton( - style: passwordController.text.isEmpty - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - onPressed: passwordController.text.isEmpty - ? null - : () async { - final String passphrase = - passwordController.text; + const SizedBox(height: 16), + const Spacer(), + TextButton( + style: + passwordController.text.isEmpty + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle( + context, + ), + onPressed: + passwordController.text.isEmpty + ? null + : () async { + final String passphrase = + passwordController.text; - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } - bool shouldPop = false; - showDialog( - barrierDismissible: false, - context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting ${AppConfig.prefix} backup file", - style: - STextStyles.pageTitleH2( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textWhite, - ), + bool shouldPop = false; + showDialog( + barrierDismissible: false, + context: context, + builder: + (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting ${AppConfig.prefix} backup file", + style: STextStyles.pageTitleH2( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textWhite, + ), + ), + ), + ), + const SizedBox(height: 64), + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], ), ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, - ), - ), - ], - ), - ), - ); + ); - final String? jsonString = await compute( - SWB.decryptStackWalletStringWithPassphrase, - Tuple2(widget.encrypted, passphrase), - debugLabel: - "stack wallet decryption compute", - ); + final String? + jsonString = await compute( + SWB.decryptStackWalletStringWithPassphrase, + Tuple2(widget.encrypted, passphrase), + debugLabel: + "stack wallet decryption compute", + ); - if (mounted) { - // pop LoadingIndicator - shouldPop = true; - Navigator.of(context).pop(); + if (mounted) { + // pop LoadingIndicator + shouldPop = true; + Navigator.of(context).pop(); - passwordController.text = ""; + passwordController.text = ""; - if (jsonString == null) { - showFloatingFlushBar( - type: FlushBarType.warning, - message: - "Failed to decrypt backup file", - context: context, - ); - return; - } + if (jsonString == null) { + showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Failed to decrypt backup file", + context: context, + ); + return; + } - Navigator.of(context).push( - RouteGenerator.getRoute( - builder: (_) => - StackRestoreProgressView( - jsonString: jsonString, - fromFile: true, - ), - ), - ); - } - }, - child: Text( - "Restore", - style: STextStyles.button(context), + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: + (_) => + StackRestoreProgressView( + jsonString: jsonString, + fromFile: true, + ), + ), + ); + } + }, + child: Text( + "Restore", + style: STextStyles.button(context), + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index 400e6e08d..d0ce73db1 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -110,21 +110,21 @@ class _RestoreFromFileViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), ), - ), - ); - }, + ); + }, + ), ), ), ), @@ -140,8 +140,9 @@ class _RestoreFromFileViewState extends ConsumerState { padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Choose file location", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark3, ), @@ -183,20 +184,17 @@ class _RestoreFromFileViewState extends ConsumerState { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -211,16 +209,15 @@ class _RestoreFromFileViewState extends ConsumerState { ), onChanged: (newValue) {}, ), - SizedBox( - height: !isDesktop ? 8 : 24, - ), + SizedBox(height: !isDesktop ? 8 : 24), if (isDesktop) Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Text( "Enter passphrase", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark3, ), @@ -249,9 +246,7 @@ class _RestoreFromFileViewState extends ConsumerState { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), GestureDetector( key: const Key( "restoreFromFilePasswordFieldShowPasswordButtonKey", @@ -263,16 +258,15 @@ class _RestoreFromFileViewState extends ConsumerState { }, child: SvgPicture.asset( hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), @@ -282,24 +276,24 @@ class _RestoreFromFileViewState extends ConsumerState { }, ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), if (!isDesktop) const Spacer(), !isDesktop ? TextButton( - style: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? null - : () async { + style: + passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: + passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { final String fileToRestore = fileLocationController.text; final String passphrase = passwordController.text; @@ -325,41 +319,42 @@ class _RestoreFromFileViewState extends ConsumerState { showDialog( barrierDismissible: false, context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting ${AppConfig.prefix} backup file", - style: STextStyles.pageTitleH2( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textWhite, + builder: + (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting ${AppConfig.prefix} backup file", + style: STextStyles.pageTitleH2( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textWhite, + ), + ), ), ), - ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, - ), + const SizedBox(height: 64), + const Center( + child: LoadingIndicator(width: 100), + ), + ], ), - ], - ), - ), + ), ), ); @@ -387,31 +382,31 @@ class _RestoreFromFileViewState extends ConsumerState { await Navigator.of(context).push( RouteGenerator.getRoute( - builder: (_) => StackRestoreProgressView( - jsonString: jsonString, - shouldPushToHome: true, - ), + builder: + (_) => StackRestoreProgressView( + jsonString: jsonString, + shouldPushToHome: true, + ), ), ); } }, - child: Text( - "Restore", - style: STextStyles.button(context), - ), - ) + child: Text("Restore", style: STextStyles.button(context)), + ) : Row( - children: [ - PrimaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Restore", - enabled: !(passwordController.text.isEmpty || - fileLocationController.text.isEmpty), - onPressed: passwordController.text.isEmpty || - fileLocationController.text.isEmpty - ? null - : () async { + children: [ + PrimaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Restore", + enabled: + !(passwordController.text.isEmpty || + fileLocationController.text.isEmpty), + onPressed: + passwordController.text.isEmpty || + fileLocationController.text.isEmpty + ? null + : () async { final String fileToRestore = fileLocationController.text; final String passphrase = @@ -438,42 +433,45 @@ class _RestoreFromFileViewState extends ConsumerState { showDialog( barrierDismissible: false, context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Decrypting ${AppConfig.prefix} backup file", - style: STextStyles.pageTitleH2( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textWhite, + builder: + (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Decrypting ${AppConfig.prefix} backup file", + style: + STextStyles.pageTitleH2( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textWhite, + ), + ), ), ), - ), - ), - const SizedBox( - height: 64, - ), - const Center( - child: LoadingIndicator( - width: 100, - ), + const SizedBox(height: 64), + const Center( + child: LoadingIndicator( + width: 100, + ), + ), + ], ), - ], - ), - ), + ), ), ); @@ -530,16 +528,15 @@ class _RestoreFromFileViewState extends ConsumerState { children: [ Padding( padding: - const EdgeInsets - .all( - 32, - ), + const EdgeInsets.all( + 32, + ), child: Text( "Restore ${AppConfig.appName}", - style: STextStyles - .desktopH3( - context, - ), + style: + STextStyles.desktopH3( + context, + ), textAlign: TextAlign .center, @@ -550,15 +547,14 @@ class _RestoreFromFileViewState extends ConsumerState { ), Padding( padding: - const EdgeInsets - .symmetric( - horizontal: 32, - ), + const EdgeInsets.symmetric( + horizontal: 32, + ), child: StackRestoreProgressView( - jsonString: - jsonString, - ), + jsonString: + jsonString, + ), ), const SizedBox( height: 32, @@ -575,18 +571,16 @@ class _RestoreFromFileViewState extends ConsumerState { ); } }, - ), - const SizedBox( - width: 16, - ), - SecondaryButton( - width: 183, - buttonHeight: ButtonHeight.m, - label: "Cancel", - onPressed: () {}, - ), - ], - ), + ), + const SizedBox(width: 16), + SecondaryButton( + width: 183, + buttonHeight: ButtonHeight.m, + label: "Cancel", + onPressed: () {}, + ), + ], + ), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart index da5c0f411..1bb9d1995 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart @@ -24,9 +24,7 @@ import 'create_backup_view.dart'; import 'restore_from_file_view.dart'; class StackBackupView extends StatelessWidget { - const StackBackupView({ - super.key, - }); + const StackBackupView({super.key}); static const String routeName = "/stackBackup"; @@ -48,134 +46,129 @@ class StackBackupView extends StatelessWidget { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: () { - Navigator.of(context).pushNamed(AutoBackupView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupAuto, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Auto Backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of(context).pushNamed(AutoBackupView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupAuto, + height: 28, + width: 28, + ), + const SizedBox(width: 12), + Text( + "Auto Backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context).pushNamed(CreateBackupView.routeName); - // .pushNamed(CreateBackupInfoView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, + const SizedBox(height: 8), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupAdd, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Create manual backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of( + context, + ).pushNamed(CreateBackupView.routeName); + // .pushNamed(CreateBackupInfoView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupAdd, + height: 28, + width: 28, + ), + const SizedBox(width: 12), + Text( + "Create manual backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () { - Navigator.of(context) - .pushNamed(RestoreFromFileView.routeName); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, + const SizedBox(height: 8), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.backupRestore, - height: 28, - width: 28, - ), - const SizedBox( - width: 12, - ), - Text( - "Restore backup", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - ], + onPressed: () { + Navigator.of( + context, + ).pushNamed(RestoreFromFileView.routeName); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.backupRestore, + height: 28, + width: 28, + ), + const SizedBox(width: 12), + Text( + "Restore backup", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/recovery_phrase_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/recovery_phrase_view.dart index dd1478b4a..dfd819506 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/recovery_phrase_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/recovery_phrase_view.dart @@ -8,16 +8,19 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; + import '../../../../../notifications/show_flush_bar.dart'; -import '../../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/assets.dart'; import '../../../../../utilities/clipboard_interface.dart'; import '../../../../../utilities/text_styles.dart'; import '../../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; class RecoverPhraseView extends StatelessWidget { const RecoverPhraseView({ @@ -52,19 +55,18 @@ class RecoverPhraseView extends StatelessWidget { child: AppBarIconButton( color: Theme.of(context).extension()!.background, shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - ), + icon: SvgPicture.asset(Assets.svg.copy, width: 20, height: 20), onPressed: () async { - await clipboardInterface - .setData(ClipboardData(text: mnemonic.join(" "))); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, + await clipboardInterface.setData( + ClipboardData(text: mnemonic.join(" ")), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), ); }, ), @@ -72,41 +74,32 @@ class RecoverPhraseView extends StatelessWidget { ), ], ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - walletName, - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 4), + Text( + walletName, + 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: 12, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: mnemonic, - isDesktop: false, + const SizedBox(height: 4), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox(height: 12), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable(words: mnemonic, isDesktop: false), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart index 2c7ddd71e..7b71dc8c6 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/stack_restore_progress_view.dart @@ -69,37 +69,34 @@ class _StackRestoreProgressViewState showDialog( barrierDismissible: false, context: context, - builder: (_) => WillPopScope( - onWillPop: () async { - return shouldPop; - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Material( - color: Colors.transparent, - child: Center( - child: Text( - "Cancelling restore. Please wait.", - style: STextStyles.pageTitleH2(context).copyWith( - color: - Theme.of(context).extension()!.textWhite, + builder: + (_) => WillPopScope( + onWillPop: () async { + return shouldPop; + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Material( + color: Colors.transparent, + child: Center( + child: Text( + "Cancelling restore. Please wait.", + style: STextStyles.pageTitleH2(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textWhite, + ), + ), ), ), - ), - ), - const SizedBox( - height: 64, + const SizedBox(height: 64), + const Center(child: LoadingIndicator(width: 100)), + ], ), - const Center( - child: LoadingIndicator( - width: 100, - ), - ), - ], - ), - ), + ), ), ); @@ -111,12 +108,12 @@ class _StackRestoreProgressViewState if (mounted) { !isDesktop ? Navigator.of(context).popUntil( - ModalRoute.withName( - widget.fromFile - ? RestoreFromEncryptedStringView.routeName - : StackBackupView.routeName, - ), - ) + ModalRoute.withName( + widget.fromFile + ? RestoreFromEncryptedStringView.routeName + : StackBackupView.routeName, + ), + ) : Navigator.of(context).popUntil((_) => count++ >= 2); } } @@ -169,7 +166,7 @@ class _StackRestoreProgressViewState ref.read(secureStoreProvider), ); } catch (e, s) { - Logging.instance.w("$e\n$s", error: e, stackTrace: s,); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); } if (finished != null && finished && uiState.done) { @@ -224,7 +221,9 @@ class _StackRestoreProgressViewState } void _addWalletsToHomeView() { - ref.read(pWallets).loadAfterStackRestore( + ref + .read(pWallets) + .loadAfterStackRestore( ref.read(prefsChangeNotifierProvider), ref.read(stackRestoringUIStateProvider).wallets, Util.isDesktop, @@ -277,59 +276,95 @@ class _StackRestoreProgressViewState style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: child, ), - child: child, ), ), ); }, child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.only( - left: 4, - top: 4, - right: 4, - bottom: 4, - ), + padding: const EdgeInsets.only(left: 4, top: 4, right: 4, bottom: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Settings", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 12, - ), + Text("Settings", style: STextStyles.itemSubtitle(context)), + const SizedBox(height: 12), Consumer( builder: (_, ref, __) { final state = ref.watch( - stackRestoringUIStateProvider - .select((value) => value.preferences), + stackRestoringUIStateProvider.select( + (value) => value.preferences, + ), ); return !isDesktop ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: + Theme.of( + context, + ).extension()!.buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.gear, + width: 16, + height: 16, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Preferences", + subTitle: + state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.zero, + color: + Theme.of(context).extension()!.popupBG, + borderColor: + Theme.of( + context, + ).extension()!.background, + child: RestoringItemCard( left: SizedBox( width: 32, height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.gear, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -340,84 +375,88 @@ class _StackRestoreProgressViewState child: _getIconForState(state), ), title: "Preferences", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ) - : RoundedContainer( - padding: EdgeInsets.zero, - color: Theme.of(context) - .extension()! - .popupBG, - borderColor: Theme.of(context) - .extension()! - .background, - child: RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.gear, - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Preferences", - subTitle: state == StackRestoringStatus.failed - ? Text( + subTitle: + state == StackRestoringStatus.failed + ? Text( "Something went wrong", style: STextStyles.errorSmall(context), ) - : null, - ), - ); + : null, + ), + ); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Consumer( builder: (_, ref, __) { final state = ref.watch( - stackRestoringUIStateProvider - .select((value) => value.addressBook), + stackRestoringUIStateProvider.select( + (value) => value.addressBook, + ), ); return !isDesktop ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: + Theme.of( + context, + ).extension()!.buttonBackSecondary, + child: Center( + child: AddressBookIcon( + width: 16, + height: 16, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Address book", + subTitle: + state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.zero, + color: + Theme.of(context).extension()!.popupBG, + borderColor: + Theme.of( + context, + ).extension()!.background, + child: RestoringItemCard( left: SizedBox( width: 32, height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: AddressBookIcon( width: 16, height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -428,84 +467,90 @@ class _StackRestoreProgressViewState child: _getIconForState(state), ), title: "Address book", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ) - : RoundedContainer( - padding: EdgeInsets.zero, - color: Theme.of(context) - .extension()! - .popupBG, - borderColor: Theme.of(context) - .extension()! - .background, - child: RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: Center( - child: AddressBookIcon( - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Address book", - subTitle: state == StackRestoringStatus.failed - ? Text( + subTitle: + state == StackRestoringStatus.failed + ? Text( "Something went wrong", style: STextStyles.errorSmall(context), ) - : null, - ), - ); + : null, + ), + ); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Consumer( builder: (_, ref, __) { final state = ref.watch( - stackRestoringUIStateProvider - .select((value) => value.nodes), + stackRestoringUIStateProvider.select( + (value) => value.nodes, + ), ); return !isDesktop ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: + Theme.of( + context, + ).extension()!.buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.node, + width: 16, + height: 16, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Nodes", + subTitle: + state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.zero, + color: + Theme.of(context).extension()!.popupBG, + borderColor: + Theme.of( + context, + ).extension()!.background, + child: RestoringItemCard( left: SizedBox( width: 32, height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.node, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -516,85 +561,90 @@ class _StackRestoreProgressViewState child: _getIconForState(state), ), title: "Nodes", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ) - : RoundedContainer( - padding: EdgeInsets.zero, - color: Theme.of(context) - .extension()! - .popupBG, - borderColor: Theme.of(context) - .extension()! - .background, - child: RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.node, - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Nodes", - subTitle: state == StackRestoringStatus.failed - ? Text( + subTitle: + state == StackRestoringStatus.failed + ? Text( "Something went wrong", style: STextStyles.errorSmall(context), ) - : null, - ), - ); + : null, + ), + ); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Consumer( builder: (_, ref, __) { final state = ref.watch( - stackRestoringUIStateProvider - .select((value) => value.trades), + stackRestoringUIStateProvider.select( + (value) => value.trades, + ), ); return !isDesktop ? RestoringItemCard( + left: SizedBox( + width: 32, + height: 32, + child: RoundedContainer( + padding: const EdgeInsets.all(0), + color: + Theme.of( + context, + ).extension()!.buttonBackSecondary, + child: Center( + child: SvgPicture.asset( + Assets.svg.arrowsTwoWay, + width: 16, + height: 16, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), + ), + ), + right: SizedBox( + width: 20, + height: 20, + child: _getIconForState(state), + ), + title: "Exchange history", + subTitle: + state == StackRestoringStatus.failed + ? Text( + "Something went wrong", + style: STextStyles.errorSmall(context), + ) + : null, + ) + : RoundedContainer( + padding: EdgeInsets.zero, + color: + Theme.of(context).extension()!.popupBG, + borderColor: + Theme.of( + context, + ).extension()!.background, + child: RestoringItemCard( left: SizedBox( width: 32, height: 32, child: RoundedContainer( padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, + color: + Theme.of(context) + .extension()! + .buttonBackSecondary, child: Center( child: SvgPicture.asset( Assets.svg.arrowsTwoWay, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -605,123 +655,72 @@ class _StackRestoreProgressViewState child: _getIconForState(state), ), title: "Exchange history", - subTitle: state == StackRestoringStatus.failed - ? Text( - "Something went wrong", - style: STextStyles.errorSmall(context), - ) - : null, - ) - : RoundedContainer( - padding: EdgeInsets.zero, - color: Theme.of(context) - .extension()! - .popupBG, - borderColor: Theme.of(context) - .extension()! - .background, - child: RestoringItemCard( - left: SizedBox( - width: 32, - height: 32, - child: RoundedContainer( - padding: const EdgeInsets.all(0), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - child: Center( - child: SvgPicture.asset( - Assets.svg.arrowsTwoWay, - width: 16, - height: 16, - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - ), - right: SizedBox( - width: 20, - height: 20, - child: _getIconForState(state), - ), - title: "Exchange history", - subTitle: state == StackRestoringStatus.failed - ? Text( + subTitle: + state == StackRestoringStatus.failed + ? Text( "Something went wrong", style: STextStyles.errorSmall(context), ) - : null, - ), - ); + : null, + ), + ); }, ), - const SizedBox( - height: 16, - ), - Text( - "Wallets", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 16), + Text("Wallets", style: STextStyles.itemSubtitle(context)), + const SizedBox(height: 8), ...ref .watch( - stackRestoringUIStateProvider - .select((value) => value.walletStateProviders), + stackRestoringUIStateProvider.select( + (value) => value.walletStateProviders, + ), ) .values .map( (provider) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: RestoringWalletCard( - provider: provider, - ), + child: RestoringWalletCard(provider: provider), ), ), - const SizedBox( - height: 30, - ), + const SizedBox(height: 30), SizedBox( width: MediaQuery.of(context).size.width - 32, - child: !isDesktop - ? TextButton( - onPressed: () async { - if (_success) { - if (widget.shouldPushToHome) { - Navigator.of(context).popUntil( - ModalRoute.withName( - HomeView.routeName, - ), - ); + child: + !isDesktop + ? TextButton( + onPressed: () async { + if (_success) { + if (widget.shouldPushToHome) { + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName), + ); + } else { + Navigator.of(context).pop(); + } } else { - Navigator.of(context).pop(); - } - } else { - if (await _requestCancel()) { - await _cancel(); + if (await _requestCancel()) { + await _cancel(); + } } - } - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - _success ? "OK" : "Cancel restore process", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + _success ? "OK" : "Cancel restore process", + style: STextStyles.button(context).copyWith( + color: + Theme.of( + context, + ).extension()!.buttonTextPrimary, + ), ), - ), - ) - : Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _success - ? PrimaryButton( + ) + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _success + ? PrimaryButton( width: 248, buttonHeight: ButtonHeight.l, enabled: true, @@ -738,15 +737,18 @@ class _StackRestoreProgressViewState if (widget.shouldPushToHome) { unawaited( - Navigator.of(context) - .pushNamedAndRemoveUntil( + Navigator.of( + context, + ).pushNamedAndRemoveUntil( DesktopHomeView.routeName, (route) => false, ), ); } else { - Navigator.of(context, rootNavigator: true) - .popUntil( + Navigator.of( + context, + rootNavigator: true, + ).popUntil( ModalRoute.withName( DesktopHomeView.routeName, ), @@ -754,7 +756,7 @@ class _StackRestoreProgressViewState } }, ) - : SecondaryButton( + : SecondaryButton( width: 248, buttonHeight: ButtonHeight.l, enabled: true, @@ -765,8 +767,8 @@ class _StackRestoreProgressViewState } }, ), - ], - ), + ], + ), ), ], ), diff --git a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart index 3970ff9c8..2e902ede6 100644 --- a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart +++ b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart @@ -79,188 +79,193 @@ class _StartupPreferencesViewState style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: () { - ref - .read(prefsChangeNotifierProvider) - .gotoWalletOnStartup = false; - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: false, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select( - (value) => - value.gotoWalletOnStartup, + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup = false; + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: + Theme.of(context) + .extension< + StackColors + >()! + .radioButtonIconEnabled, + value: false, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select( + (value) => + value + .gotoWalletOnStartup, + ), ), + onChanged: (value) { + if (value is bool) { + ref + .read( + prefsChangeNotifierProvider, + ) + .gotoWalletOnStartup = value; + } + }, ), - onChanged: (value) { - if (value is bool) { - ref - .read( - prefsChangeNotifierProvider, - ) - .gotoWalletOnStartup = - value; - } - }, ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Home screen", - style: - STextStyles.titleBold12( - context, + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Home screen", + style: + STextStyles.titleBold12( + context, + ), + textAlign: TextAlign.left, ), - textAlign: TextAlign.left, - ), - Text( - "${AppConfig.appName} home screen", - style: - STextStyles.itemSubtitle( - context, + Text( + "${AppConfig.appName} home screen", + style: + STextStyles.itemSubtitle( + context, + ), + textAlign: TextAlign.left, ), - textAlign: TextAlign.left, - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ), ), - ), - Padding( - padding: const EdgeInsets.all(4), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + Padding( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: () { - ref - .read(prefsChangeNotifierProvider) - .gotoWalletOnStartup = true; - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: true, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select( - (value) => - value.gotoWalletOnStartup, + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .gotoWalletOnStartup = true; + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: + Theme.of(context) + .extension< + StackColors + >()! + .radioButtonIconEnabled, + value: true, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select( + (value) => + value + .gotoWalletOnStartup, + ), ), + onChanged: (value) { + if (value is bool) { + ref + .read( + prefsChangeNotifierProvider, + ) + .gotoWalletOnStartup = value; + } + }, ), - onChanged: (value) { - if (value is bool) { - ref - .read( - prefsChangeNotifierProvider, - ) - .gotoWalletOnStartup = - value; - } - }, ), - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Specific wallet", - style: - STextStyles.titleBold12( - context, + const SizedBox(width: 12), + Flexible( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Specific wallet", + style: + STextStyles.titleBold12( + context, + ), + textAlign: TextAlign.left, ), - textAlign: TextAlign.left, - ), - (safe && - ref.watch( - prefsChangeNotifierProvider - .select( - (value) => value - .startupWalletId, - ), - ) != - null) - ? Padding( + (safe && + ref.watch( + prefsChangeNotifierProvider + .select( + (value) => + value + .startupWalletId, + ), + ) != + null) + ? Padding( padding: - const EdgeInsets - .only(top: 12), + const EdgeInsets.only( + top: 12, + ), child: Row( children: [ SvgPicture.file( @@ -270,9 +275,10 @@ class _StartupPreferencesViewState ref.watch( pWalletCoin( ref.watch( - prefsChangeNotifierProvider - .select( - (value) => + prefsChangeNotifierProvider.select( + ( + value, + ) => value.startupWalletId!, ), ), @@ -291,115 +297,116 @@ class _StartupPreferencesViewState ref.watch( prefsChangeNotifierProvider .select( - (value) => - value - .startupWalletId!, - ), + ( + value, + ) => + value.startupWalletId!, + ), ), ), ), - style: STextStyles - .itemSubtitle( - context, - ), + style: + STextStyles.itemSubtitle( + context, + ), ), ], ), ) - : Text( + : Text( "Select a specific wallet to load into on startup", - style: STextStyles - .itemSubtitle( - context, - ), + style: + STextStyles.itemSubtitle( + context, + ), textAlign: TextAlign.left, ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ), ), - ), - if (!ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.gotoWalletOnStartup, - ), - )) - const SizedBox( - height: 12, - ), - if (ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.gotoWalletOnStartup, - ), - )) - Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - right: 12, - bottom: 12, - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const SizedBox( - width: 12 + 20, - height: 12, - ), - Flexible( - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize - .shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular( - Constants - .size.circularBorderRadius, + if (!ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.gotoWalletOnStartup, + ), + )) + const SizedBox(height: 12), + if (ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.gotoWalletOnStartup, + ), + )) + Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + right: 12, + bottom: 12, + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const SizedBox( + width: 12 + 20, + height: 12, + ), + Flexible( + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize + .shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + Constants + .size + .circularBorderRadius, + ), ), - ), - onPressed: () { - Navigator.of(context).pushNamed( - StartupWalletSelectionView - .routeName, - ); - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Select wallet...", - style: STextStyles.link2( - context, + onPressed: () { + Navigator.of(context).pushNamed( + StartupWalletSelectionView + .routeName, + ); + }, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Select wallet...", + style: STextStyles.link2( + context, + ), + textAlign: TextAlign.left, ), - textAlign: TextAlign.left, - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), diff --git a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart index 7c073c5ce..0caa4720d 100644 --- a/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart +++ b/lib/pages/settings_views/global_settings_view/startup_preferences/startup_wallet_selection_view.dart @@ -13,6 +13,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; + import '../../../../providers/providers.dart'; import '../../../../themes/coin_icon_provider.dart'; import '../../../../themes/stack_colors.dart'; @@ -64,180 +65,179 @@ class _StartupWalletSelectionViewState ), ), ), - body: LayoutBuilder( - builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 4, - ), - Text( - "Select a wallet to load into immediately on startup", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: Column( - children: [ - ...wallets.map( - (wallet) => Padding( - padding: const EdgeInsets.all(12), - child: Row( - key: Key( - "startupWalletSelectionGroupKey_${wallet.walletId}", - ), - children: [ - Container( - decoration: BoxDecoration( - color: ref - .watch( - pCoinColor( - ref.watch( - pWalletCoin( - wallet.walletId, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + "Select a wallet to load into immediately on startup", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: Column( + children: [ + ...wallets.map( + (wallet) => Padding( + padding: const EdgeInsets.all(12), + child: Row( + key: Key( + "startupWalletSelectionGroupKey_${wallet.walletId}", + ), + children: [ + Container( + decoration: BoxDecoration( + color: ref + .watch( + pCoinColor( + ref.watch( + pWalletCoin( + wallet.walletId, + ), ), ), + ) + .withOpacity(0.5), + borderRadius: + BorderRadius.circular( + Constants + .size + .circularBorderRadius, ), - ) - .withOpacity(0.5), - borderRadius: BorderRadius.circular( - Constants - .size.circularBorderRadius, ), - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.file( - File( - ref.watch( - coinIconProvider( - ref.watch( - pWalletCoin( - wallet.walletId, + child: Padding( + padding: const EdgeInsets.all(4), + child: SvgPicture.file( + File( + ref.watch( + coinIconProvider( + ref.watch( + pWalletCoin( + wallet.walletId, + ), ), ), ), ), + width: 20, + height: 20, ), - width: 20, - height: 20, ), ), - ), - const SizedBox( - width: 12, - ), - Expanded( - child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - ref.watch( - pWalletName(wallet.walletId), - ), - style: STextStyles.titleBold12( - context, + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ref.watch( + pWalletName( + wallet.walletId, + ), + ), + style: + STextStyles.titleBold12( + context, + ), ), - ), - // const SizedBox( - // height: 2, - // ), - // FutureBuilder( - // future: manager.totalBalance, - // builder: (builderContext, - // AsyncSnapshot snapshot) { - // if (snapshot.connectionState == - // ConnectionState.done && - // snapshot.hasData) { - // return Text( - // "${Format.localizedStringAsFixed( - // value: snapshot.data!, - // locale: ref.watch( - // localeServiceChangeNotifierProvider - // .select((value) => - // value.locale)), - // decimalPlaces: 8, - // )} ${manager.coin.ticker}", - // style: STextStyles.itemSubtitle(context), - // ); - // } else { - // return AnimatedText( - // stringsToLoopThrough: const [ - // "Loading balance", - // "Loading balance.", - // "Loading balance..", - // "Loading balance..." - // ], - // style: STextStyles.itemSubtitle(context), - // ); - // } - // }, - // ), - ], + // const SizedBox( + // height: 2, + // ), + // FutureBuilder( + // future: manager.totalBalance, + // builder: (builderContext, + // AsyncSnapshot snapshot) { + // if (snapshot.connectionState == + // ConnectionState.done && + // snapshot.hasData) { + // return Text( + // "${Format.localizedStringAsFixed( + // value: snapshot.data!, + // locale: ref.watch( + // localeServiceChangeNotifierProvider + // .select((value) => + // value.locale)), + // decimalPlaces: 8, + // )} ${manager.coin.ticker}", + // style: STextStyles.itemSubtitle(context), + // ); + // } else { + // return AnimatedText( + // stringsToLoopThrough: const [ + // "Loading balance", + // "Loading balance.", + // "Loading balance..", + // "Loading balance..." + // ], + // style: STextStyles.itemSubtitle(context), + // ); + // } + // }, + // ), + ], + ), ), - ), - SizedBox( - height: 20, - width: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: wallet.walletId, - groupValue: ref.watch( - prefsChangeNotifierProvider - .select( - (value) => - value.startupWalletId, + SizedBox( + height: 20, + width: 20, + child: Radio( + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: wallet.walletId, + groupValue: ref.watch( + prefsChangeNotifierProvider + .select( + (value) => + value.startupWalletId, + ), ), + onChanged: (value) { + if (value is String) { + ref + .read( + prefsChangeNotifierProvider, + ) + .startupWalletId = value; + } + }, ), - onChanged: (value) { - if (value is String) { - ref - .read( - prefsChangeNotifierProvider, - ) - .startupWalletId = value; - } - }, ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/support_view.dart b/lib/pages/settings_views/global_settings_view/support_view.dart index 8c348d99a..a88e47099 100644 --- a/lib/pages/settings_views/global_settings_view/support_view.dart +++ b/lib/pages/settings_views/global_settings_view/support_view.dart @@ -10,6 +10,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + import '../../../app_config.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; @@ -19,13 +21,12 @@ import '../../../utilities/util.dart'; import '../../../widgets/background.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/rounded_white_container.dart'; -import 'package:url_launcher/url_launcher.dart'; class SupportView extends StatelessWidget { - const SupportView({ - super.key, - }); + const SupportView({super.key}); static const String routeName = "/support"; @@ -48,14 +49,10 @@ class SupportView extends StatelessWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Support", - style: STextStyles.navBarTitle(context), - ), + title: Text("Support", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, + body: SafeArea( + child: Padding(padding: const EdgeInsets.all(16), child: child), ), ), ); @@ -69,13 +66,7 @@ class SupportView extends StatelessWidget { style: STextStyles.smallMed12(context), ), ), - isDesktop - ? const SizedBox( - height: 24, - ) - : const SizedBox( - height: 12, - ), + isDesktop ? const SizedBox(height: 24) : const SizedBox(height: 12), AboutItem( linkUrl: "https://t.me/stackwallet", label: "Telegram", @@ -83,9 +74,7 @@ class SupportView extends StatelessWidget { iconAsset: Assets.socials.telegram, isDesktop: isDesktop, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), AboutItem( linkUrl: "https://discord.com/invite/mRPZuXx3At", label: "Discord", @@ -93,9 +82,7 @@ class SupportView extends StatelessWidget { iconAsset: Assets.socials.discord, isDesktop: isDesktop, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), AboutItem( linkUrl: "https://www.reddit.com/r/stackwallet/", label: "Reddit", @@ -103,19 +90,15 @@ class SupportView extends StatelessWidget { iconAsset: Assets.socials.reddit, isDesktop: isDesktop, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), AboutItem( - linkUrl: "https://twitter.com/stack_wallet", - label: "Twitter", + linkUrl: "https://x.com/stack_wallet", + label: "X", buttonText: "@stack_wallet", iconAsset: Assets.socials.twitter, isDesktop: isDesktop, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), AboutItem( linkUrl: "mailto:support@stackwallet.com", label: "Email", @@ -159,22 +142,32 @@ class AboutItem extends StatelessWidget { Constants.size.circularBorderRadius, ), ), - onPressed: () { - launchUrl( - Uri.parse(linkUrl), - mode: LaunchMode.externalApplication, - ); + onPressed: () async { + if (label == "Email") { + await launchUrl( + Uri.parse(linkUrl), + mode: LaunchMode.externalApplication, + ); + } else { + await showDialog( + context: context, + builder: + (_) => ScamWarningDialog( + channel: label, + onUnderstandPressed: + () => launchUrl( + Uri.parse(linkUrl), + mode: LaunchMode.externalApplication, + ), + ), + ); + } }, child: Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 20, - vertical: 15, - ) - : const EdgeInsets.symmetric( - horizontal: 12, - vertical: 20, - ), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 20, vertical: 15) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -182,31 +175,30 @@ class AboutItem extends StatelessWidget { children: [ ConditionalParent( condition: isDesktop, - builder: (child) => Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10000), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, - ), - child: Center( - child: child, - ), - ), + builder: + (child) => Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10000), + color: + Theme.of( + context, + ).extension()!.buttonBackSecondary, + ), + child: Center(child: child), + ), child: SvgPicture.asset( iconAsset, width: iconSize, height: iconSize, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( label, style: STextStyles.titleBold12(context), @@ -235,3 +227,182 @@ class AboutItem extends StatelessWidget { ); } } + +class ScamWarningDialog extends StatelessWidget { + const ScamWarningDialog({ + super.key, + required this.onUnderstandPressed, + required this.channel, + }); + + final String channel; + final VoidCallback onUnderstandPressed; + + @override + Widget build(BuildContext context) { + return SDialog( + padding: EdgeInsets.all(Util.isDesktop ? 32 : 16), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => IntrinsicWidth(child: child), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan( + style: + Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_14(context), + children: [ + TextSpan( + text: "Important: Protect Yourself from Scammers!\n\n", + style: + Util.isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH2(context), + ), + const TextSpan( + text: "All official support for ", + style: TextStyle(fontWeight: FontWeight.normal), + ), + const TextSpan( + text: AppConfig.appName, + style: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: " in ", + style: TextStyle(fontWeight: FontWeight.normal), + ), + TextSpan( + text: channel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: " is provided ", + style: TextStyle(fontWeight: FontWeight.normal), + ), + const TextSpan( + text: "ONLY", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: " in public channels.\n\n", + style: TextStyle(fontWeight: FontWeight.normal), + ), + ], + ), + ), + const _Bullet( + text: + "Never trust direct messages (DMs) from anyone" + " claiming to be support staff.\n", + ), + const _Bullet( + text: + "Do not share personal information," + " wallet details, or private keys.\n", + ), + const _Bullet( + text: + "If someone asks you to send them money or crypto," + " they are a scammer.\n\n", + ), + RichText( + text: TextSpan( + style: + Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_14(context), + children: const [ + TextSpan( + text: "Our support staff will ", + style: TextStyle(fontWeight: FontWeight.normal), + ), + TextSpan( + text: "*never*", + style: TextStyle( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + " contact you privately first. " + "They will only help you in the public chat.", + style: TextStyle(fontWeight: FontWeight.normal), + ), + ], + ), + ), + SizedBox(height: Util.isDesktop ? 40 : 32), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!Util.isDesktop) const Spacer(), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded(child: child), + child: PrimaryButton( + width: Util.isDesktop ? 240 : null, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + label: "I UNDERSTAND", + onPressed: onUnderstandPressed, + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _Bullet extends StatelessWidget { + const _Bullet({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: + Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_14(context), + children: const [ + TextSpan( + text: " • ", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded(child: child), + child: RichText( + text: TextSpan( + style: + Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_14(context), + children: [ + TextSpan( + text: text, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + ), + ), + ], + ); + } +} 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 b036f89f8..7a0644f4d 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 @@ -46,26 +46,23 @@ class SyncingOptionsView extends ConsumerWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Syncing", - style: STextStyles.navBarTitle(context), - ), + title: Text("Syncing", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: child, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), ), - ), - ); - }, + ); + }, + ), ), ), ), @@ -114,13 +111,15 @@ class SyncingOptionsView extends ConsumerWidget { width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: SyncingType.currentWalletOnly, groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.syncType), + prefsChangeNotifierProvider.select( + (value) => value.syncType, + ), ), onChanged: (value) { if (value is SyncingType) { @@ -131,9 +130,7 @@ class SyncingOptionsView extends ConsumerWidget { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -192,13 +189,15 @@ class SyncingOptionsView extends ConsumerWidget { width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: SyncingType.allWalletsOnStartup, groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.syncType), + prefsChangeNotifierProvider.select( + (value) => value.syncType, + ), ), onChanged: (value) { if (value is SyncingType) { @@ -209,9 +208,7 @@ class SyncingOptionsView extends ConsumerWidget { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -272,13 +269,15 @@ class SyncingOptionsView extends ConsumerWidget { width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: SyncingType.selectedWalletsAtStartup, groupValue: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.syncType), + prefsChangeNotifierProvider.select( + (value) => value.syncType, + ), ), onChanged: (value) { if (value is SyncingType) { @@ -289,9 +288,7 @@ class SyncingOptionsView extends ConsumerWidget { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -316,16 +313,16 @@ class SyncingOptionsView extends ConsumerWidget { ), ), if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.syncType), + prefsChangeNotifierProvider.select( + (value) => value.syncType, + ), ) != SyncingType.selectedWalletsAtStartup) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.syncType), + prefsChangeNotifierProvider.select( + (value) => value.syncType, + ), ) == SyncingType.selectedWalletsAtStartup) Container( @@ -339,10 +336,7 @@ class SyncingOptionsView extends ConsumerWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - width: 12 + 20, - height: 12, - ), + const SizedBox(width: 12 + 20, height: 12), Flexible( child: RawMaterialButton( // splashColor: Theme.of(context).extension()!.highlight, @@ -356,48 +350,50 @@ class SyncingOptionsView extends ConsumerWidget { onPressed: () { !isDesktop ? Navigator.of(context).pushNamed( - WalletSyncingOptionsView.routeName, - ) + WalletSyncingOptionsView.routeName, + ) : showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return DesktopDialog( - maxWidth: 600, - maxHeight: 800, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Padding( - padding: - const EdgeInsets.all( - 32, - ), - child: Text( - "Select wallets to sync", - style: STextStyles - .desktopH3(context), - textAlign: - TextAlign.center, - ), + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxWidth: 600, + maxHeight: 800, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.all( + 32, + ), + child: Text( + "Select wallets to sync", + style: + STextStyles.desktopH3( + context, + ), + textAlign: + TextAlign.center, ), - const DesktopDialogCloseButton(), - ], - ), - const Expanded( - child: - WalletSyncingOptionsView(), - ), - ], - ), - ); - }, - ); + ), + const DesktopDialogCloseButton(), + ], + ), + const Expanded( + child: + WalletSyncingOptionsView(), + ), + ], + ), + ); + }, + ); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart index 988d951f1..7c29aa9b0 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart @@ -54,127 +54,136 @@ class SyncingPreferencesView extends ConsumerWidget { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( padding: const EdgeInsets.all(0), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + padding: const EdgeInsets.all(0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: () { - Navigator.of(context) - .pushNamed(SyncingOptionsView.routeName); - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Syncing", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - Text( - _currentTypeDescription( - ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.syncType, + onPressed: () { + Navigator.of( + context, + ).pushNamed(SyncingOptionsView.routeName); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Syncing", + style: STextStyles.titleBold12( + context, + ), + textAlign: TextAlign.left, + ), + Text( + _currentTypeDescription( + ref.watch( + prefsChangeNotifierProvider + .select( + (value) => value.syncType, + ), ), ), + style: STextStyles.itemSubtitle( + context, + ), + textAlign: TextAlign.left, ), - style: - STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - ], - ), - const Spacer(), - ], + ], + ), + const Spacer(), + ], + ), ), ), ), - ), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "AutoSync only on Wi-Fi", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.wifiOnly, + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "AutoSync only on Wi-Fi", + style: STextStyles.titleBold12( + context, + ), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select( + (value) => value.wifiOnly, + ), ), + onValueChanged: (newValue) { + ref + .read( + prefsChangeNotifierProvider, + ) + .wifiOnly = newValue; + }, ), - onValueChanged: (newValue) { - ref - .read( - prefsChangeNotifierProvider, - ) - .wifiOnly = newValue; - }, ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ), 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 f974226b6..c5916cf70 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 @@ -60,13 +60,11 @@ class WalletSyncingOptionsView extends ConsumerWidget { ), ), ), - body: Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: child, ), - child: child, ), ), ); @@ -92,23 +90,20 @@ class WalletSyncingOptionsView extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( "Choose the wallets to sync automatically at startup", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( padding: const EdgeInsets.all(0), - borderColor: !isDesktop - ? Colors.transparent - : Theme.of(context) - .extension()! - .background, + borderColor: + !isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.background, child: Column( children: [ ...walletInfos.map( @@ -141,9 +136,7 @@ class WalletSyncingOptionsView extends ConsumerWidget { ), ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -153,11 +146,10 @@ class WalletSyncingOptionsView extends ConsumerWidget { Text( info.name, style: STextStyles.titleBold12( - context), - ), - const SizedBox( - height: 2, + context, + ), ), + const SizedBox(height: 2), Text( ref .watch( @@ -173,7 +165,8 @@ class WalletSyncingOptionsView extends ConsumerWidget { .total, ), style: STextStyles.itemSubtitle( - context), + context, + ), ), ], ), @@ -184,10 +177,10 @@ class WalletSyncingOptionsView extends ConsumerWidget { child: DraggableSwitchButton( isOn: ref .watch( - prefsChangeNotifierProvider - .select( - (value) => value - .walletIdsSyncOnStartup, + prefsChangeNotifierProvider.select( + (value) => + value + .walletIdsSyncOnStartup, ), ) .contains(info.walletId), @@ -195,11 +188,13 @@ class WalletSyncingOptionsView extends ConsumerWidget { // final syncType = ref // .read(prefsChangeNotifierProvider) // .syncType; - final ids = ref - .read( - prefsChangeNotifierProvider) - .walletIdsSyncOnStartup - .toList(); + final ids = + ref + .read( + prefsChangeNotifierProvider, + ) + .walletIdsSyncOnStartup + .toList(); if (value) { ids.add(info.walletId); } else { @@ -228,7 +223,8 @@ class WalletSyncingOptionsView extends ConsumerWidget { ref .read( - prefsChangeNotifierProvider) + prefsChangeNotifierProvider, + ) .walletIdsSyncOnStartup = ids; }, ), diff --git a/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart b/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart index 245937fdf..24800936d 100644 --- a/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart @@ -34,9 +34,7 @@ import '../../../../widgets/stack_dialog.dart'; import '../../../../widgets/tor_subscription.dart'; class TorSettingsView extends ConsumerStatefulWidget { - const TorSettingsView({ - super.key, - }); + const TorSettingsView({super.key}); static const String routeName = "/torSettings"; @@ -59,17 +57,12 @@ class _TorSettingsViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Tor settings", - style: STextStyles.navBarTitle(context), - ), + title: Text("Tor settings", style: STextStyles.navBarTitle(context)), actions: [ AspectRatio( aspectRatio: 1, child: AppBarIconButton( - icon: SvgPicture.asset( - Assets.svg.circleQuestion, - ), + icon: SvgPicture.asset(Assets.svg.circleQuestion), onPressed: () { showDialog( context: context, @@ -94,107 +87,107 @@ class _TorSettingsViewState extends ConsumerState { ), ], ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.all(10.0), - child: TorAnimatedButton(), - ), - ], - ), - const SizedBox( - height: 30, - ), - const TorButton(), - const SizedBox( - height: 8, - ), - RoundedWhiteContainer( - child: Consumer( - builder: (_, ref, __) { - return RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all(10.0), + child: TorAnimatedButton(), + ), + ], + ), + const SizedBox(height: 30), + const TorButton(), + const SizedBox(height: 8), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - onPressed: null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - "Tor killswitch", - style: STextStyles.titleBold12(context), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "What is Tor killswitch?", - message: - "A security feature that protects your information from accidental exposure by" - " disconnecting your device from the Tor network if the" - " connection is disrupted or compromised.", - rightButton: SecondaryButton( - label: "Close", - onPressed: - Navigator.of(context).pop, - ), - ); - }, - ); - }, - child: SvgPicture.asset( - Assets.svg.circleInfo, - height: 16, - width: 16, - color: Theme.of(context) - .extension()! - .infoItemLabel, + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "Tor killswitch", + style: STextStyles.titleBold12(context), ), + const SizedBox(width: 8), + GestureDetector( + onTap: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "What is Tor killswitch?", + message: + "A security feature that protects your information from accidental exposure by" + " disconnecting your device from the Tor network if the" + " connection is disrupted or compromised.", + rightButton: SecondaryButton( + label: "Close", + onPressed: + Navigator.of(context).pop, + ), + ); + }, + ); + }, + child: SvgPicture.asset( + Assets.svg.circleInfo, + height: 16, + width: 16, + color: + Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + ], + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.torKillSwitch, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .torKillSwitch = newValue; + }, ), - ], - ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.torKillSwitch), - ), - onValueChanged: (newValue) { - ref - .read(prefsChangeNotifierProvider) - .torKillSwitch = newValue; - }, ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), - ], + ], + ), ), ), ), @@ -248,11 +241,7 @@ class _TorAnimatedButtonState extends ConsumerState } Future _playPlug() async { - await _play( - from: "0.0", - to: "connecting-start", - repeat: false, - ); + await _play(from: "0.0", to: "connecting-start", repeat: false); } Future _playConnecting({double? start}) async { @@ -272,11 +261,7 @@ class _TorAnimatedButtonState extends ConsumerState } Future _playConnected() async { - await _play( - from: "connected-start", - to: "connected-end", - repeat: true, - ); + await _play(from: "connected-start", to: "connected-end", repeat: true); } Future _playDisconnect() async { @@ -358,10 +343,7 @@ class _TorAnimatedButtonState extends ConsumerState }, child: ConditionalParent( condition: _status != TorConnectionStatus.connecting, - builder: (child) => GestureDetector( - onTap: onTap, - child: child, - ), + builder: (child) => GestureDetector(onTap: onTap, child: child), child: Column( children: [ SizedBox( @@ -401,10 +383,7 @@ class TorButton extends ConsumerStatefulWidget { class _TorButtonState extends ConsumerState { late TorConnectionStatus _status; - Color _color( - TorConnectionStatus status, - StackColors colors, - ) { + Color _color(TorConnectionStatus status, StackColors colors) { switch (status) { case TorConnectionStatus.disconnected: return colors.textSubtitle3; @@ -417,10 +396,7 @@ class _TorButtonState extends ConsumerState { } } - String _label( - TorConnectionStatus status, - StackColors colors, - ) { + String _label(TorConnectionStatus status, StackColors colors) { switch (status) { case TorConnectionStatus.disconnected: return "Disconnected"; @@ -487,16 +463,10 @@ class _TorButtonState extends ConsumerState { padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( children: [ - Text( - "Tor status", - style: STextStyles.titleBold12(context), - ), + Text("Tor status", style: STextStyles.titleBold12(context)), const Spacer(), Text( - _label( - _status, - Theme.of(context).extension()!, - ), + _label(_status, Theme.of(context).extension()!), style: STextStyles.itemSubtitle(context).copyWith( color: _color( _status, @@ -523,10 +493,7 @@ class UpperCaseTorText extends ConsumerStatefulWidget { class _UpperCaseTorTextState extends ConsumerState { late TorConnectionStatus _status; - Color _color( - TorConnectionStatus status, - StackColors colors, - ) { + Color _color(TorConnectionStatus status, StackColors colors) { switch (status) { case TorConnectionStatus.disconnected: return colors.textSubtitle3; @@ -539,9 +506,7 @@ class _UpperCaseTorTextState extends ConsumerState { } } - String _label( - TorConnectionStatus status, - ) { + String _label(TorConnectionStatus status) { switch (status) { case TorConnectionStatus.disconnected: return "CONNECT"; @@ -570,16 +535,9 @@ class _UpperCaseTorTextState extends ConsumerState { }); }, child: Text( - _label( - _status, - ), - style: STextStyles.pageTitleH2( - context, - ).copyWith( - color: _color( - _status, - Theme.of(context).extension()!, - ), + _label(_status), + style: STextStyles.pageTitleH2(context).copyWith( + color: _color(_status, Theme.of(context).extension()!), ), ), ); 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 3404b395e..812639eb4 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 @@ -34,10 +34,7 @@ import 'frost_participants_view.dart'; import 'initiate_resharing/initiate_resharing_view.dart'; class FrostMSWalletOptionsView extends ConsumerWidget { - const FrostMSWalletOptionsView({ - super.key, - required this.walletId, - }); + const FrostMSWalletOptionsView({super.key, required this.walletId}); static const String routeName = "/frostMSWalletOptionsView"; @@ -49,44 +46,39 @@ class FrostMSWalletOptionsView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return ConditionalParent( condition: Util.isDesktop, - builder: (child) => DesktopScaffold( - background: Theme.of(context).extension()!.background, - appBar: const DesktopAppBar( - isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), - ), - body: SizedBox( - width: 480, - child: child, - ), - ), + builder: + (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox(width: 480, child: child), + ), child: ConditionalParent( condition: !Util.isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "FROST Multisig options", - style: STextStyles.navBarTitle(context), + 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: SafeArea(child: child), ), ), - body: child, - ), - ), child: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -105,9 +97,7 @@ class FrostMSWalletOptionsView extends ConsumerWidget { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( padding: EdgeInsets.zero, child: SettingsListButton( @@ -116,11 +106,12 @@ class FrostMSWalletOptionsView extends ConsumerWidget { 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)!; + final frostInfo = + ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; ref.read(pFrostMyName.state).state = frostInfo.myName; @@ -131,9 +122,7 @@ class FrostMSWalletOptionsView extends ConsumerWidget { }, ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( padding: EdgeInsets.zero, child: SettingsListButton( @@ -143,16 +132,18 @@ class FrostMSWalletOptionsView extends ConsumerWidget { iconSize: 16, onPressed: () { // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) - final frostInfo = ref - .read(mainDBProvider) - .isar - .frostWalletInfo - .getByWalletIdSync(walletId)!; + final frostInfo = + ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; ref.read(pFrostMyName.state).state = frostInfo.myName; - final wallet = ref.read(pWallets).getWallet(walletId) - as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(walletId) + as BitcoinFrostWallet; ref.read(pFrostScaffoldArgs.state).state = ( info: ( @@ -167,9 +158,9 @@ class FrostMSWalletOptionsView extends ConsumerWidget { callerRouteName: FrostMSWalletOptionsView.routeName, ); - Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - ); + Navigator.of( + context, + ).pushNamed(FrostStepScaffold.routeName); }, ), ), 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 75b8ca87b..d94cca088 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 @@ -62,7 +62,8 @@ class WalletBackupView extends ConsumerWidget { String config, String keys, ({String config, String keys})? prevGen, - })? frostWalletData; + })? + frostWalletData; final KeyDataInterface? keyData; @override @@ -80,10 +81,7 @@ class WalletBackupView extends ConsumerWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Wallet backup", - style: STextStyles.navBarTitle(context), - ), + title: Text("Wallet backup", style: STextStyles.navBarTitle(context)), actions: [ if (keyData != null) Padding( @@ -93,7 +91,8 @@ class WalletBackupView extends ConsumerWidget { final XPrivData _ => "xpriv(s)", final CWKeyData _ => "keys", final ViewOnlyWalletData _ => "keys", - _ => throw UnimplementedError( + _ => + throw UnimplementedError( "Don't forget to add your KeyDataInterface here! ${keyData.runtimeType}", ), }, @@ -101,27 +100,24 @@ class WalletBackupView extends ConsumerWidget { Navigator.pushNamed( context, MobileKeyDataView.routeName, - arguments: ( - walletId: walletId, - keyData: keyData!, - ), + arguments: (walletId: walletId, keyData: keyData!), ); }, ), ), ], ), - body: Padding( - padding: const EdgeInsets.all(16), - child: frost - ? _FrostKeys( - frostWalletData: frostWalletData, - walletId: walletId, - ) - : _Mnemonic( - walletId: walletId, - mnemonic: mnemonic, - ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: + frost + ? _FrostKeys( + frostWalletData: frostWalletData, + walletId: walletId, + ) + : _Mnemonic(walletId: walletId, mnemonic: mnemonic), + ), ), ), ); @@ -148,21 +144,15 @@ class _Mnemonic extends ConsumerWidget { Text( ref.watch(pWalletName(walletId)), textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 4, + 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, - ), + const SizedBox(height: 16), Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, @@ -182,25 +172,19 @@ class _Mnemonic extends ConsumerWidget { ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: SingleChildScrollView( - child: MnemonicTable( - words: mnemonic, - isDesktop: false, - ), + child: MnemonicTable(words: mnemonic, isDesktop: false), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), SecondaryButton( label: "Copy", onPressed: () async { - await clipboardInterface - .setData(ClipboardData(text: mnemonic.join(" "))); + await clipboardInterface.setData( + ClipboardData(text: mnemonic.join(" ")), + ); if (context.mounted) { unawaited( showFloatingFlushBar( @@ -213,9 +197,7 @@ class _Mnemonic extends ConsumerWidget { } }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), PrimaryButton( label: "Show QR Code", onPressed: () { @@ -237,25 +219,18 @@ class _Mnemonic extends ConsumerWidget { style: STextStyles.pageTitleH2(context), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Center( child: RepaintBoundary( // key: _qrKey, child: SizedBox( width: width + 20, height: width + 20, - child: QR( - data: data, - size: width, - ), + child: QR(data: data, size: width), ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Center( child: SizedBox( width: width, @@ -266,15 +241,14 @@ class _Mnemonic extends ConsumerWidget { }, style: Theme.of(context) .extension()! - .getSecondaryEnabledButtonStyle( - context, - ), + .getSecondaryEnabledButtonStyle(context), child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -293,11 +267,7 @@ class _Mnemonic extends ConsumerWidget { } class _FrostKeys extends StatelessWidget { - const _FrostKeys({ - super.key, - required this.walletId, - this.frostWalletData, - }); + const _FrostKeys({super.key, required this.walletId, this.frostWalletData}); final String walletId; final ({ @@ -305,7 +275,8 @@ class _FrostKeys extends StatelessWidget { String config, String keys, ({String config, String keys})? prevGen, - })? frostWalletData; + })? + frostWalletData; @override Widget build(BuildContext context) { @@ -314,9 +285,7 @@ class _FrostKeys extends StatelessWidget { builder: (builderContext, constraints) { return SingleChildScrollView( child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), + constraints: BoxConstraints(minHeight: constraints.maxHeight - 24), child: IntrinsicHeight( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -334,9 +303,7 @@ class _FrostKeys extends StatelessWidget { style: STextStyles.label(context), ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), // DetailItem( // title: "My name", // detail: frostWalletData!.myName, @@ -354,32 +321,21 @@ class _FrostKeys extends StatelessWidget { DetailItem( title: "Multisig config", detail: frostWalletData!.config, - button: Util.isDesktop - ? IconCopyButton( - data: frostWalletData!.config, - ) - : SimpleCopyButton( - data: frostWalletData!.config, - ), - ), - const SizedBox( - height: 16, + 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, - ), + button: + Util.isDesktop + ? IconCopyButton(data: frostWalletData!.keys) + : SimpleCopyButton(data: frostWalletData!.keys), ), - if (prevGen) - const SizedBox( - height: 24, - ), + if (prevGen) const SizedBox(height: 24), if (prevGen) RoundedWhiteContainer( child: Text( @@ -387,37 +343,33 @@ class _FrostKeys extends StatelessWidget { style: STextStyles.label(context), ), ), - if (prevGen) - const SizedBox( - height: 12, - ), + 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, + 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, - ), + button: + Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.prevGen!.keys, + ) + : SimpleCopyButton( + data: frostWalletData!.prevGen!.keys, + ), ), ], ), @@ -459,9 +411,7 @@ class MobileKeyDataView extends ConsumerWidget { final XPrivData _ => "xpriv(s)", final CWKeyData _ => "keys", final ViewOnlyWalletData _ => "keys", - _ => throw UnimplementedError( - "Don't forget to add your KeyDataInterface here!", - ), + _ => throw UnimplementedError("Don't forget to add your KeyDataInterface here!"), }}", style: STextStyles.navBarTitle(context), ), @@ -470,40 +420,40 @@ class MobileKeyDataView extends ConsumerWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: LayoutBuilder( - builder: (context, constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: switch (keyData) { - final XPrivData e => WalletXPrivs( - walletId: walletId, - xprivData: e, - ), - final CWKeyData e => CNWalletKeys( - walletId: walletId, - cwKeyData: e, - ), - final ViewOnlyWalletData e => - ViewOnlyWalletDataWidget( - data: e, - ), - _ => throw UnimplementedError( - "Don't forget to add your KeyDataInterface here!", - ), - }, - ), - const SizedBox( - height: 16, + builder: + (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: switch (keyData) { + final XPrivData e => WalletXPrivs( + walletId: walletId, + xprivData: e, + ), + final CWKeyData e => CNWalletKeys( + walletId: walletId, + cwKeyData: e, + ), + final ViewOnlyWalletData e => + ViewOnlyWalletDataWidget(data: e), + _ => + throw UnimplementedError( + "Don't forget to add your KeyDataInterface here!", + ), + }, + ), + const SizedBox(height: 16), + ], ), - ], + ), ), ), - ), - ), ), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 5b3a2eb07..27e7cc63c 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -33,13 +33,16 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/coins/epiccash.dart'; +import '../../../../wallets/crypto_currency/coins/litecoin.dart'; import '../../../../wallets/crypto_currency/coins/monero.dart'; +import '../../../../wallets/crypto_currency/coins/salvium.dart'; import '../../../../wallets/crypto_currency/coins/wownero.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; -import '../../../../wallets/wallet/impl/monero_wallet.dart'; -import '../../../../wallets/wallet/impl/wownero_wallet.dart'; +import '../../../../wallets/wallet/impl/salvium_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../widgets/animated_text.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/conditional_parent.dart'; @@ -142,9 +145,7 @@ class _WalletNetworkSettingsViewState try { final wallet = ref.read(pWallets).getWallet(widget.walletId); - await wallet.recover( - isRescan: true, - ); + await wallet.recover(isRescan: true); if (mounted) { // pop rescanning dialog @@ -155,29 +156,31 @@ class _WalletNetworkSettingsViewState context: context, useSafeArea: false, barrierDismissible: true, - builder: (context) => ConditionalParent( - condition: isDesktop, - builder: (child) => DesktopDialog( - maxHeight: 150, - maxWidth: 500, - child: child, - ), - child: StackDialog( - title: "Rescan completed", - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.itemSubtitle12(context), + builder: + (context) => ConditionalParent( + condition: isDesktop, + builder: + (child) => DesktopDialog( + maxHeight: 150, + maxWidth: 500, + child: child, + ), + child: StackDialog( + title: "Rescan completed", + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context, rootNavigator: isDesktop).pop(); + }, + ), ), - onPressed: () { - Navigator.of(context, rootNavigator: isDesktop).pop(); - }, ), - ), - ), ); } } catch (e) { @@ -192,22 +195,23 @@ class _WalletNetworkSettingsViewState context: context, useSafeArea: false, barrierDismissible: true, - builder: (context) => StackDialog( - title: "Rescan failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.itemSubtitle12(context), + builder: + (context) => StackDialog( + title: "Rescan failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context, rootNavigator: isDesktop).pop(); + }, + ), ), - onPressed: () { - Navigator.of(context, rootNavigator: isDesktop).pop(); - }, - ), - ), ); } } @@ -246,30 +250,35 @@ class _WalletNetworkSettingsViewState eventBus = widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); + _syncStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }); - _refreshSubscription = eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _percent = event.percent.clamp(0.0, 1.0); - }); - } - }, - ); + _refreshSubscription = eventBus.on().listen(( + event, + ) async { + if (event.walletId == widget.walletId) { + setState(() { + _percent = event.percent.clamp(0.0, 1.0); + }); + } + }); final coin = ref.read(pWalletCoin(widget.walletId)); - if (coin is Monero || coin is Wownero || coin is Epiccash) { + // TODO: handle isMwebEnabled toggled + if (coin is Monero || + coin is Wownero || + coin is Epiccash || + coin is Salvium || + (coin is Litecoin && + ref.read(pWalletInfo(widget.walletId)).isMwebEnabled)) { _blocksRemainingSubscription = eventBus.on().listen( (event) async { if (event.walletId == widget.walletId) { @@ -322,22 +331,30 @@ class _WalletNetworkSettingsViewState Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; - final progressLength = isDesktop - ? 430.0 - : screenWidth - (_padding * 2) - (_boxPadding * 3) - _iconSize; + final progressLength = + isDesktop + ? 430.0 + : screenWidth - (_padding * 2) - (_boxPadding * 3) - _iconSize; final coin = ref.watch(pWalletCoin(widget.walletId)); - if (coin is Monero) { + if (coin is Salvium) { + final double highestPercent = + (ref.read(pWallets).getWallet(widget.walletId) as SalviumWallet) + .highestPercentCached; + if (_percent < highestPercent) { + _percent = highestPercent.clamp(0.0, 1.0); + } + } else if (coin is Monero || coin is Wownero) { final double highestPercent = - (ref.read(pWallets).getWallet(widget.walletId) as MoneroWallet) + (ref.read(pWallets).getWallet(widget.walletId) as LibMoneroWallet) .highestPercentCached; if (_percent < highestPercent) { _percent = highestPercent.clamp(0.0, 1.0); } - } else if (coin is Wownero) { + } else if (coin is Litecoin) { final double highestPercent = - (ref.watch(pWallets).getWallet(widget.walletId) as WowneroWallet) + (ref.watch(pWallets).getWallet(widget.walletId) as MwebInterface) .highestPercentCached; if (_percent < highestPercent) { _percent = highestPercent.clamp(0.0, 1.0); @@ -364,10 +381,7 @@ class _WalletNetworkSettingsViewState Navigator.of(context).pop(); }, ), - title: Text( - "Network", - style: STextStyles.navBarTitle(context), - ), + title: Text("Network", style: STextStyles.navBarTitle(context)), actions: [ if (ref.watch(pWalletCoin(widget.walletId)) is! Epiccash) Padding( @@ -384,14 +398,16 @@ class _WalletNetworkSettingsViewState ), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.verticalEllipsis, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -408,9 +424,10 @@ class _WalletNetworkSettingsViewState right: 10, child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, + color: + Theme.of( + context, + ).extension()!.popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -461,18 +478,18 @@ class _WalletNetworkSettingsViewState ), ], ), - body: Padding( - padding: EdgeInsets.only( - top: 12, - left: _padding, - right: _padding, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - child, - ], + body: SafeArea( + child: Padding( + padding: EdgeInsets.only( + top: 12, + left: _padding, + right: _padding, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [child], + ), ), ), ), @@ -488,9 +505,10 @@ class _WalletNetworkSettingsViewState Text( "Blockchain status", textAlign: TextAlign.left, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), CustomTextButton( text: "Resync", @@ -500,17 +518,17 @@ class _WalletNetworkSettingsViewState ), ], ), - SizedBox( - height: isDesktop ? 12 : 9, - ), + SizedBox(height: isDesktop ? 12 : 9), if (_currentSyncStatus == WalletSyncStatus.synced) RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension()!.background - : null, - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + borderColor: + isDesktop + ? Theme.of(context).extension()!.background + : null, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( children: [ Container( @@ -528,15 +546,14 @@ class _WalletNetworkSettingsViewState Assets.svg.radio, height: isDesktop ? 19 : 14, width: isDesktop ? 19 : 14, - color: Theme.of(context) - .extension()! - .accentColorGreen, + color: + Theme.of( + context, + ).extension()!.accentColorGreen, ), ), ), - SizedBox( - width: _boxPadding, - ), + SizedBox(width: _boxPadding), Column( children: [ SizedBox( @@ -551,18 +568,18 @@ class _WalletNetworkSettingsViewState ], ), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), ProgressBar( width: progressLength, height: 5, - fillColor: Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + fillColor: + Theme.of( + context, + ).extension()!.accentColorGreen, + backgroundColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, percent: 1, ), ], @@ -572,12 +589,14 @@ class _WalletNetworkSettingsViewState ), if (_currentSyncStatus == WalletSyncStatus.syncing) RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension()!.background - : null, - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + borderColor: + isDesktop + ? Theme.of(context).extension()!.background + : null, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( children: [ Container( @@ -595,15 +614,14 @@ class _WalletNetworkSettingsViewState Assets.svg.radioSyncing, height: 14, width: 14, - color: Theme.of(context) - .extension()! - .accentColorYellow, + color: + Theme.of( + context, + ).extension()!.accentColorYellow, ), ), ), - SizedBox( - width: _boxPadding, - ), + SizedBox(width: _boxPadding), Column( children: [ SizedBox( @@ -624,23 +642,34 @@ class _WalletNetworkSettingsViewState children: [ Text( _percentString(_percent), - style: - STextStyles.syncPercent(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorYellow, + style: STextStyles.syncPercent( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorYellow, ), ), if (coin is Monero || coin is Wownero || - coin is Epiccash) + coin is Epiccash || + coin is Salvium || + (coin is Litecoin && + ref.watch( + pWalletInfo( + widget.walletId, + ).select((s) => s.isMwebEnabled), + ))) Text( " (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})", - style: STextStyles.syncPercent(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorYellow, + style: STextStyles.syncPercent( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorYellow, ), ), ], @@ -648,18 +677,18 @@ class _WalletNetworkSettingsViewState ], ), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), ProgressBar( width: progressLength, height: 5, - fillColor: Theme.of(context) - .extension()! - .accentColorYellow, - backgroundColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + fillColor: + Theme.of( + context, + ).extension()!.accentColorYellow, + backgroundColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, percent: _percent, ), ], @@ -669,12 +698,14 @@ class _WalletNetworkSettingsViewState ), if (_currentSyncStatus == WalletSyncStatus.unableToSync) RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension()!.background - : null, - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + borderColor: + isDesktop + ? Theme.of(context).extension()!.background + : null, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( children: [ Container( @@ -692,15 +723,14 @@ class _WalletNetworkSettingsViewState Assets.svg.radioProblem, height: 14, width: 14, - color: Theme.of(context) - .extension()! - .accentColorRed, + color: + Theme.of( + context, + ).extension()!.accentColorRed, ), ), ), - SizedBox( - width: _boxPadding, - ), + SizedBox(width: _boxPadding), Column( children: [ SizedBox( @@ -711,34 +741,36 @@ class _WalletNetworkSettingsViewState Text( "Unable to synchronize", style: STextStyles.w600_12(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorRed, + color: + Theme.of( + context, + ).extension()!.accentColorRed, ), ), Text( "0%", style: STextStyles.syncPercent(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorRed, + color: + Theme.of( + context, + ).extension()!.accentColorRed, ), ), ], ), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), ProgressBar( width: progressLength, height: 5, - fillColor: Theme.of(context) - .extension()! - .accentColorRed, - backgroundColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + fillColor: + Theme.of( + context, + ).extension()!.accentColorRed, + backgroundColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, percent: 0, ), ], @@ -748,30 +780,29 @@ class _WalletNetworkSettingsViewState ), if (_currentSyncStatus == WalletSyncStatus.unableToSync) Padding( - padding: const EdgeInsets.only( - top: 12, - ), + padding: const EdgeInsets.only(top: 12), child: RoundedContainer( - color: Theme.of(context) - .extension()! - .warningBackground, + color: + Theme.of( + context, + ).extension()!.warningBackground, child: Text( "Please check your internet connection and make sure your current node is not having issues.", style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: + Theme.of( + context, + ).extension()!.warningForeground, ), ), ), ), - SizedBox( - height: isDesktop ? 12 : 9, - ), + SizedBox(height: isDesktop ? 12 : 9), RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension()!.background - : null, + borderColor: + isDesktop + ? Theme.of(context).extension()!.background + : null, padding: isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), child: Row( @@ -780,9 +811,10 @@ class _WalletNetworkSettingsViewState Text( "Current height", textAlign: TextAlign.left, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), Text( ref.watch(pWalletChainHeight(widget.walletId)).toString(), @@ -791,36 +823,37 @@ class _WalletNetworkSettingsViewState ], ), ), - SizedBox( - height: isDesktop ? 32 : 20, - ), + SizedBox(height: isDesktop ? 32 : 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Tor status", textAlign: TextAlign.left, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), CustomTextButton( - text: ref.watch( - prefsChangeNotifierProvider.select((value) => value.useTor), - ) - ? "Disconnect" - : "Connect", + text: + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.useTor, + ), + ) + ? "Disconnect" + : "Connect", onTap: onTorTapped, ), ], ), - SizedBox( - height: isDesktop ? 12 : 9, - ), + SizedBox(height: isDesktop ? 12 : 9), RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension()!.background - : null, + borderColor: + isDesktop + ? Theme.of(context).extension()!.background + : null, padding: isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), child: Row( @@ -843,9 +876,10 @@ class _WalletNetworkSettingsViewState Assets.svg.tor, height: isDesktop ? 19 : 14, width: isDesktop ? 19 : 14, - color: Theme.of(context) - .extension()! - .accentColorGreen, + color: + Theme.of( + context, + ).extension()!.accentColorGreen, ), ), ), @@ -856,10 +890,9 @@ class _WalletNetworkSettingsViewState width: _iconSize, height: _iconSize, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textDark - .withOpacity(0.08), + color: Theme.of( + context, + ).extension()!.textDark.withOpacity(0.08), borderRadius: BorderRadius.circular(_iconSize), ), child: Center( @@ -867,15 +900,14 @@ class _WalletNetworkSettingsViewState Assets.svg.tor, height: isDesktop ? 19 : 14, width: isDesktop ? 19 : 14, - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ), - SizedBox( - width: _boxPadding, - ), + SizedBox(width: _boxPadding), TorSubscription( onTorStatusChanged: (status) { setState(() { @@ -887,32 +919,37 @@ class _WalletNetworkSettingsViewState children: [ Text( "Tor status", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ), if (_torConnectionStatus == TorConnectionStatus.connected) Text( "Connected", - style: - STextStyles.desktopTextExtraExtraSmall(context), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), if (_torConnectionStatus == TorConnectionStatus.connecting) Text( "Connecting...", - style: - STextStyles.desktopTextExtraExtraSmall(context), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), if (_torConnectionStatus == TorConnectionStatus.disconnected) Text( "Disconnected", - style: - STextStyles.desktopTextExtraExtraSmall(context), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ], ), @@ -920,18 +957,17 @@ class _WalletNetworkSettingsViewState ], ), ), - SizedBox( - height: isDesktop ? 32 : 20, - ), + SizedBox(height: isDesktop ? 32 : 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "${ref.watch(pWalletCoin(widget.walletId)).prettyName} nodes", textAlign: TextAlign.left, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), CustomTextButton( text: "Add new node", @@ -949,22 +985,16 @@ class _WalletNetworkSettingsViewState ), ], ), - SizedBox( - height: isDesktop ? 12 : 8, - ), + SizedBox(height: isDesktop ? 12 : 8), NodesList( coin: ref.watch(pWalletCoin(widget.walletId)), popBackToRoute: WalletNetworkSettingsView.routeName, ), if (isDesktop && ref.watch(pWalletCoin(widget.walletId)) is! Epiccash) - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), if (isDesktop && ref.watch(pWalletCoin(widget.walletId)) is! Epiccash) Padding( - padding: const EdgeInsets.only( - bottom: 12, - ), + padding: const EdgeInsets.only(bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -978,12 +1008,14 @@ class _WalletNetworkSettingsViewState ), if (isDesktop && ref.watch(pWalletCoin(widget.walletId)) is! Epiccash) RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context).extension()!.background - : null, - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + borderColor: + isDesktop + ? Theme.of(context).extension()!.background + : null, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Expandable( onExpandChanged: (state) { setState(() { @@ -999,24 +1031,24 @@ class _WalletNetworkSettingsViewState width: _iconSize, height: _iconSize, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular(_iconSize), ), child: Center( child: SvgPicture.asset( Assets.svg.networkWired, width: 24, - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -1026,9 +1058,10 @@ class _WalletNetworkSettingsViewState style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), Text( @@ -1047,9 +1080,10 @@ class _WalletNetworkSettingsViewState : Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ], ), 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 18b18d9e1..3d899add7 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 @@ -38,6 +38,7 @@ import '../../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; @@ -115,27 +116,26 @@ class _WalletSettingsViewState extends ConsumerState { eventBus = widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == walletId) { - switch (event.newStatus) { - case WalletSyncStatus.unableToSync: - // TODO: Handle this case. - break; - case WalletSyncStatus.synced: - // TODO: Handle this case. - break; - case WalletSyncStatus.syncing: - // TODO: Handle this case. - break; + _syncStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == walletId) { + switch (event.newStatus) { + case WalletSyncStatus.unableToSync: + // TODO: Handle this case. + break; + case WalletSyncStatus.synced: + // TODO: Handle this case. + break; + case WalletSyncStatus.syncing: + // TODO: Handle this case. + break; + } + setState(() { + _currentSyncStatus = event.newStatus; + }); } - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); + }); // _nodeStatusSubscription = // eventBus.on().listen( @@ -189,331 +189,327 @@ class _WalletSettingsViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Settings", - style: STextStyles.navBarTitle(context), - ), + title: Text("Settings", style: STextStyles.navBarTitle(context)), ), - body: LayoutBuilder( - builder: (builderContext, constraints) { - return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(4), - child: Column( - children: [ - SettingsListButton( - iconAssetName: Assets.svg.addressBook, - iconSize: 16, - title: "Address book", - onPressed: () { - Navigator.of(context).pushNamed( - AddressBookView.routeName, - arguments: coin, - ); - }, - ), - if (coin is FrostCurrency) - const SizedBox( - height: 8, - ), - if (coin is FrostCurrency) + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(4), + child: Column( + children: [ SettingsListButton( - iconAssetName: Assets.svg.addressBook2, + iconAssetName: Assets.svg.addressBook, iconSize: 16, - title: "FROST Multisig settings", + title: "Address book", onPressed: () { Navigator.of(context).pushNamed( - FrostMSWalletOptionsView.routeName, - arguments: walletId, + AddressBookView.routeName, + arguments: coin, ); }, ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.node, - iconSize: 16, - title: "Network", - onPressed: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - widget.initialNodeStatus, - ), - ); - }, - ), - if (canBackup) - const SizedBox( - height: 8, + if (coin is FrostCurrency) + const SizedBox(height: 8), + if (coin is FrostCurrency) + SettingsListButton( + iconAssetName: Assets.svg.addressBook2, + iconSize: 16, + title: "FROST Multisig settings", + onPressed: () { + Navigator.of(context).pushNamed( + FrostMSWalletOptionsView.routeName, + arguments: walletId, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.node, + iconSize: 16, + title: "Network", + onPressed: () { + Navigator.of(context).pushNamed( + WalletNetworkSettingsView.routeName, + arguments: Tuple3( + walletId, + _currentSyncStatus, + widget.initialNodeStatus, + ), + ); + }, ), - if (canBackup) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.lock, - iconSize: 16, - title: "Wallet backup", - onPressed: () async { - // TODO: [prio=med] take wallets that don't have a mnemonic into account - - List? mnemonic; - ({ - String myName, - String config, - String keys, + if (canBackup) const SizedBox(height: 8), + if (canBackup) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.lock, + iconSize: 16, + title: "Wallet backup", + onPressed: () async { + // TODO: [prio=med] take wallets that don't have a mnemonic into account + + List? mnemonic; ({ + String myName, String config, - String keys - })? prevGen, - })? frostWalletData; - if (wallet is BitcoinFrostWallet) { - final futures = [ - wallet.getSerializedKeys(), - wallet.getMultisigConfig(), - wallet.getSerializedKeysPrevGen(), - wallet.getMultisigConfigPrevGen(), - ]; - - final results = - await Future.wait(futures); - - if (results.length == 4) { - frostWalletData = ( - myName: wallet.frostInfo.myName, - config: results[1]!, - keys: results[0]!, - prevGen: results[2] == null || - results[3] == null - ? null - : ( - config: results[3]!, - keys: results[2]!, - ), + String keys, + ({String config, String keys})? + prevGen, + })? + frostWalletData; + if (wallet is BitcoinFrostWallet) { + final futures = [ + wallet.getSerializedKeys(), + wallet.getMultisigConfig(), + wallet + .getSerializedKeysPrevGen(), + wallet + .getMultisigConfigPrevGen(), + ]; + + final results = await Future.wait( + futures, ); - } - } else { - if (wallet is MnemonicInterface) { - if (wallet - is ViewOnlyOptionInterface && - (wallet as ViewOnlyOptionInterface) - .isViewOnly) { - // TODO: is something needed here? - } else { - mnemonic = await wallet - .getMnemonicAsWords(); + + if (results.length == 4) { + frostWalletData = ( + myName: + wallet.frostInfo.myName, + config: results[1]!, + keys: results[0]!, + prevGen: + results[2] == null || + results[3] == null + ? null + : ( + config: results[3]!, + keys: results[2]!, + ), + ); + } + } else { + if (wallet is MnemonicInterface) { + if (wallet + is ViewOnlyOptionInterface && + (wallet as ViewOnlyOptionInterface) + .isViewOnly) { + // TODO: is something needed here? + } else { + mnemonic = + await wallet + .getMnemonicAsWords(); + } } } - } KeyDataInterface? keyData; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { - keyData = await wallet - .getViewOnlyWalletData(); + keyData = + await wallet + .getViewOnlyWalletData(); } else if (wallet is ExtendedKeysInterface) { keyData = await wallet.getXPrivs(); } else if (wallet is LibMoneroWallet) { keyData = await wallet.getKeys(); + } else if (wallet + is LibSalviumWallet) { + keyData = await wallet.getKeys(); } - if (context.mounted) { - if (keyData != null && - wallet - is ViewOnlyOptionInterface && - wallet.isViewOnly) { - await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => - LockscreenView( - routeOnSuccessArguments: ( - walletId: walletId, - keyData: keyData, + if (context.mounted) { + if (keyData != null && + wallet + is ViewOnlyOptionInterface && + wallet.isViewOnly) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: + (_) => LockscreenView( + routeOnSuccessArguments: + ( + walletId: + walletId, + keyData: + keyData, + ), + showBackButton: true, + routeOnSuccess: + MobileKeyDataView + .routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery data", + biometricsAuthenticationTitle: + "View recovery data", + ), + settings: const RouteSettings( + name: + "/viewRecoveryDataLockscreen", ), - showBackButton: true, - routeOnSuccess: - MobileKeyDataView - .routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery data", - biometricsAuthenticationTitle: - "View recovery data", - ), - settings: const RouteSettings( - name: - "/viewRecoveryDataLockscreen", ), - ), - ); - } else { - await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => - LockscreenView( - routeOnSuccessArguments: ( - walletId: walletId, - mnemonic: mnemonic ?? [], - frostWalletData: - frostWalletData, - keyData: keyData, + ); + } else { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: + (_) => LockscreenView( + routeOnSuccessArguments: ( + walletId: walletId, + mnemonic: + mnemonic ?? [], + frostWalletData: + frostWalletData, + keyData: keyData, + ), + showBackButton: true, + routeOnSuccess: + WalletBackupView + .routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: + "View recovery phrase", + ), + settings: const RouteSettings( + name: + "/viewRecoverPhraseLockscreen", ), - showBackButton: true, - routeOnSuccess: - WalletBackupView - .routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery phrase", - biometricsAuthenticationTitle: - "View recovery phrase", - ), - settings: const RouteSettings( - name: - "/viewRecoverPhraseLockscreen", ), - ), - ); + ); + } } - } - }, + }, + ); + }, + ), + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.downloadFolder, + title: "Wallet settings", + iconSize: 16, + onPressed: () { + Navigator.of(context).pushNamed( + WalletSettingsWalletSettingsView + .routeName, + arguments: walletId, ); }, ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.downloadFolder, - title: "Wallet settings", - iconSize: 16, - onPressed: () { - Navigator.of(context).pushNamed( - WalletSettingsWalletSettingsView - .routeName, - arguments: walletId, - ); - }, - ), - const SizedBox( - height: 8, - ), - SettingsListButton( - iconAssetName: Assets.svg.arrowRotate, - title: "Syncing preferences", - onPressed: () { - Navigator.of(context).pushNamed( - SyncingPreferencesView.routeName, - ); - }, - ), - if (xPubEnabled) - const SizedBox( - height: 8, - ), - if (xPubEnabled) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.eye, - title: "Wallet xPub", - onPressed: () async { - final xpubData = await showLoading( - delay: const Duration( - milliseconds: 800, - ), - whileFuture: (ref - .read(pWallets) - .getWallet(walletId) - as ExtendedKeysInterface) - .getXPubs(), - context: context, - message: "Loading xpubs", - rootNavigator: Util.isDesktop, - ); - if (context.mounted) { - await Navigator.of(context) - .pushNamed( - XPubView.routeName, - arguments: ( - widget.walletId, - xpubData - ), - ); - } - }, + const SizedBox(height: 8), + SettingsListButton( + iconAssetName: Assets.svg.arrowRotate, + title: "Syncing preferences", + onPressed: () { + Navigator.of(context).pushNamed( + SyncingPreferencesView.routeName, ); }, ), - if (coin is Firo) - const SizedBox( - height: 8, - ), - if (coin is Firo) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.eye, - title: "Clear electrumx cache", - onPressed: () async { - String? result; - await showDialog( - useSafeArea: false, - barrierDismissible: true, - context: context, - builder: (_) => StackOkDialog( - title: - "Are you sure you want to clear " - "${coin.prettyName} electrumx cache?", - onOkPressed: (value) { - result = value; - }, - leftButton: SecondaryButton( - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(); - }, + if (xPubEnabled) const SizedBox(height: 8), + if (xPubEnabled) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Wallet xPub", + onPressed: () async { + final xpubData = await showLoading( + delay: const Duration( + milliseconds: 800, ), - ), - ); - - if (result == "OK" && - context.mounted) { - await showLoading( - whileFuture: Future.wait( - [ + whileFuture: + (ref + .read(pWallets) + .getWallet( + walletId, + ) + as ExtendedKeysInterface) + .getXPubs(), + context: context, + message: "Loading xpubs", + rootNavigator: Util.isDesktop, + ); + if (context.mounted) { + await Navigator.of( + context, + ).pushNamed( + XPubView.routeName, + arguments: ( + widget.walletId, + xpubData, + ), + ); + } + }, + ); + }, + ), + if (coin is Firo) const SizedBox(height: 8), + if (coin is Firo) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Clear electrumx cache", + onPressed: () async { + String? result; + await showDialog( + useSafeArea: false, + barrierDismissible: true, + context: context, + builder: + (_) => StackOkDialog( + title: + "Are you sure you want to clear " + "${coin.prettyName} electrumx cache?", + onOkPressed: (value) { + result = value; + }, + leftButton: SecondaryButton( + label: "Cancel", + onPressed: () { + Navigator.of( + context, + ).pop(); + }, + ), + ), + ); + + if (result == "OK" && + context.mounted) { + await showLoading( + whileFuture: Future.wait([ Future.delayed( const Duration( milliseconds: 1500, @@ -521,99 +517,96 @@ class _WalletSettingsViewState extends ConsumerState { ), DB.instance .clearSharedTransactionCache( - currency: coin, - ), + currency: coin, + ), if (coin is Firo) - FiroCacheCoordinator - .clearSharedCache( + FiroCacheCoordinator.clearSharedCache( coin.network, ), - ], - ), - context: context, - message: "Clearing cache...", + ]), + context: context, + message: "Clearing cache...", + ); + } + }, + ); + }, + ), + if (coin is NanoCurrency) + const SizedBox(height: 8), + if (coin is NanoCurrency) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Change representative", + onPressed: () { + Navigator.of(context).pushNamed( + ChangeRepresentativeView + .routeName, + arguments: widget.walletId, ); - } - }, - ); - }, - ), - if (coin is NanoCurrency) - const SizedBox( - height: 8, - ), - if (coin is NanoCurrency) - Consumer( - builder: (_, ref, __) { - return SettingsListButton( - iconAssetName: Assets.svg.eye, - title: "Change representative", - onPressed: () { - Navigator.of(context).pushNamed( - ChangeRepresentativeView.routeName, - arguments: widget.walletId, - ); - }, - ); - }, - ), - // const SizedBox( - // height: 8, - // ), - // SettingsListButton( - // iconAssetName: Assets.svg.ellipsis, - // title: "Debug Info", - // onPressed: () { - // Navigator.of(context) - // .pushNamed(DebugView.routeName); - // }, - // ), - ], + }, + ); + }, + ), + // const SizedBox( + // height: 8, + // ), + // SettingsListButton( + // iconAssetName: Assets.svg.ellipsis, + // title: "Debug Info", + // onPressed: () { + // Navigator.of(context) + // .pushNamed(DebugView.routeName); + // }, + // ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - const Spacer(), - Consumer( - builder: (_, ref, __) { - return TextButton( - onPressed: () { - // TODO: [prio=med] needs more thought if this is still required - // ref - // .read(pWallets) - // .getWallet(walletId) - // .isActiveWallet = false; - ref - .read(transactionFilterProvider.state) - .state = null; - - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName), - ); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Log out", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + const SizedBox(height: 12), + const Spacer(), + Consumer( + builder: (_, ref, __) { + return TextButton( + onPressed: () { + // TODO: [prio=med] needs more thought if this is still required + // ref + // .read(pWallets) + // .getWallet(walletId) + // .isActiveWallet = false; + ref + .read(transactionFilterProvider.state) + .state = null; + + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName), + ); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Log out", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), ), - ), - ); - }, - ), - ], + ); + }, + ), + ], + ), ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); @@ -621,10 +614,7 @@ class _WalletSettingsViewState extends ConsumerState { } class EpicBoxInfoForm extends ConsumerStatefulWidget { - const EpicBoxInfoForm({ - super.key, - required this.walletId, - }); + const EpicBoxInfoForm({super.key, required this.walletId}); final String walletId; @@ -668,9 +658,7 @@ class _EpiBoxInfoFormState extends ConsumerState { controller: hostController, decoration: const InputDecoration(hintText: "Host"), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -679,9 +667,7 @@ class _EpiBoxInfoFormState extends ConsumerState { keyboardType: Util.isDesktop ? null : const TextInputType.numberWithOptions(), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), TextButton( onPressed: () async { try { 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 cfca56bf2..af3f9123e 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 @@ -128,8 +128,9 @@ class _ChangeRepresentativeViewState } Future _copy() async { - await _clipboardInterface - .setData(ClipboardData(text: representative ?? "")); + await _clipboardInterface.setData( + ClipboardData(text: representative ?? ""), + ); if (mounted) { unawaited( showFloatingFlushBar( @@ -146,107 +147,99 @@ class _ChangeRepresentativeViewState Widget build(BuildContext context) { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: SafeArea( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Wallet representative", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - color: Theme.of(context) - .extension()! - .background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 24, - height: 24, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Wallet representative", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: + Theme.of( + context, + ).extension()!.background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 24, + height: 24, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, + ), + onPressed: () { + if (representative != null) { + _copy(); + } + }, ), - onPressed: () { - if (representative != null) { - _copy(); - } - }, ), ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), + child: child, ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, ), - child: child, ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 600, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxWidth: 600, + maxHeight: double.infinity, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Change representative", - style: STextStyles.desktopH2(context), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Change representative", + style: STextStyles.desktopH2(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of(context, rootNavigator: true).pop, + ), + ], ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + AnimatedSize( + duration: const Duration(milliseconds: 150), + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 0, 32, 32), + child: child, + ), ), ], ), - AnimatedSize( - duration: const Duration( - milliseconds: 150, - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(32, 0, 32, 32), - child: child, - ), - ), - ], - ), - ), + ), child: Column( children: [ if (isDesktop) const SizedBox(height: 24), ConditionalParent( condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: FutureBuilder( future: loadRepresentative(), builder: (context, AsyncSnapshot snapshot) { @@ -261,63 +254,54 @@ class _ChangeRepresentativeViewState child = const SizedBox( key: Key("loadingRepresentative"), height: height, - child: Center( - child: LoadingIndicator( - width: 100, - ), - ), + child: Center(child: LoadingIndicator(width: 100)), ); } else { child = Column( children: [ ConditionalParent( condition: !isDesktop, - builder: (child) => RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - ], - ), - ), + builder: + (child) => RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [child], + ), + ), child: ConditionalParent( condition: isDesktop, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Current representative", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - const SizedBox( - height: 4, - ), - Row( + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - child, + Text( + "Current representative", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 4), + Row(children: [child]), ], ), - ], - ), child: SelectableText( representative!, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), ), ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -326,15 +310,18 @@ class _ChangeRepresentativeViewState autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, controller: _textController, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), focusNode: _textFocusNode, decoration: standardInputDecoration( "Enter new representative", @@ -342,54 +329,50 @@ class _ChangeRepresentativeViewState context, desktopMed: isDesktop, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, - ) - : null, - suffixIcon: _textController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _textController.text = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: + _textController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, ), - ), - ) - : null, + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _textController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), ), if (isDesktop) const SizedBox(height: 60), if (!isDesktop) const Spacer(), - PrimaryButton( - label: "Save", - onPressed: _save, - ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + PrimaryButton(label: "Save", onPressed: _save), + if (!isDesktop) const SizedBox(height: 16), ], ); } return AnimatedSwitcher( - duration: const Duration( - milliseconds: 200, - ), + duration: const Duration(milliseconds: 200), child: child, ); }, diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart index 16c48cbb6..03ba8cdf0 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart @@ -55,7 +55,8 @@ class DeleteWalletRecoveryPhraseView extends ConsumerStatefulWidget { String config, String keys, ({String config, String keys})? prevGen, - })? frostWalletData; + })? + frostWalletData; final ClipboardInterface clipboardInterface; @@ -79,45 +80,47 @@ class _DeleteWalletRecoveryPhraseViewState showDialog( barrierDismissible: true, context: context, - builder: (_) => StackDialog( - title: "Thanks! Your wallet will be deleted.", - leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: - Theme.of(context).extension()!.accentColorDark, + builder: + (_) => StackDialog( + title: "Thanks! Your wallet will be deleted.", + leftButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), ), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () async { - await ref.read(pWallets).deleteWallet( - ref.read(pWalletInfo(widget.walletId)), - ref.read(secureStoreProvider), - ); + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () async { + await ref + .read(pWallets) + .deleteWallet( + ref.read(pWalletInfo(widget.walletId)), + ref.read(secureStoreProvider), + ); - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName), - ); - } - }, - child: Text( - "Ok", - style: STextStyles.button(context), + if (mounted) { + Navigator.of( + context, + ).popUntil(ModalRoute.withName(HomeView.routeName)); + } + }, + child: Text("Ok", style: STextStyles.button(context)), + ), ), - ), - ), ); } finally { _lock = false; @@ -159,13 +162,15 @@ class _DeleteWalletRecoveryPhraseViewState Assets.svg.copy, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: () async { - await _clipboardInterface - .setData(ClipboardData(text: _mnemonic.join(" "))); + await _clipboardInterface.setData( + ClipboardData(text: _mnemonic.join(" ")), + ); if (context.mounted) { unawaited( showFloatingFlushBar( @@ -182,209 +187,218 @@ class _DeleteWalletRecoveryPhraseViewState ), ], ), - body: Padding( - padding: const EdgeInsets.all(16), - child: frost - ? LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - 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" - "${AppConfig.appName} 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: 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: widget.frostWalletData!.config, - button: Util.isDesktop - ? IconCopyButton( - data: widget.frostWalletData!.config, - ) - : SimpleCopyButton( - data: widget.frostWalletData!.config, - ), - ), - const SizedBox( - height: 16, - ), - DetailItem( - title: "Keys", - detail: widget.frostWalletData!.keys, - button: Util.isDesktop - ? IconCopyButton( - data: widget.frostWalletData!.keys, - ) - : SimpleCopyButton( - data: widget.frostWalletData!.keys, - ), - ), - if (prevGen) - const SizedBox( - height: 24, - ), - if (prevGen) - RoundedWhiteContainer( - child: Text( - "Previous generation info", - style: STextStyles.label(context), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: + frost + ? LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + 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" + "${AppConfig.appName} 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: 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: widget.frostWalletData!.config, + button: + Util.isDesktop + ? IconCopyButton( + data: + widget + .frostWalletData! + .config, + ) + : SimpleCopyButton( + data: + widget + .frostWalletData! + .config, + ), + ), + const SizedBox(height: 16), + DetailItem( + title: "Keys", + detail: widget.frostWalletData!.keys, + button: + Util.isDesktop + ? IconCopyButton( + data: + widget.frostWalletData!.keys, + ) + : SimpleCopyButton( + data: + widget.frostWalletData!.keys, + ), ), - ), - if (prevGen) - const SizedBox( - height: 12, - ), - if (prevGen) - DetailItem( - title: "Previous multisig config", - detail: - widget.frostWalletData!.prevGen!.config, - button: Util.isDesktop - ? IconCopyButton( - data: widget - .frostWalletData!.prevGen!.config, - ) - : SimpleCopyButton( - data: widget - .frostWalletData!.prevGen!.config, - ), - ), - if (prevGen) - const SizedBox( - height: 16, - ), - if (prevGen) - DetailItem( - title: "Previous keys", - detail: widget.frostWalletData!.prevGen!.keys, - button: Util.isDesktop - ? IconCopyButton( - data: widget - .frostWalletData!.prevGen!.keys, - ) - : SimpleCopyButton( - data: widget - .frostWalletData!.prevGen!.keys, - ), - ), + if (prevGen) const SizedBox(height: 24), + if (prevGen) + RoundedWhiteContainer( + child: Text( + "Previous generation info", + style: STextStyles.label(context), + ), + ), + if (prevGen) const SizedBox(height: 12), + if (prevGen) + DetailItem( + title: "Previous multisig config", + detail: + widget + .frostWalletData! + .prevGen! + .config, + button: + Util.isDesktop + ? IconCopyButton( + data: + widget + .frostWalletData! + .prevGen! + .config, + ) + : SimpleCopyButton( + data: + widget + .frostWalletData! + .prevGen! + .config, + ), + ), + if (prevGen) const SizedBox(height: 16), + if (prevGen) + DetailItem( + title: "Previous keys", + detail: + widget.frostWalletData!.prevGen!.keys, + button: + Util.isDesktop + ? IconCopyButton( + data: + widget + .frostWalletData! + .prevGen! + .keys, + ) + : SimpleCopyButton( + data: + widget + .frostWalletData! + .prevGen! + .keys, + ), + ), - const Spacer(), - const SizedBox( - height: 16, - ), - PrimaryButton( - label: "Continue", - onPressed: _continuePressed, + const Spacer(), + const SizedBox(height: 16), + PrimaryButton( + label: "Continue", + onPressed: _continuePressed, + ), + ], ), - ], + ), ), + ); + }, + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 4), + Text( + ref.watch(pWalletName(widget.walletId)), + textAlign: TextAlign.center, + style: STextStyles.label( + context, + ).copyWith(fontSize: 12), ), - ), - ); - }, - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - ref.watch(pWalletName(widget.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, + const SizedBox(height: 4), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), ), - ), - child: Padding( - padding: const EdgeInsets.all(12), - 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.label(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + 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 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.label(context).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), ), ), - ), - ), - const SizedBox( - height: 8, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: _mnemonic, - isDesktop: false, + const SizedBox(height: 8), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: _mnemonic, + isDesktop: false, + ), + ), ), - ), - ), - const SizedBox( - height: 16, - ), - TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: _continuePressed, - child: Text( - "Continue", - style: STextStyles.button(context), - ), + const SizedBox(height: 16), + TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: _continuePressed, + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ], ), - ], - ), + ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart index 39a723d66..d6bc5f2e2 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart @@ -26,10 +26,7 @@ import 'delete_view_only_wallet_keys_view.dart'; import 'delete_wallet_recovery_phrase_view.dart'; class DeleteWalletWarningView extends ConsumerWidget { - const DeleteWalletWarningView({ - super.key, - required this.walletId, - }); + const DeleteWalletWarningView({super.key, required this.walletId}); static const String routeName = "/deleteWalletWarning"; @@ -47,143 +44,132 @@ class DeleteWalletWarningView extends ConsumerWidget { }, ), ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 32, - ), - Center( - child: Text( - "Attention!", - style: STextStyles.pageTitleH1(context), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 32), + Center( + child: Text( + "Attention!", + style: STextStyles.pageTitleH1(context), + ), ), - ), - const SizedBox( - height: 16, - ), - RoundedContainer( - color: Theme.of(context) - .extension()! - .warningBackground, - child: Text( - "You are going to permanently delete your wallet.\n\n" - "If you delete your wallet, the only way you can have access" - " to your funds is by using your backup key.\n\n" - "${AppConfig.appName} does not keep nor is able to restore " - "your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + const SizedBox(height: 16), + RoundedContainer( + color: + Theme.of( + context, + ).extension()!.warningBackground, + child: Text( + "You are going to permanently delete your wallet.\n\n" + "If you delete your wallet, the only way you can have access" + " to your funds is by using your backup key.\n\n" + "${AppConfig.appName} does not keep nor is able to restore " + "your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", + style: STextStyles.baseXS(context).copyWith( + color: + Theme.of( + context, + ).extension()!.warningForeground, + ), ), ), - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + const Spacer(), + TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), ), ), - ), - const SizedBox( - height: 12, - ), - TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () async { - final wallet = ref.read(pWallets).getWallet(walletId); + const SizedBox(height: 12), + TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () async { + final wallet = ref.read(pWallets).getWallet(walletId); - // TODO: [prio=med] take wallets that don't have a mnemonic into account + // 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; - ViewOnlyWalletData? viewOnlyData; + List? mnemonic; + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostWalletData; + ViewOnlyWalletData? viewOnlyData; - if (wallet is BitcoinFrostWallet) { - final futures = [ - wallet.getSerializedKeys(), - wallet.getMultisigConfig(), - wallet.getSerializedKeysPrevGen(), - wallet.getMultisigConfigPrevGen(), - ]; + if (wallet is BitcoinFrostWallet) { + final futures = [ + wallet.getSerializedKeys(), + wallet.getMultisigConfig(), + wallet.getSerializedKeysPrevGen(), + wallet.getMultisigConfigPrevGen(), + ]; - final results = await Future.wait(futures); + final results = await Future.wait(futures); - if (results.length == 4) { - frostWalletData = ( - myName: wallet.frostInfo.myName, - config: results[1]!, - keys: results[0]!, - prevGen: results[2] == null || results[3] == null - ? null - : ( - config: results[3]!, - keys: results[2]!, - ), - ); - } - } else { - if (wallet is ViewOnlyOptionInterface && - wallet.isViewOnly) { - viewOnlyData = await wallet.getViewOnlyWalletData(); - } else if (wallet is MnemonicInterface) { - mnemonic = await wallet.getMnemonicAsWords(); - } - } - if (context.mounted) { - if (viewOnlyData != null) { - await Navigator.of(context).pushNamed( - DeleteViewOnlyWalletKeysView.routeName, - arguments: ( - walletId: walletId, - data: viewOnlyData, - ), - ); + if (results.length == 4) { + frostWalletData = ( + myName: wallet.frostInfo.myName, + config: results[1]!, + keys: results[0]!, + prevGen: + results[2] == null || results[3] == null + ? null + : (config: results[3]!, keys: results[2]!), + ); + } } else { - await Navigator.of(context).pushNamed( - DeleteWalletRecoveryPhraseView.routeName, - arguments: ( - walletId: walletId, - mnemonicWords: mnemonic ?? [], - frostWalletData: frostWalletData, - ), - ); + if (wallet is ViewOnlyOptionInterface && + wallet.isViewOnly) { + viewOnlyData = await wallet.getViewOnlyWalletData(); + } else if (wallet is MnemonicInterface) { + mnemonic = await wallet.getMnemonicAsWords(); + } + } + if (context.mounted) { + if (viewOnlyData != null) { + await Navigator.of(context).pushNamed( + DeleteViewOnlyWalletKeysView.routeName, + arguments: (walletId: walletId, data: viewOnlyData), + ); + } else { + await Navigator.of(context).pushNamed( + DeleteWalletRecoveryPhraseView.routeName, + arguments: ( + walletId: walletId, + mnemonicWords: mnemonic ?? [], + frostWalletData: frostWalletData, + ), + ); + } } - } - }, - child: Text( - "View Backup Key", - style: STextStyles.button(context), + }, + child: Text( + "View Backup Key", + style: STextStyles.button(context), + ), ), - ), - const SizedBox( - height: 16, - ), - ], + const SizedBox(height: 16), + ], + ), ), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart index 61d497452..f1d0aa5f9 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart @@ -22,10 +22,7 @@ import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; class EditRefreshHeightView extends ConsumerStatefulWidget { - const EditRefreshHeightView({ - super.key, - required this.walletId, - }); + const EditRefreshHeightView({super.key, required this.walletId}); static const String routeName = "/editRefreshHeightView"; @@ -92,8 +89,10 @@ class _EditRefreshHeightViewState extends ConsumerState { void initState() { super.initState(); _wallet = ref.read(pWallets).getWallet(widget.walletId) as LibMoneroWallet; - _controller = TextEditingController() - ..text = _wallet.libMoneroWallet!.getRefreshFromBlockHeight().toString(); + _controller = + TextEditingController() + ..text = + _wallet.libMoneroWallet!.getRefreshFromBlockHeight().toString(); } @override @@ -119,10 +118,8 @@ class _EditRefreshHeightViewState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.end, children: [ DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + onPressedOverride: + Navigator.of(context, rootNavigator: true).pop, ), ], ), @@ -130,9 +127,7 @@ class _EditRefreshHeightViewState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 32), child: child, ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), ], ), ); @@ -155,9 +150,8 @@ class _EditRefreshHeightViewState extends ConsumerState { style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, + body: SafeArea( + child: Padding(padding: const EdgeInsets.all(16), child: child), ), ), ); @@ -173,11 +167,12 @@ class _EditRefreshHeightViewState extends ConsumerState { key: const Key("restoreHeightFieldKey"), controller: _controller, focusNode: _focusNode, - style: Util.isDesktop - ? STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ) - : STextStyles.field(context), + style: + Util.isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2) + : STextStyles.field(context), enableSuggestions: false, autocorrect: false, autofocus: true, @@ -188,48 +183,39 @@ class _EditRefreshHeightViewState extends ConsumerState { _focusNode, context, ).copyWith( - suffixIcon: _controller.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: ConditionalParent( - condition: Util.isDesktop, - builder: (child) => SizedBox( - height: 70, - child: child, - ), - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _controller.text = ""; - }); - }, - ), - ], + suffixIcon: + _controller.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: ConditionalParent( + condition: Util.isDesktop, + builder: + (child) => + SizedBox(height: 70, child: child), + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _controller.text = ""; + }); + }, + ), + ], + ), ), ), - ), - ) - : Util.isDesktop - ? const SizedBox( - height: 70, - ) + ) + : Util.isDesktop + ? const SizedBox(height: 70) : null, ), ), ), - Util.isDesktop - ? const SizedBox( - height: 32, - ) - : const Spacer(), - PrimaryButton( - label: "Save", - onPressed: _save, - ), + Util.isDesktop ? const SizedBox(height: 32) : const Spacer(), + PrimaryButton(label: "Save", onPressed: _save), ], ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart deleted file mode 100644 index da8c328bf..000000000 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart +++ /dev/null @@ -1,207 +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:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -import '../../../../providers/db/main_db_provider.dart'; -import '../../../../providers/global/wallets_provider.dart'; -import '../../../../themes/stack_colors.dart'; -import '../../../../utilities/logger.dart'; -import '../../../../utilities/show_loading.dart'; -import '../../../../utilities/text_styles.dart'; -import '../../../../utilities/util.dart'; -import '../../../../wallets/isar/models/wallet_info.dart'; -import '../../../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../../../widgets/background.dart'; -import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; -import '../../../../widgets/desktop/primary_button.dart'; -import '../../../../widgets/desktop/secondary_button.dart'; -import '../../../../widgets/stack_dialog.dart'; - -class LelantusSettingsView extends ConsumerStatefulWidget { - const LelantusSettingsView({ - super.key, - required this.walletId, - }); - - static const String routeName = "/lelantusSettings"; - - final String walletId; - - @override - ConsumerState createState() => - _LelantusSettingsViewState(); -} - -class _LelantusSettingsViewState extends ConsumerState { - bool _isUpdatingLelantusScanning = false; - - Future _switchToggled(bool newValue) async { - if (_isUpdatingLelantusScanning) return; - _isUpdatingLelantusScanning = true; // Lock mutex. - - try { - // Toggle enableLelantusScanning in wallet info. - await ref.read(pWalletInfo(widget.walletId)).updateOtherData( - newEntries: { - WalletInfoKeys.enableLelantusScanning: newValue, - }, - isar: ref.read(mainDBProvider).isar, - ); - if (newValue) { - await _doRescanMaybe(); - } - } finally { - // ensure _isUpdatingLelantusScanning is set to false no matter what - _isUpdatingLelantusScanning = false; - } - } - - Future _doRescanMaybe() async { - final shouldRescan = await showDialog( - context: context, - builder: (context) { - return StackDialog( - title: "Rescan may be required", - message: "A blockchain rescan may be required to fully recover all " - "lelantus history. This may take a while.", - leftButton: SecondaryButton( - label: "Rescan now", - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - rightButton: PrimaryButton( - label: "Later", - onPressed: () => Navigator.of(context).pop(false), - ), - ); - }, - ); - - if (mounted && shouldRescan == true) { - try { - if (!Platform.isLinux) await WakelockPlus.enable(); - - Exception? e; - if (mounted) { - await showLoading( - whileFuture: ref.read(pWallets).getWallet(widget.walletId).recover( - isRescan: true, - ), - context: context, - message: "Rescanning blockchain", - subMessage: "This may take a while." - "\nPlease do not exit this screen.", - rootNavigator: Util.isDesktop, - onException: (ex) => e = ex, - ); - - if (e != null) { - throw e!; - } - } - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); - if (mounted) { - // show error - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) => StackDialog( - title: "Rescan failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of(context, rootNavigator: Util.isDesktop).pop(); - }, - ), - ), - ); - } - } finally { - if (!Platform.isLinux) await WakelockPlus.disable(); - } - } - } - - @override - Widget build(BuildContext context) { - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Lelantus settings", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - pWalletInfo(widget.walletId) - .select((value) => value.otherData), - )[WalletInfoKeys.enableLelantusScanning] as bool? ?? - false, - onValueChanged: _switchToggled, - ), - ), - const SizedBox( - width: 16, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Scan for Lelantus transactions", - style: STextStyles.smallMed12(context), - ), - // Text( - // detail, - // style: STextStyles.desktopTextExtraExtraSmall(context), - // ), - ], - ), - ], - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart index 088b33379..aefbe6e79 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart @@ -11,10 +11,7 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; class RbfSettingsView extends ConsumerStatefulWidget { - const RbfSettingsView({ - super.key, - required this.walletId, - }); + const RbfSettingsView({super.key, required this.walletId}); static const String routeName = "/rbfSettings"; @@ -34,12 +31,12 @@ class _RbfSettingsViewState extends ConsumerState { try { // Toggle enableOptInRbf in wallet info. - await ref.read(pWalletInfo(widget.walletId)).updateOtherData( - newEntries: { - WalletInfoKeys.enableOptInRbf: newValue, - }, - isar: ref.read(mainDBProvider).isar, - ); + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.enableOptInRbf: newValue}, + isar: ref.read(mainDBProvider).isar, + ); } finally { // ensure _switchRbfToggledLock is set to false no matter what _switchRbfToggledLock = false; @@ -57,46 +54,46 @@ class _RbfSettingsViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "RBF settings", - style: STextStyles.navBarTitle(context), - ), + title: Text("RBF settings", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - const SizedBox(width: 3), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: ref.watch( - pWalletInfo(widget.walletId) - .select((value) => value.otherData), - )[WalletInfoKeys.enableOptInRbf] as bool? ?? - false, - onValueChanged: _switchRbfToggled, - ), - ), - const SizedBox( - width: 16, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Enable opt-in RBF", - style: STextStyles.w600_20(context), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.enableOptInRbf] + as bool? ?? + false, + onValueChanged: _switchRbfToggled, ), - ], - ), - ], - ), - ], + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enable opt-in RBF", + style: STextStyles.w600_20(context), + ), + ], + ), + ], + ), + ], + ), ), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart index 319cc067f..04991bbb3 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart @@ -12,6 +12,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/db/main_db_provider.dart'; import '../../../../themes/stack_colors.dart'; @@ -26,10 +27,7 @@ import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; class RenameWalletView extends ConsumerStatefulWidget { - const RenameWalletView({ - super.key, - required this.walletId, - }); + const RenameWalletView({super.key, required this.walletId}); static const String routeName = "/renameWallet"; @@ -73,105 +71,104 @@ class _RenameWalletViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Rename wallet", - style: STextStyles.navBarTitle(context), - ), + title: Text("Rename wallet", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _controller, - focusNode: _focusNode, - style: STextStyles.field(context), - onChanged: (_) => setState(() {}), - decoration: standardInputDecoration( - "Wallet name", - _focusNode, - context, - ).copyWith( - suffixIcon: _controller.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _controller.text = ""; - }); - }, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _controller, + focusNode: _focusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Wallet name", + _focusNode, + context, + ).copyWith( + suffixIcon: + _controller.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _controller.text = ""; + }); + }, + ), + ], ), - ], - ), - ), - ) - : null, + ), + ) + : null, + ), ), ), - ), - const Spacer(), - TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () async { - final newName = _controller.text; + const Spacer(), + TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () async { + final newName = _controller.text; - String? errMessage; - try { - await ref.read(pWalletInfo(walletId)).updateName( - newName: newName, - isar: ref.read(mainDBProvider).isar, - ); - } catch (e) { - if (e - .toString() - .contains("Empty wallet name not allowed!")) { - errMessage = "Empty wallet name not allowed."; - } else { - errMessage = e.toString(); + String? errMessage; + try { + await ref + .read(pWalletInfo(walletId)) + .updateName( + newName: newName, + isar: ref.read(mainDBProvider).isar, + ); + } catch (e) { + if (e.toString().contains( + "Empty wallet name not allowed!", + )) { + errMessage = "Empty wallet name not allowed."; + } else { + errMessage = e.toString(); + } } - } - if (mounted) { - if (errMessage == null) { - Navigator.of(context).pop(); - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Wallet renamed", - context: context, - ), - ); - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Wallet named \"$newName\" already exists", - context: context, - ), - ); + if (mounted) { + if (errMessage == null) { + Navigator.of(context).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Wallet renamed", + context: context, + ), + ); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Wallet named \"$newName\" already exists", + context: context, + ), + ); + } } - } - }, - child: Text( - "Save", - style: STextStyles.button(context), + }, + child: Text("Save", style: STextStyles.button(context)), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart index 934736311..99bd2f94c 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart @@ -10,10 +10,7 @@ import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/detail_item.dart'; class SparkInfoView extends ConsumerWidget { - const SparkInfoView({ - super.key, - required this.walletId, - }); + const SparkInfoView({super.key, required this.walletId}); static const String routeName = "/sparkInfo"; @@ -30,33 +27,32 @@ class SparkInfoView extends ConsumerWidget { Navigator.of(context).pop(); }, ), - title: Text( - "Spark Info", - style: STextStyles.navBarTitle(context), - ), + title: Text("Spark Info", style: STextStyles.navBarTitle(context)), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FutureBuilder( - future: FiroCacheCoordinator.getSparkCacheSize( - ref.watch(pWalletCoin(walletId)).network, - ), - builder: (_, snapshot) { - String detail = "Loading..."; - if (snapshot.connectionState == ConnectionState.done) { - detail = snapshot.data ?? detail; - } + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FutureBuilder( + future: FiroCacheCoordinator.getSparkCacheSize( + ref.watch(pWalletCoin(walletId)).network, + ), + builder: (_, snapshot) { + String detail = "Loading..."; + if (snapshot.connectionState == ConnectionState.done) { + detail = snapshot.data ?? detail; + } - return DetailItem( - title: "Spark electrumx cache size", - detail: detail, - ); - }, - ), - ], + return DetailItem( + title: "Spark electrumx cache size", + detail: detail, + ); + }, + ), + ], + ), ), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index c689baf30..7792fb2e1 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -8,42 +8,43 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../models/keys/view_only_wallet_data.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/constants.dart'; +import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/isar/models/wallet_info.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; -import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; +import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; import '../../../pinpad_views/lock_screen_view.dart'; import 'delete_wallet_warning_view.dart'; import 'edit_refresh_height_view.dart'; -import 'lelantus_settings_view.dart'; import 'rbf_settings_view.dart'; import 'rename_wallet_view.dart'; import 'spark_info.dart'; class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget { - const WalletSettingsWalletSettingsView({ - super.key, - required this.walletId, - }); + const WalletSettingsWalletSettingsView({super.key, required this.walletId}); static const String routeName = "/walletSettingsWalletSettings"; @@ -57,6 +58,35 @@ class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget { class _WalletSettingsWalletSettingsViewState extends ConsumerState { late final DSBController _switchController; + late final DSBController _switchControllerMwebToggle; + + bool _switchDuressToggleLock = false; // Mutex. + Future _switchDuressToggled() async { + if (_switchDuressToggleLock) { + return; + } + _switchDuressToggleLock = true; // Lock mutex. + + try { + final visibility = ref.read(pWalletInfo(widget.walletId)).isDuressVisible; + + await ref + .read(pWalletInfo(widget.walletId)) + .updateDuressVisibilityStatus( + isDuressVisible: !visibility, + isar: ref.read(mainDBProvider).isar, + ); + } catch (e, s) { + Logging.instance.f( + "Failed to update duress visibility for wallet", + error: e, + stackTrace: s, + ); + } finally { + // ensure _switchDuressToggleLock is set to false no matter what. + _switchDuressToggleLock = false; + } + } bool _switchReuseAddressToggledLock = false; // Mutex. Future _switchReuseAddressToggled() async { @@ -90,10 +120,7 @@ class _WalletSettingsWalletSettingsViewState style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), + child: Text("Continue", style: STextStyles.button(context)), onPressed: () { Navigator.of(context).pop(true); }, @@ -114,13 +141,78 @@ class _WalletSettingsWalletSettingsViewState } } + bool _switchMwebToggleToggledLock = false; // Mutex. + Future _switchMwebToggleToggled() async { + if (_switchMwebToggleToggledLock) { + return; + } + _switchMwebToggleToggledLock = true; // Lock mutex. + + try { + if (_switchControllerMwebToggle.isOn?.call() != true) { + final canContinue = await showDialog( + context: context, + builder: (context) { + return StackDialog( + title: "Notice", + message: + "Activating MWEB requires synchronizing on-chain MWEB related data. " + "This currently requires about 800 MB of storage.", + leftButton: SecondaryButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + label: "Cancel", + ), + rightButton: PrimaryButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + label: "Continue", + ), + ); + }, + ); + + if (canContinue == true) { + await _updateMwebToggle(true); + + unawaited( + (ref.read(pWallets).getWallet(widget.walletId) as MwebInterface) + .open(), + ); + } + } else { + await _updateMwebToggle(false); + } + } finally { + // ensure _switchMwebToggleToggledLock is set to false no matter what. + _switchMwebToggleToggledLock = false; + } + } + + Future _updateMwebToggle(bool value) async { + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: value}, + isar: ref.read(mainDBProvider).isar, + ); + + if (_switchControllerMwebToggle.isOn != null) { + if (_switchControllerMwebToggle.isOn!.call() != value) { + _switchControllerMwebToggle.activate?.call(); + } + } + } + Future _updateAddressReuse(bool shouldReuse) async { - await ref.read(pWalletInfo(widget.walletId)).updateOtherData( - newEntries: { - WalletInfoKeys.reuseAddress: shouldReuse, - }, - isar: ref.read(mainDBProvider).isar, - ); + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.reuseAddress: shouldReuse}, + isar: ref.read(mainDBProvider).isar, + ); if (_switchController.isOn != null) { if (_switchController.isOn!.call() != shouldReuse) { @@ -132,6 +224,7 @@ class _WalletSettingsWalletSettingsViewState @override void initState() { _switchController = DSBController(); + _switchControllerMwebToggle = DSBController(); super.initState(); } @@ -139,7 +232,8 @@ class _WalletSettingsWalletSettingsViewState Widget build(BuildContext context) { final wallet = ref.watch(pWallets).getWallet(widget.walletId); - final isViewOnlyNoAddressGen = wallet is ViewOnlyOptionInterface && + final isViewOnlyNoAddressGen = + wallet is ViewOnlyOptionInterface && wallet.isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.addressOnly; @@ -157,56 +251,17 @@ class _WalletSettingsWalletSettingsViewState style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - 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: () { - Navigator.of(context).pushNamed( - RenameWalletView.routeName, - arguments: widget.walletId, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - "Rename wallet", - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ), - if (wallet is RbfInterface) - const SizedBox( - height: 8, - ), - if (wallet is RbfInterface) + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -215,7 +270,7 @@ class _WalletSettingsWalletSettingsViewState materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, onPressed: () { Navigator.of(context).pushNamed( - RbfSettingsView.routeName, + RenameWalletView.routeName, arguments: widget.walletId, ); }, @@ -227,7 +282,7 @@ class _WalletSettingsWalletSettingsViewState child: Row( children: [ Text( - "RBF settings", + "Rename wallet", style: STextStyles.titleBold12(context), ), ], @@ -235,145 +290,335 @@ class _WalletSettingsWalletSettingsViewState ), ), ), - if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen) - const SizedBox( - height: 8, - ), - if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen) - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (wallet is RbfInterface) const SizedBox(height: 8), + if (wallet is RbfInterface) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + RbfSettingsView.routeName, + arguments: widget.walletId, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "RBF settings", + style: STextStyles.titleBold12(context), + ), + ], + ), ), ), - onPressed: _switchReuseAddressToggled, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, + ), + if (wallet is MultiAddressInterface && + !isViewOnlyNoAddressGen) + const SizedBox(height: 8), + if (wallet is MultiAddressInterface && + !isViewOnlyNoAddressGen) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Reuse receiving address", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - SizedBox( - height: 20, - width: 40, - child: IgnorePointer( - child: DraggableSwitchButton( - isOn: ref.watch( - pWalletInfo(widget.walletId).select( - (value) => value.otherData, - ), - )[WalletInfoKeys.reuseAddress] as bool? ?? - false, - controller: _switchController, + onPressed: _switchReuseAddressToggled, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Reuse receiving address", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select( + (value) => value.otherData, + ), + )[WalletInfoKeys.reuseAddress] + as bool? ?? + false, + controller: _switchController, + ), ), ), - ), - ], + ], + ), ), ), ), - ), - if (wallet is LelantusInterface && !wallet.isViewOnly) - const SizedBox( - height: 8, - ), - if (wallet is LelantusInterface && !wallet.isViewOnly) - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (wallet is MwebInterface) const SizedBox(height: 8), + if (wallet is MwebInterface) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: _switchMwebToggleToggled, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable MWEB", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select( + (value) => value.otherData, + ), + )[WalletInfoKeys.mwebEnabled] + as bool? ?? + false, + controller: _switchControllerMwebToggle, + ), + ), + ), + ], + ), ), ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - LelantusSettingsView.routeName, - arguments: widget.walletId, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, + ), + if (!ref.watch(pDuress)) const SizedBox(height: 8), + if (!ref.watch(pDuress)) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - child: Row( - children: [ - Text( - "Lelantus settings", - style: STextStyles.titleBold12(context), - ), - ], + onPressed: _switchDuressToggled, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Show in duress", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitch( + value: + ref.watch( + pWalletInfo( + widget.walletId, + ).select( + (value) => value.otherData, + ), + )[WalletInfoKeys + .duressMarkedVisibleWalletKey] + as bool? ?? + false, + onChanged: (_) => (), + ), + ), + ), + ], + ), ), ), ), - ), - if (wallet is SparkInterface && !wallet.isViewOnly) - const SizedBox( - height: 8, - ), - if (wallet is SparkInterface && !wallet.isViewOnly) - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + if (wallet is SparkInterface && !wallet.isViewOnly) + const SizedBox(height: 8), + if (wallet is SparkInterface && !wallet.isViewOnly) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + SparkInfoView.routeName, + arguments: widget.walletId, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Spark info", + style: STextStyles.titleBold12(context), + ), + ], + ), ), ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - SparkInfoView.routeName, - arguments: widget.walletId, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, + ), + if (wallet is LibMoneroWallet || wallet is LibSalviumWallet) + const SizedBox(height: 8), + if (wallet is LibMoneroWallet || wallet is LibSalviumWallet) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), - child: Row( - children: [ - Text( - "Spark info", - style: STextStyles.titleBold12(context), - ), - ], + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + EditRefreshHeightView.routeName, + arguments: widget.walletId, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Restore height", + style: STextStyles.titleBold12(context), + ), + ], + ), ), ), ), - ), - if (wallet is LibMoneroWallet) - const SizedBox( - height: 8, - ), - if (wallet is LibMoneroWallet) + const SizedBox(height: 8), 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, + padding: const EdgeInsets.all(0), onPressed: () { - Navigator.of(context).pushNamed( - EditRefreshHeightView.routeName, - arguments: widget.walletId, + showDialog( + barrierDismissible: true, + context: context, + builder: + (_) => StackDialog( + title: + "Do you want to delete ${ref.read(pWalletName(widget.walletId))}?", + leftButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => LockscreenView( + routeOnSuccessArguments: + widget.walletId, + showBackButton: true, + routeOnSuccess: + DeleteWalletWarningView + .routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to delete wallet", + biometricsAuthenticationTitle: + "Delete wallet", + ), + settings: const RouteSettings( + name: "/deleteWalletLockscreen", + ), + ), + ); + }, + child: Text( + "Delete", + style: STextStyles.button(context), + ), + ), + ), ); }, child: Padding( @@ -384,7 +629,7 @@ class _WalletSettingsWalletSettingsViewState child: Row( children: [ Text( - "Restore height", + "Delete wallet", style: STextStyles.titleBold12(context), ), ], @@ -392,96 +637,8 @@ class _WalletSettingsWalletSettingsViewState ), ), ), - const SizedBox( - height: 8, - ), - 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, - padding: const EdgeInsets.all(0), - onPressed: () { - showDialog( - barrierDismissible: true, - context: context, - builder: (_) => StackDialog( - title: - "Do you want to delete ${ref.read(pWalletName(widget.walletId))}?", - leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: widget.walletId, - showBackButton: true, - routeOnSuccess: - DeleteWalletWarningView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to delete wallet", - biometricsAuthenticationTitle: - "Delete wallet", - ), - settings: const RouteSettings( - name: "/deleteWalletLockscreen", - ), - ), - ); - }, - child: Text( - "Delete", - style: STextStyles.button(context), - ), - ), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - "Delete wallet", - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart index 2eddd3078..35a46d6b4 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart @@ -87,115 +87,106 @@ class XPubViewState extends ConsumerState { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Wallet xpub(s)", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), - child: LayoutBuilder( - builder: (context, constraints) => SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: Column( - children: [ - Expanded( - child: child, - ), - const SizedBox( - height: 16, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Wallet xpub(s)", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), + child: LayoutBuilder( + builder: + (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Expanded(child: child), + const SizedBox(height: 16), + ], + ), + ), + ), ), - ], - ), ), ), ), ), ), - ), - ), child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 600, - maxHeight: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxWidth: 600, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "${ref.watch(pWalletName(widget.walletId))} xpub(s)", - style: STextStyles.desktopH2(context), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "${ref.watch(pWalletName(widget.walletId))} xpub(s)", + style: STextStyles.desktopH2(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of(context, rootNavigator: true).pop, + ), + ], ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + Flexible( + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 0, 32, 32), + child: SingleChildScrollView(child: child), + ), ), ], ), - Flexible( - child: Padding( - padding: const EdgeInsets.fromLTRB(32, 0, 32, 32), - child: SingleChildScrollView( - child: child, - ), - ), - ), - ], - ), - ), + ), child: Column( mainAxisSize: Util.isDesktop ? MainAxisSize.min : MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox( - height: Util.isDesktop ? 12 : 16, - ), + SizedBox(height: Util.isDesktop ? 12 : 16), DetailItem( title: "Master fingerprint", detail: widget.xpubData.fingerprint, horizontal: true, - borderColor: Util.isDesktop - ? Theme.of(context) - .extension()! - .textFieldDefaultBG - : null, - ), - SizedBox( - height: Util.isDesktop ? 12 : 16, + borderColor: + Util.isDesktop + ? Theme.of( + context, + ).extension()!.textFieldDefaultBG + : null, ), + SizedBox(height: Util.isDesktop ? 12 : 16), DetailItemBase( horizontal: true, - borderColor: Util.isDesktop - ? Theme.of(context) - .extension()! - .textFieldDefaultBG - : null, + borderColor: + Util.isDesktop + ? Theme.of( + context, + ).extension()!.textFieldDefaultBG + : null, title: Text( "Derivation", style: STextStyles.itemSubtitle(context), @@ -226,9 +217,10 @@ class XPubViewState extends ConsumerState { isExpanded: true, buttonStyleData: ButtonStyleData( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -241,9 +233,10 @@ class XPubViewState extends ConsumerState { Assets.svg.chevronDown, width: 12, height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), ), @@ -251,9 +244,10 @@ class XPubViewState extends ConsumerState { offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -269,46 +263,34 @@ class XPubViewState extends ConsumerState { ), ), ), - SizedBox( - height: Util.isDesktop ? 12 : 16, - ), + SizedBox(height: Util.isDesktop ? 12 : 16), QR( data: _current(_currentDropDownValue), - size: Util.isDesktop - ? 256 - : MediaQuery.of(context).size.width / 1.5, - ), - SizedBox( - height: Util.isDesktop ? 12 : 16, + size: + Util.isDesktop + ? 256 + : MediaQuery.of(context).size.width / 1.5, ), + SizedBox(height: Util.isDesktop ? 12 : 16), RoundedWhiteContainer( - borderColor: Util.isDesktop - ? Theme.of(context) - .extension()! - .textFieldDefaultBG - : null, + borderColor: + Util.isDesktop + ? Theme.of( + context, + ).extension()!.textFieldDefaultBG + : null, child: SelectableText( _current(_currentDropDownValue), style: STextStyles.w500_14(context), ), ), - SizedBox( - height: Util.isDesktop ? 12 : 16, - ), + SizedBox(height: Util.isDesktop ? 12 : 16), if (!Util.isDesktop) const Spacer(), Row( children: [ if (Util.isDesktop) const Spacer(), - if (Util.isDesktop) - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Copy", - onPressed: _copy, - ), - ), + if (Util.isDesktop) const SizedBox(width: 16), + Expanded(child: PrimaryButton(label: "Copy", onPressed: _copy)), ], ), ], diff --git a/lib/pages/spark_names/buy_spark_name_view.dart b/lib/pages/spark_names/buy_spark_name_view.dart new file mode 100644 index 000000000..036a551d3 --- /dev/null +++ b/lib/pages/spark_names/buy_spark_name_view.dart @@ -0,0 +1,548 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; + +import '../../../providers/providers.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../../db/drift/database.dart'; +import '../../models/isar/models/blockchain_data/address.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/show_loading.dart'; +import '../../utilities/text_styles.dart'; +import '../../wallets/crypto_currency/coins/firo.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/custom_buttons/blue_text_button.dart'; +import '../../widgets/dialogs/s_dialog.dart'; +import '../../widgets/rounded_white_container.dart'; +import 'confirm_spark_name_transaction_view.dart'; + +class BuySparkNameView extends ConsumerStatefulWidget { + const BuySparkNameView({ + super.key, + required this.walletId, + required this.name, + this.nameToRenew, + }); + + final String walletId; + final String name; + final SparkName? nameToRenew; + + static const routeName = "/buySparkNameView"; + + @override + ConsumerState createState() => _BuySparkNameViewState(); +} + +class _BuySparkNameViewState extends ConsumerState { + final addressController = TextEditingController(); + final additionalInfoController = TextEditingController(); + + bool get isRenewal => widget.nameToRenew != null; + String get _title => isRenewal ? "Renew name" : "Buy name"; + + int _years = 1; + late bool _buttonEnabled; + + bool _lockAddressFill = false; + Future _fillCurrentReceiving() async { + if (_lockAddressFill) return; + _lockAddressFill = true; + try { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as SparkInterface; + final myAddress = await wallet.getCurrentReceivingSparkAddress(); + if (myAddress == null) { + throw Exception("No spark address found"); + } + addressController.text = myAddress.value; + } catch (e, s) { + Logging.instance.e("_fillCurrentReceiving", error: e, stackTrace: s); + } finally { + _lockAddressFill = false; + } + } + + Future _preRegFuture() async { + final chosenAddress = addressController.text; + + if (chosenAddress.isEmpty) { + throw Exception( + "Please select the Spark address you want to link to your Spark Name", + ); + } + + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as SparkInterface; + + if (!(wallet.cryptoCurrency as Firo).validateSparkAddress(chosenAddress)) { + throw Exception("Invalid Spark address selected"); + } + + final myAddresses = + await wallet.mainDB.isar.addresses + .where() + .walletIdEqualTo(widget.walletId) + .filter() + .typeEqualTo(AddressType.spark) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .valueProperty() + .findAll(); + + if (!myAddresses.contains(chosenAddress)) { + throw Exception("Selected Spark address does not belong to this wallet"); + } + + final txData = await wallet.prepareSparkNameTransaction( + name: widget.name, + address: chosenAddress, + years: _years, + additionalInfo: additionalInfoController.text, + ); + return txData; + } + + bool _preRegLock = false; + Future _prepareNameTx() async { + if (_preRegLock) return; + _preRegLock = true; + try { + final txData = + (await showLoading( + whileFuture: _preRegFuture(), + context: context, + message: "Preparing transaction...", + onException: (e) { + throw e; + }, + ))!; + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmSparkNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmSparkNameTransactionView.routeName, + arguments: (walletId: widget.walletId, txData: txData), + ); + } + } + } catch (e, s) { + Logging.instance.e("_prepareNameTx failed", error: e, stackTrace: s); + + if (mounted) { + String err = e.toString(); + if (err.startsWith("Exception: ")) { + err = err.replaceFirst("Exception: ", ""); + } + + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _preRegLock = false; + } + } + + @override + void initState() { + super.initState(); + + if (isRenewal) { + additionalInfoController.text = widget.nameToRenew!.additionalInfo ?? ""; + addressController.text = widget.nameToRenew!.address; + } + _buttonEnabled = addressController.text.isNotEmpty; + addressController.addListener(() { + if (mounted) { + setState(() { + _buttonEnabled = addressController.text.isNotEmpty; + }); + } + }); + } + + @override + void dispose() { + additionalInfoController.dispose(); + addressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(widget.walletId)); + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: const AppBarBackButton(), + titleSpacing: 0, + title: Text( + _title, + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (ctx, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ); + }, + child: Column( + crossAxisAlignment: + Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Name", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + Text( + widget.name, + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), + ), + ], + ), + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Spark address", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + CustomTextButton( + text: "Use current", + onTap: _fillCurrentReceiving, + ), + ], + ), + const SizedBox(height: 4), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + controller: addressController, + readOnly: isRenewal, + textAlignVertical: TextAlignVertical.center, + minLines: 1, + maxLines: 5, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.all(16), + hintStyle: STextStyles.fieldLabel(context), + hintText: "Spark address (required)", + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ), + ], + ), + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Additional info", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + ], + ), + const SizedBox(height: 4), + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + controller: additionalInfoController, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.all(16), + hintStyle: STextStyles.fieldLabel(context), + hintText: "Additional info (optional)", + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ), + ), + ], + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${isRenewal ? "Renew" : "Register"} for", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + SizedBox( + width: Util.isDesktop ? 180 : 140, + child: DropdownButtonHideUnderline( + child: DropdownButton2( + value: _years, + items: [ + ...List.generate(10, (i) => i + 1).map( + (e) => DropdownMenuItem( + value: e, + child: Text( + "$e years", + style: STextStyles.w500_14(context), + ), + ), + ), + ], + onChanged: (value) { + if (value is int) { + setState(() { + _years = value; + }); + } + }, + isExpanded: true, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + ), + ), + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + maxHeight: 250, + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + ), + ), + ], + ), + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Cost", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + Amount.fromDecimal( + Decimal.fromInt( + kStandardSparkNamesFee[widget.name.length] * _years, + ), + fractionDigits: coin.fractionDigits, + ), + ), + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), + ), + ], + ), + ), + + SizedBox(height: Util.isDesktop ? 32 : 16), + if (!Util.isDesktop) const Spacer(), + PrimaryButton( + label: isRenewal ? "Renew" : "Buy", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + enabled: _buttonEnabled, + onPressed: _buttonEnabled ? _prepareNameTx : null, + ), + SizedBox(height: Util.isDesktop ? 32 : 16), + ], + ), + ); + } +} diff --git a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart new file mode 100644 index 000000000..c98409dfd --- /dev/null +++ b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart @@ -0,0 +1,977 @@ +/* + * 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:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../models/isar/models/transaction_note.dart'; +import '../../notifications/show_flush_bar.dart'; +import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; +import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; +import '../../providers/providers.dart'; +import '../../route_generator.dart'; +import '../../themes/stack_colors.dart'; +import '../../themes/theme_providers.dart'; +import '../../utilities/amount/amount.dart'; +import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/logger.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/models/tx_data.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; +import '../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../widgets/desktop/primary_button.dart'; +import '../../widgets/icon_widgets/x_icon.dart'; +import '../../widgets/rounded_container.dart'; +import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; +import '../../widgets/stack_text_field.dart'; +import '../../widgets/textfield_icon_button.dart'; +import '../pinpad_views/lock_screen_view.dart'; +import '../send_view/sub_widgets/sending_transaction_dialog.dart'; + +class ConfirmSparkNameTransactionView extends ConsumerStatefulWidget { + const ConfirmSparkNameTransactionView({ + super.key, + required this.txData, + required this.walletId, + }); + + static const String routeName = "/confirmSparkNameTransactionView"; + + final TxData txData; + final String walletId; + + @override + ConsumerState createState() => + _ConfirmSparkNameTransactionViewState(); +} + +class _ConfirmSparkNameTransactionViewState + extends ConsumerState { + late final String walletId; + late final bool isDesktop; + + late final FocusNode _noteFocusNode; + late final TextEditingController noteController; + + Future _attemptSend() async { + final wallet = ref.read(pWallets).getWallet(walletId) as SparkInterface; + final coin = wallet.info.coin; + + final sendProgressController = ProgressAndSuccessController(); + + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return SendingTransactionDialog( + coin: coin, + controller: sendProgressController, + ); + }, + ), + ); + + final time = Future.delayed(const Duration(milliseconds: 2500)); + + final List txids = []; + Future txDataFuture; + + final note = noteController.text; + + try { + txDataFuture = wallet.confirmSendSpark(txData: widget.txData); + + // await futures in parallel + final futureResults = await Future.wait([txDataFuture, time]); + + final txData = (futureResults.first as TxData); + + sendProgressController.triggerSuccess?.call(); + + // await futures in parallel + await Future.wait([ + // wait for animation + Future.delayed(const Duration(seconds: 5)), + ]); + + txids.add(txData.txid!); + ref.refresh(desktopUseUTXOs); + + // save note + for (final txid in txids) { + await ref + .read(mainDBProvider) + .putTransactionNote( + TransactionNote(walletId: walletId, txid: txid, value: note), + ); + } + + unawaited(wallet.refresh()); + + if (mounted) { + // pop sending dialog + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + // pop confirm send view + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + // pop buy popup + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + } + } catch (e, s) { + const niceError = "Broadcast name transaction failed"; + + Logging.instance.e(niceError, error: e, stackTrace: s); + + if (mounted) { + // pop sending dialog + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(niceError, style: STextStyles.desktopH3(context)), + const SizedBox(height: 24), + Flexible( + child: SingleChildScrollView( + child: SelectableText( + e.toString(), + style: STextStyles.smallMed14(context), + ), + ), + ), + const SizedBox(height: 56), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ); + } else { + return StackDialog( + title: niceError, + 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(); + }, + ), + ); + } + }, + ); + } + } + } + + @override + void initState() { + isDesktop = Util.isDesktop; + walletId = widget.walletId; + _noteFocusNode = FocusNode(); + noteController = TextEditingController(); + noteController.text = widget.txData.note ?? ""; + + super.initState(); + } + + @override + void dispose() { + noteController.dispose(); + + _noteFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(walletId)); + + final unit = coin.ticker; + + final fee = widget.txData.fee; + final amountWithoutChange = widget.txData.amountWithoutChange!; + + return ConditionalParent( + condition: !isDesktop, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: + () => + Navigator.of(context, rootNavigator: true).pop(), + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), + ), + ], + ), + Flexible(child: SingleChildScrollView(child: child)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Confirm Name transaction", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text("Name", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), + Text( + widget.txData.sparkNameInfo!.name, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Additional info", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 4), + Text( + widget.txData.sparkNameInfo!.additionalInfo, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Recipient", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 4), + Text( + widget.txData.recipients!.first.address, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Registration fee", + style: STextStyles.smallMed12(context), + ), + SelectableText( + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), + SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (widget.txData.fee != null && widget.txData.vSize != null) + const SizedBox(height: 12), + if (widget.txData.fee != null && widget.txData.vSize != null) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "sats/vByte", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 4), + SelectableText( + "~${fee.raw.toInt() ~/ widget.txData.vSize!}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (widget.txData.note != null && + widget.txData.note!.isNotEmpty) + const SizedBox(height: 12), + if (widget.txData.note != null && + widget.txData.note!.isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text("Note", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), + SelectableText( + widget.txData.note!, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ], + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 50, + ), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: + Theme.of(context).extension()!.background, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.background, + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 22, + ), + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch( + themeProvider.select( + (value) => value.assets.send, + ), + ), + ), + width: 32, + height: 32, + ), + const SizedBox(width: 16), + Text( + "Send $unit Name transaction", + style: STextStyles.desktopTextMedium(context), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Name", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + SelectableText( + widget.txData.sparkNameInfo!.name, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], + ), + ), + Container( + height: 1, + color: + Theme.of( + context, + ).extension()!.background, + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Additional info", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + SelectableText( + widget.txData.sparkNameInfo!.additionalInfo, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], + ), + ), + ], + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(left: 32, right: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "Note (optional)", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + autocorrect: isDesktop ? false : true, + enableSuggestions: isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + 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: 20), + ], + ), + ), + + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 16, left: 32), + child: Text( + "Registration fee", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: Builder( + builder: (context) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + ); + String fiatAmount = "N/A"; + + if (externalCalls) { + final price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + ?.value; + if (price != null && price > Decimal.zero) { + fiatAmount = (amountWithoutChange.decimal * price) + .toAmount(fractionDigits: 2) + .fiatString( + locale: + ref + .read( + localeServiceChangeNotifierProvider, + ) + .locale, + ); + } + } + + return Row( + children: [ + SelectableText( + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange), + style: STextStyles.itemSubtitle(context), + ), + if (externalCalls) + Text( + " | ", + style: STextStyles.itemSubtitle(context), + ), + if (externalCalls) + SelectableText( + "~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.itemSubtitle(context), + ), + ], + ); + }, + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 16, left: 32), + child: Text( + "Recipient", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: SelectableText( + widget.txData.recipients!.first.address, + style: STextStyles.itemSubtitle(context), + ), + ), + ), + + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 16, left: 32), + child: Text( + "Transaction fee", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle(context), + ), + ), + ), + if (isDesktop && + widget.txData.fee != null && + widget.txData.vSize != null) + Padding( + padding: const EdgeInsets.only(top: 16, left: 32), + child: Text( + "sats/vByte", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop && + widget.txData.fee != null && + widget.txData.vSize != null) + Padding( + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: SelectableText( + "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + if (!isDesktop) const Spacer(), + SizedBox(height: isDesktop ? 23 : 12), + Padding( + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), + child: RoundedContainer( + padding: + isDesktop + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ) + : const EdgeInsets.all(12), + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isDesktop ? "Total amount to send" : "Total amount", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + ), + SelectableText( + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange + fee!), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + ), + SizedBox(height: isDesktop ? 28 : 16), + Padding( + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + final dynamic unlocked; + + if (isDesktop) { + unlocked = await showDialog( + context: context, + builder: + (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: + "Confirm Transaction", + ), + settings: const RouteSettings( + name: "/confirmsendlockscreen", + ), + ), + ); + } + + if (mounted) { + if (unlocked == true) { + unawaited(_attemptSend()); + } else { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: + Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", + context: context, + ), + ); + } + } + } + }, + ), + ), + if (isDesktop) const SizedBox(height: 32), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spark_names/spark_names_home_view.dart b/lib/pages/spark_names/spark_names_home_view.dart new file mode 100644 index 000000000..e87889ce5 --- /dev/null +++ b/lib/pages/spark_names/spark_names_home_view.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_app_bar.dart'; +import '../../widgets/desktop/desktop_scaffold.dart'; +import '../../widgets/toggle.dart'; +import 'sub_widgets/buy_spark_name_option_widget.dart'; +import 'sub_widgets/manage_spark_names_option_widget.dart'; + +class SparkNamesHomeView extends ConsumerStatefulWidget { + const SparkNamesHomeView({super.key, required this.walletId}); + + final String walletId; + + static const String routeName = "/sparkNamesHomeView"; + + @override + ConsumerState createState() => + _NamecoinNamesHomeViewState(); +} + +class _NamecoinNamesHomeViewState extends ConsumerState { + bool _onManage = true; + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + + return MasterScaffold( + isDesktop: isDesktop, + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 24, right: 20), + 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, + ), + ), + SvgPicture.asset( + Assets.svg.robotHead, + width: 32, + height: 32, + color: + Theme.of(context).extension()!.textDark, + ), + const SizedBox(width: 10), + Text("Names", style: STextStyles.desktopH3(context)), + ], + ), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Names", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: ConditionalParent( + condition: !isDesktop, + builder: + (child) => SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: child, + ), + ), + child: + Util.isDesktop + ? Padding( + padding: const EdgeInsets.only(top: 24, left: 24, right: 24), + child: Row( + children: [ + SizedBox( + width: 460, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Register", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ], + ), + const SizedBox(height: 14), + Flexible( + child: BuySparkNameOptionWidget( + walletId: widget.walletId, + ), + ), + ], + ), + ), + const SizedBox(width: 24), + Flexible( + child: SizedBox( + width: 520, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Names", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ], + ), + const SizedBox(height: 14), + Flexible( + child: SingleChildScrollView( + child: ManageSparkNamesOptionWidget( + walletId: widget.walletId, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: + Theme.of(context).extension()!.popupBG, + offColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + onText: "Register", + offText: "Names", + isOn: !_onManage, + onValueChanged: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { + _onManage = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + const SizedBox(height: 16), + Expanded( + child: IndexedStack( + index: _onManage ? 0 : 1, + children: [ + BuySparkNameOptionWidget(walletId: widget.walletId), + LayoutBuilder( + builder: (context, constraints) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: SingleChildScrollView( + child: IntrinsicHeight( + child: ManageSparkNamesOptionWidget( + walletId: widget.walletId, + ), + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart b/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart new file mode 100644 index 000000000..18b6bb6f4 --- /dev/null +++ b/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart @@ -0,0 +1,376 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../providers/providers.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/show_loading.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_white_container.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../buy_spark_name_view.dart'; + +class BuySparkNameOptionWidget extends ConsumerStatefulWidget { + const BuySparkNameOptionWidget({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => + _BuySparkNameWidgetState(); +} + +class _BuySparkNameWidgetState extends ConsumerState { + final _nameController = TextEditingController(); + final _nameFieldFocus = FocusNode(); + + bool _isAvailable = false; + bool _isInvalidCharacters = false; + String? _lastLookedUpName; + + Future _checkIsAvailable(String name) async { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as SparkInterface; + + try { + await wallet.electrumXClient.getSparkNameData(sparkName: name); + // name exists + return false; + } catch (e) { + if (e.toString().contains( + "(method not found): unknown method \"spark.getsparknamedata\"", + )) { + rethrow; + } + // name not found + return true; + } + } + + bool _lookupLock = false; + Future _lookup() async { + if (_lookupLock) return; + _lookupLock = true; + try { + _isAvailable = false; + + _lastLookedUpName = _nameController.text; + final result = await showLoading( + whileFuture: _checkIsAvailable(_lastLookedUpName!), + context: context, + message: "Searching...", + onException: (e) => throw e, + rootNavigator: Util.isDesktop, + delay: const Duration(seconds: 2), + ); + + _isAvailable = result == true; + + if (mounted) { + setState(() {}); + } + + Logging.instance.i("LOOKUP RESULT: $result"); + } catch (e, s) { + final String message; + if (e.toString().contains( + "(method not found): unknown method \"spark.getsparknamedata\"", + )) { + message = e.toString(); + } else { + message = "Spark name lookup failed"; + } + + Logging.instance.e(message, error: e, stackTrace: s); + + if (mounted) { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: message, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _lookupLock = false; + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _nameFieldFocus.requestFocus(); + } + }); + } + + @override + void dispose() { + _nameController.dispose(); + _nameFieldFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: + Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Container( + height: 48, + width: 100, + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.all( + Radius.circular(Constants.size.circularBorderRadius), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextField( + inputFormatters: [ + LengthLimitingTextInputFormatter(kMaxNameLength), + ], + textInputAction: TextInputAction.search, + focusNode: _nameFieldFocus, + controller: _nameController, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + prefixIcon: Padding( + padding: const EdgeInsets.all(14), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + color: + Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, + ), + ), + fillColor: Colors.transparent, + hintText: "Find a spark name", + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + onSubmitted: (_) { + if (_nameController.text.isNotEmpty) { + _lookup(); + } + }, + onChanged: (value) { + // trigger look up button enabled/disabled state change + setState(() { + _isInvalidCharacters = + value.isNotEmpty && + !RegExp(kNameRegexString).hasMatch(value); + }); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: + _isInvalidCharacters + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.end, + children: [ + if (_isInvalidCharacters) + Text( + "Invalid name", + style: STextStyles.w500_10(context).copyWith( + color: Theme.of(context).extension()!.textError, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Builder( + builder: (context) { + final length = _nameController.text.length; + return Text( + "$length/$kMaxNameLength", + style: STextStyles.w500_10(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle2, + ), + ); + }, + ), + ), + ], + ), + SizedBox(height: Util.isDesktop ? 24 : 16), + SecondaryButton( + label: "Lookup", + enabled: + _nameController.text.isNotEmpty && + RegExp(kNameRegexString).hasMatch(_nameController.text), + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _lookup, + ), + const SizedBox(height: 32), + if (_lastLookedUpName != null) + _NameCard( + walletId: widget.walletId, + isAvailable: _isAvailable, + name: _lastLookedUpName!, + ), + ], + ); + } +} + +class _NameCard extends ConsumerWidget { + const _NameCard({ + super.key, + required this.walletId, + required this.isAvailable, + required this.name, + }); + + final String walletId; + final bool isAvailable; + final String name; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final availability = isAvailable ? "Available" : "Unavailable"; + final color = + isAvailable + ? Theme.of(context).extension()!.accentColorGreen + : Theme.of(context).extension()!.accentColorRed; + + final style = + (Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_12(context)); + + return RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 24 : 16), + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(name, style: style), + const SizedBox(height: 4), + Text(availability, style: style.copyWith(color: color)), + ], + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PrimaryButton( + label: "Buy name", + enabled: isAvailable, + buttonHeight: + Util.isDesktop ? ButtonHeight.m : ButtonHeight.l, + width: Util.isDesktop ? 140 : 120, + onPressed: () async { + if (context.mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Buy name", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: BuySparkNameView( + walletId: walletId, + name: name, + ), + ), + ], + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + BuySparkNameView.routeName, + arguments: (walletId: walletId, name: name), + ); + } + } + }, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spark_names/sub_widgets/manage_spark_names_option_widget.dart b/lib/pages/spark_names/sub_widgets/manage_spark_names_option_widget.dart new file mode 100644 index 000000000..18a554cee --- /dev/null +++ b/lib/pages/spark_names/sub_widgets/manage_spark_names_option_widget.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../providers/db/drift_provider.dart'; +import '../../../utilities/util.dart'; +import 'owned_spark_name_card.dart'; + +class ManageSparkNamesOptionWidget extends ConsumerStatefulWidget { + const ManageSparkNamesOptionWidget({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => + _ManageSparkNamesWidgetState(); +} + +class _ManageSparkNamesWidgetState + extends ConsumerState { + @override + Widget build(BuildContext context) { + final db = ref.watch(pDrift(widget.walletId)); + return StreamBuilder( + stream: db.select(db.sparkNames).watch(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + children: [ + ...snapshot.data!.map( + (e) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: OwnedSparkNameCard( + key: ValueKey(e), + name: e, + walletId: widget.walletId, + ), + ), + ), + SizedBox(height: Util.isDesktop ? 14 : 6), + ], + ); + } else { + return Container(); + } + }, + ); + } +} diff --git a/lib/pages/spark_names/sub_widgets/owned_spark_name_card.dart b/lib/pages/spark_names/sub_widgets/owned_spark_name_card.dart new file mode 100644 index 000000000..fc813efb9 --- /dev/null +++ b/lib/pages/spark_names/sub_widgets/owned_spark_name_card.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../db/drift/database.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_white_container.dart'; +import 'spark_name_details.dart'; + +class OwnedSparkNameCard extends ConsumerStatefulWidget { + const OwnedSparkNameCard({ + super.key, + required this.name, + required this.walletId, + }); + + final SparkName name; + final String walletId; + + @override + ConsumerState createState() => _OwnedSparkNameCardState(); +} + +class _OwnedSparkNameCardState extends ConsumerState { + (String, Color) _getExpiry(int currentChainHeight, StackColors theme) { + final String message; + final Color color; + + final remaining = widget.name.validUntil - currentChainHeight; + + if (remaining <= 0) { + color = theme.accentColorRed; + message = "Expired"; + } else { + message = "Expires in $remaining blocks"; + if (remaining < 1000) { + // todo change arbitrary 1000 to something else? + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } + } + + return (message, color); + } + + bool _lock = false; + + Future _showDetails() async { + if (_lock) return; + _lock = true; + try { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => SDialog( + child: SparkNameDetailsView( + name: widget.name, + walletId: widget.walletId, + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + SparkNameDetailsView.routeName, + arguments: (name: widget.name, walletId: widget.walletId), + ); + } + } finally { + _lock = false; + } + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final (message, color) = _getExpiry( + ref.watch(pWalletChainHeight(widget.walletId)), + Theme.of(context).extension()!, + ); + + return RoundedWhiteContainer( + padding: + Util.isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(widget.name.name), + const SizedBox(height: 8), + SelectableText( + message, + style: STextStyles.w500_12(context).copyWith(color: color), + ), + ], + ), + const SizedBox(width: 12), + PrimaryButton( + label: "Details", + width: Util.isDesktop ? 90 : null, + buttonHeight: Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l, + onPressed: _showDetails, + ), + ], + ), + ); + } +} diff --git a/lib/pages/spark_names/sub_widgets/spark_name_details.dart b/lib/pages/spark_names/sub_widgets/spark_name_details.dart new file mode 100644 index 000000000..3acaad8f5 --- /dev/null +++ b/lib/pages/spark_names/sub_widgets/spark_name_details.dart @@ -0,0 +1,464 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../db/drift/database.dart'; +import '../../../models/isar/models/isar_models.dart'; +import '../../../providers/db/drift_provider.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../widgets/background.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../widgets/custom_buttons/simple_copy_button.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_container.dart'; +import '../../wallet_view/transaction_views/transaction_details_view.dart'; +import '../buy_spark_name_view.dart'; + +class SparkNameDetailsView extends ConsumerStatefulWidget { + const SparkNameDetailsView({ + super.key, + required this.name, + required this.walletId, + }); + + static const routeName = "/sparkNameDetails"; + + final SparkName name; + final String walletId; + + @override + ConsumerState createState() => + _SparkNameDetailsViewState(); +} + +class _SparkNameDetailsViewState extends ConsumerState { + // todo change arbitrary 1000 to something else? + static const _remainingMagic = 1000; + + late Stream _nameStream; + late SparkName name; + + Stream? _labelStream; + AddressLabel? label; + + (String, Color, int) _getExpiry(int currentChainHeight, StackColors theme) { + final String message; + final Color color; + + final remaining = name.validUntil - currentChainHeight; + + if (remaining <= 0) { + color = theme.accentColorRed; + message = "Expired"; + } else { + message = "Expires in $remaining blocks"; + if (remaining < _remainingMagic) { + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } + } + + return (message, color, remaining); + } + + bool _lock = false; + + Future _renew() async { + if (_lock) return; + _lock = true; + try { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Renew name", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: BuySparkNameView( + walletId: widget.walletId, + name: name.name, + nameToRenew: name, + ), + ), + ], + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + BuySparkNameView.routeName, + arguments: ( + walletId: widget.walletId, + name: name.name, + nameToRenew: name, + ), + ); + } + } finally { + _lock = false; + } + } + + @override + void initState() { + super.initState(); + name = widget.name; + + label = ref + .read(mainDBProvider) + .getAddressLabelSync(widget.walletId, name.address); + + if (label != null) { + _labelStream = ref.read(mainDBProvider).watchAddressLabel(id: label!.id); + } + + final db = ref.read(pDrift(widget.walletId)); + + _nameStream = + (db.select(db.sparkNames) + ..where((e) => e.name.equals(name.name))).watchSingleOrNull(); + } + + @override + Widget build(BuildContext context) { + final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); + + final (message, color, remaining) = _getExpiry( + currentHeight, + Theme.of(context).extension()!, + ); + + return ConditionalParent( + condition: !Util.isDesktop, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + // Theme.of(context).extension()!.background, + leading: const AppBarBackButton(), + title: Text( + "Spark name details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), + ), + ); + }, + ), + ), + ), + ), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Spark name details", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 10, + ), + child: RoundedContainer( + padding: EdgeInsets.zero, + color: Colors.transparent, + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: child, + ), + ), + ], + ), + ); + }, + child: StreamBuilder( + stream: _nameStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + name = snapshot.data!; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedContainer( + padding: const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SelectableText( + name.name, + style: + Util.isDesktop + ? STextStyles.pageTitleH2(context) + : STextStyles.w500_14(context), + ), + ], + ), + ), + + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ), + Util.isDesktop + ? IconCopyButton(data: name.address) + : SimpleCopyButton(data: name.address), + ], + ), + const SizedBox(height: 4), + SelectableText( + name.address, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + if (_labelStream != null) + StreamBuilder( + stream: _labelStream!, + builder: (context, snapshot) { + label = snapshot.data; + + return (label != null && label!.value.isNotEmpty) + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _Div(), + + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address label", + style: STextStyles.w500_14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + Util.isDesktop + ? IconCopyButton(data: label!.value) + : SimpleCopyButton( + data: label!.value, + ), + ], + ), + const SizedBox(height: 4), + SelectableText( + label!.value, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + ], + ) + : const SizedBox(width: 0, height: 0); + }, + ), + + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Expiry", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ), + const SizedBox(height: 4), + SelectableText( + message, + style: STextStyles.w500_14( + context, + ).copyWith(color: color), + ), + ], + ), + if (remaining < _remainingMagic) + PrimaryButton( + label: "Renew", + buttonHeight: + Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l, + onPressed: _renew, + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Additional info", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ), + const SizedBox(height: 4), + SelectableText( + name.additionalInfo ?? "", + style: STextStyles.w500_14(context), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Container( + width: double.infinity, + height: 1.0, + color: Theme.of(context).extension()!.textFieldDefaultBG, + ); + } else { + return const SizedBox(height: 12); + } + } +} diff --git a/lib/pages/special/firo_rescan_recovery_error_dialog.dart b/lib/pages/special/firo_rescan_recovery_error_dialog.dart index fa59d841a..1b8675db6 100644 --- a/lib/pages/special/firo_rescan_recovery_error_dialog.dart +++ b/lib/pages/special/firo_rescan_recovery_error_dialog.dart @@ -13,6 +13,7 @@ import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../widgets/background.dart'; @@ -28,17 +29,10 @@ import '../pinpad_views/lock_screen_view.dart'; import '../settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import '../settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart'; -enum FiroRescanRecoveryErrorViewOption { - retry, - showMnemonic, - deleteWallet; -} +enum FiroRescanRecoveryErrorViewOption { retry, showMnemonic, deleteWallet } class FiroRescanRecoveryErrorView extends ConsumerStatefulWidget { - const FiroRescanRecoveryErrorView({ - super.key, - required this.walletId, - }); + const FiroRescanRecoveryErrorView({super.key, required this.walletId}); static const String routeName = "/firoRescanRecoveryErrorView"; @@ -71,20 +65,21 @@ class _FiroRescanRecoveryErrorViewState final result = await showDialog( context: context, barrierDismissible: false, - builder: (context) => Navigator( - initialRoute: DesktopDeleteWalletDialog.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - RouteGenerator.generateRoute( - RouteSettings( - name: DesktopDeleteWalletDialog.routeName, - arguments: widget.walletId, - ), - ), - ]; - }, - ), + builder: + (context) => Navigator( + initialRoute: DesktopDeleteWalletDialog.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: DesktopDeleteWalletDialog.routeName, + arguments: widget.walletId, + ), + ), + ]; + }, + ), ); if (result == true) { @@ -119,82 +114,97 @@ class _FiroRescanRecoveryErrorViewState child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( - semanticsLabel: "Delete wallet button. " + semanticsLabel: + "Delete wallet button. " "Start process of deleting current wallet.", key: const Key("walletViewRadioButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.trash, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: () async { - final walletName = - ref.read(pWalletName(widget.walletId)); + final walletName = ref.read( + pWalletName(widget.walletId), + ); await showDialog( barrierDismissible: true, context: context, - builder: (_) => StackDialog( - title: "Do you want to delete $walletName?", - leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - onPressed: () { - Navigator.pop(context); - }, - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) + builder: + (_) => StackDialog( + title: "Do you want to delete $walletName?", + leftButton: TextButton( + style: Theme.of(context) .extension()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.pop(context); - Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator.useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: - widget.walletId, - showBackButton: true, - routeOnSuccess: - DeleteWalletWarningView.routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to delete wallet", - biometricsAuthenticationTitle: - "Delete wallet", - ), - settings: const RouteSettings( - name: "/deleteWalletLockscreen", + .getSecondaryEnabledButtonStyle( + context, + ), + onPressed: () { + Navigator.pop(context); + }, + child: Text( + "Cancel", + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), - ); - }, - child: Text( - "Delete", - style: STextStyles.button(context), + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle( + context, + ), + onPressed: () { + Navigator.pop(context); + Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: + (_) => LockscreenView( + routeOnSuccessArguments: + widget.walletId, + showBackButton: true, + routeOnSuccess: + DeleteWalletWarningView + .routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to delete wallet", + biometricsAuthenticationTitle: + "Delete wallet", + ), + settings: const RouteSettings( + name: "/deleteWalletLockscreen", + ), + ), + ); + }, + child: Text( + "Delete", + style: STextStyles.button(context), + ), + ), ), - ), - ), ); }, ), @@ -202,9 +212,11 @@ class _FiroRescanRecoveryErrorViewState ), ], ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), ), ), ); @@ -217,25 +229,23 @@ class _FiroRescanRecoveryErrorViewState "Failed to rescan Firo wallet", style: STextStyles.pageTitleH2(context), ), - Util.isDesktop - ? const SizedBox( - height: 60, - ) - : const Spacer(), + Util.isDesktop ? const SizedBox(height: 60) : const Spacer(), BranchedParent( condition: Util.isDesktop, - conditionBranchBuilder: (children) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: children, - ), - otherBranchBuilder: (children) => Row( - children: [ - Expanded(child: children[0]), - children[1], - Expanded(child: children[2]), - ], - ), + conditionBranchBuilder: + (children) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ), + otherBranchBuilder: + (children) => Row( + children: [ + Expanded(child: children[0]), + children[1], + Expanded(child: children[2]), + ], + ), children: [ SecondaryButton( label: "Show mnemonic", @@ -245,24 +255,26 @@ class _FiroRescanRecoveryErrorViewState await showDialog( context: context, barrierDismissible: false, - builder: (context) => Navigator( - initialRoute: UnlockWalletKeysDesktop.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - RouteGenerator.generateRoute( - RouteSettings( - name: UnlockWalletKeysDesktop.routeName, - arguments: widget.walletId, - ), - ), - ]; - }, - ), + builder: + (context) => Navigator( + initialRoute: UnlockWalletKeysDesktop.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: UnlockWalletKeysDesktop.routeName, + arguments: widget.walletId, + ), + ), + ]; + }, + ), ); } else { - final wallet = - ref.read(pWallets).getWallet(widget.walletId); + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId); // TODO: [prio=low] take wallets that don't have a mnemonic into account if (wallet is MnemonicInterface) { final mnemonic = await wallet.getMnemonicAsWords(); @@ -272,6 +284,8 @@ class _FiroRescanRecoveryErrorViewState keyData = await wallet.getXPrivs(); } else if (wallet is LibMoneroWallet) { keyData = await wallet.getKeys(); + } else if (wallet is LibSalviumWallet) { + keyData = await wallet.getKeys(); } if (context.mounted) { @@ -280,20 +294,22 @@ class _FiroRescanRecoveryErrorViewState RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: ( - walletId: widget.walletId, - mnemonic: mnemonic, - keyData: keyData, - ), - showBackButton: true, - routeOnSuccess: WalletBackupView.routeName, - biometricsCancelButtonString: "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery phrase", - biometricsAuthenticationTitle: - "View recovery phrase", - ), + builder: + (_) => LockscreenView( + routeOnSuccessArguments: ( + walletId: widget.walletId, + mnemonic: mnemonic, + keyData: keyData, + ), + showBackButton: true, + routeOnSuccess: + WalletBackupView.routeName, + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: + "View recovery phrase", + ), settings: const RouteSettings( name: "/viewRecoverPhraseLockscreen", ), @@ -304,17 +320,12 @@ class _FiroRescanRecoveryErrorViewState } }, ), - const SizedBox( - width: 16, - height: 16, - ), + const SizedBox(width: 16, height: 16), PrimaryButton( label: "Retry", buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: () { - Navigator.of(context).pop( - true, - ); + Navigator.of(context).pop(true); }, ), ], diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index eef37ced9..10e84751b 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -13,8 +13,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import '../add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; -import 'sub_widgets/my_tokens_list.dart'; + import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; @@ -27,12 +26,11 @@ import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/icon_widgets/x_icon.dart'; import '../../widgets/stack_text_field.dart'; import '../../widgets/textfield_icon_button.dart'; +import '../add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; +import 'sub_widgets/my_tokens_list.dart'; class MyTokensView extends ConsumerStatefulWidget { - const MyTokensView({ - super.key, - required this.walletId, - }); + const MyTokensView({super.key, required this.walletId}); static const String routeName = "/myTokens"; final String walletId; @@ -68,78 +66,80 @@ class _MyTokensViewState extends ConsumerState { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "${ref.watch( - pWalletName(widget.walletId), - )} Tokens", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 20, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "${ref.watch(pWalletName(widget.walletId))} Tokens", + style: STextStyles.navBarTitle(context), ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addTokenAppBarIconButtonKey"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension()!.background, - icon: SvgPicture.asset( - Assets.svg.circlePlusFilled, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - width: 20, - height: 20, + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, ), - onPressed: () async { - final result = await Navigator.of(context).pushNamed( - EditWalletTokensView.routeName, - arguments: widget.walletId, - ); + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addTokenAppBarIconButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, + width: 20, + height: 20, + ), + onPressed: () async { + final result = await Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: widget.walletId, + ); - if (mounted && result == 42) { - setState(() {}); - } - }, + if (mounted && result == 42) { + setState(() {}); + } + }, + ), + ), ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), + child: child, ), ), - ], - ), - body: Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, ), - child: child, ), - ), - ), child: Column( children: [ Padding( @@ -148,14 +148,10 @@ class _MyTokensViewState extends ConsumerState { children: [ ConditionalParent( condition: isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: ConditionalParent( condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -170,15 +166,18 @@ class _MyTokensViewState extends ConsumerState { _searchString = value; }); }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), decoration: standardInputDecoration( "Search...", searchFieldFocusNode, @@ -196,26 +195,27 @@ class _MyTokensViewState extends ConsumerState { 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 = ""; - }); - }, - ), - ], + 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, + ) + : null, ), ), ), @@ -224,9 +224,7 @@ class _MyTokensViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: MyTokensList( walletId: widget.walletId, 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 4f1eec6d1..745f2a0fd 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 @@ -56,10 +56,7 @@ class _MyTokenSelectItemState extends ConsumerState { late final CachedEthTokenBalance cachedBalance; - Future _loadTokenWallet( - BuildContext context, - WidgetRef ref, - ) async { + Future _loadTokenWallet(BuildContext context, WidgetRef ref) async { try { await ref.read(pCurrentTokenWallet)!.init(); return true; @@ -67,20 +64,21 @@ class _MyTokenSelectItemState extends ConsumerState { await showDialog( barrierDismissible: false, context: context, - builder: (context) => BasicDialog( - title: "Failed to load token data", - desktopHeight: double.infinity, - desktopWidth: 450, - rightButton: PrimaryButton( - label: "OK", - onPressed: () { - Navigator.of(context).pop(); - if (!isDesktop) { - Navigator.of(context).pop(); - } - }, - ), - ), + builder: + (context) => BasicDialog( + title: "Failed to load token data", + desktopHeight: double.infinity, + desktopWidth: 450, + rightButton: PrimaryButton( + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + if (!isDesktop) { + Navigator.of(context).pop(); + } + }, + ), + ), ); return false; } @@ -90,11 +88,14 @@ class _MyTokenSelectItemState extends ConsumerState { final old = ref.read(tokenServiceStateProvider); // exit previous if there is one unawaited(old?.exit()); - ref.read(tokenServiceStateProvider.state).state = Wallet.loadTokenWallet( - ethWallet: - ref.read(pWallets).getWallet(widget.walletId) as EthereumWallet, - contract: widget.token, - ) as EthTokenWallet; + ref.read(tokenServiceStateProvider.state).state = + Wallet.loadTokenWallet( + ethWallet: + ref.read(pWallets).getWallet(widget.walletId) + as EthereumWallet, + contract: widget.token, + ) + as EthTokenWallet; final success = await showLoading( whileFuture: _loadTokenWallet(context, ref), @@ -138,17 +139,29 @@ class _MyTokenSelectItemState extends ConsumerState { @override Widget build(BuildContext context) { + String? priceString; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + priceString = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (s) => + s.getTokenPrice(widget.token.address)?.value.toStringAsFixed(2), + ), + ); + } + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: MaterialButton( key: Key("walletListItemButtonKey_${widget.token.symbol}"), - padding: isDesktop - ? const EdgeInsets.symmetric(horizontal: 28, vertical: 24) - : const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 28, vertical: 24) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 13), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), onPressed: _onPressed, child: Row( @@ -157,9 +170,7 @@ class _MyTokenSelectItemState extends ConsumerState { contractAddress: widget.token.address, size: isDesktop ? 32 : 28, ), - SizedBox( - width: isDesktop ? 12 : 10, - ), + SizedBox(width: isDesktop ? 12 : 10), Expanded( child: Consumer( builder: (_, ref, __) { @@ -170,14 +181,17 @@ class _MyTokenSelectItemState extends ConsumerState { children: [ Text( widget.token.name, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.titleBold12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ) + : STextStyles.titleBold12(context), ), const Spacer(), Text( @@ -190,62 +204,52 @@ class _MyTokenSelectItemState extends ConsumerState { .format( ref .watch( - pTokenBalance( - ( - walletId: widget.walletId, - contractAddress: - widget.token.address - ), - ), + pTokenBalance(( + walletId: widget.walletId, + contractAddress: widget.token.address, + )), ) .total, ethContract: widget.token, ), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ) + : STextStyles.itemSubtitle(context), ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Row( children: [ Text( widget.token.symbol, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle(context), ), const Spacer(), - Text( - "${ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value - .getTokenPrice(widget.token.address) - .item1 - .toStringAsFixed(2), - ), - )} " - "${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), - ), + if (priceString != null) + Text( + "$priceString " + "${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle(context), + ), ], ), ], diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index f5b051ef7..2c09077cb 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -11,6 +11,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -33,6 +34,7 @@ import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../../../wallets/isar/providers/eth/token_balance_provider.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../widgets/coin_ticker_tag.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/rounded_container.dart'; import '../../buy_view/buy_in_wallet_view.dart'; @@ -53,12 +55,22 @@ class TokenSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final token = - ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract)); + final token = ref.watch( + pCurrentTokenWallet.select((value) => value!.tokenContract), + ); final balance = ref.watch( pTokenBalance((walletId: walletId, contractAddress: token.address)), ); + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getTokenPrice(token.address)?.value, + ), + ); + } + return Stack( children: [ RoundedContainer( @@ -71,30 +83,26 @@ class TokenSummary extends ConsumerWidget { children: [ SvgPicture.asset( Assets.svg.walletDesktop, - color: Theme.of(context) - .extension()! - .tokenSummaryTextSecondary, + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextSecondary, width: 12, height: 12, ), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), Text( - ref.watch( - pWalletName(walletId), - ), + ref.watch(pWalletName(walletId)), style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .tokenSummaryTextSecondary, + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextSecondary, ), ), ], ), - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -105,58 +113,35 @@ class TokenSummary extends ConsumerWidget { Ethereum(CryptoCurrencyNetwork.main), ), ) - .format( - balance.total, - ethContract: token, - ), + .format(balance.total, ethContract: token), style: STextStyles.pageTitleH1(context).copyWith( - color: Theme.of(context) - .extension()! - .tokenSummaryTextPrimary, + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextPrimary, ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), CoinTickerTag( - walletId: walletId, + ticker: ref.watch( + pWalletCoin(walletId).select((s) => s.ticker), + ), ), ], ), - const SizedBox( - height: 6, - ), - Text( - "${(balance.total.decimal * ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getTokenPrice(token.address).item1, - ), - )).toAmount( - fractionDigits: 2, - ).fiatString( - locale: ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), - ), - )} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, + if (price != null) const SizedBox(height: 6), + if (price != null) + Text( + "${(balance.total.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.subtitle500(context).copyWith( + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextPrimary, ), - )}", - style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .tokenSummaryTextPrimary, ), - ), - const SizedBox( - height: 20, - ), - TokenWalletOptions( - walletId: walletId, - tokenContract: token, - ), + const SizedBox(height: 20), + TokenWalletOptions(walletId: walletId, tokenContract: token), ], ), ), @@ -195,11 +180,7 @@ class TokenWalletOptions extends ConsumerWidget { unawaited( Navigator.of(context).pushNamed( WalletInitiatedExchangeView.routeName, - arguments: Tuple3( - walletId, - ethereum, - tokenContract, - ), + arguments: Tuple3(walletId, ethereum, tokenContract), ), ); } @@ -208,10 +189,7 @@ class TokenWalletOptions extends ConsumerWidget { unawaited( Navigator.of(context).pushNamed( BuyInWalletView.routeName, - arguments: Tuple2( - ethereum, - tokenContract, - ), + arguments: Tuple2(ethereum, tokenContract), ), ); } @@ -228,50 +206,35 @@ class TokenWalletOptions extends ConsumerWidget { onPressed: () { Navigator.of(context).pushNamed( ReceiveView.routeName, - arguments: Tuple2( - walletId, - tokenContract, - ), + arguments: Tuple2(walletId, tokenContract), ); }, subLabel: "Receive", iconAssetPathSVG: Assets.svg.arrowDownLeft, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), TokenOptionsButton( onPressed: () { Navigator.of(context).pushNamed( TokenSendView.routeName, - arguments: Tuple3( - walletId, - ethereum, - tokenContract, - ), + arguments: Tuple3(walletId, ethereum, tokenContract), ); }, subLabel: "Send", iconAssetPathSVG: Assets.svg.arrowUpRight, ), if (AppConfig.hasFeature(AppFeature.swap) && showExchange) - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), if (AppConfig.hasFeature(AppFeature.swap) && showExchange) TokenOptionsButton( onPressed: () => _onExchangePressed(context), subLabel: "Swap", iconAssetPathSVG: ref.watch( - themeProvider.select( - (value) => value.assets.exchange, - ), + themeProvider.select((value) => value.assets.exchange), ), ), if (AppConfig.hasFeature(AppFeature.buy) && showExchange) - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), if (AppConfig.hasFeature(AppFeature.buy) && showExchange) TokenOptionsButton( onPressed: () => _onBuyPressed(context), @@ -320,77 +283,50 @@ class TokenOptionsButton extends StatelessWidget { padding: const EdgeInsets.all(10), child: ConditionalParent( condition: iconSize < 24, - builder: (child) => RoundedContainer( - padding: const EdgeInsets.all(6), - color: Theme.of(context) - .extension()! - .tokenSummaryIcon - .withOpacity(0.4), - radiusMultiplier: 10, - child: Center( - child: child, - ), - ), - child: iconAssetPathSVG.startsWith("assets/") - ? SvgPicture.asset( - iconAssetPathSVG, - color: Theme.of(context) - .extension()! - .tokenSummaryIcon, - width: iconSize, - height: iconSize, - ) - : SvgPicture.file( - File(iconAssetPathSVG), - color: Theme.of(context) - .extension()! - .tokenSummaryIcon, - width: iconSize, - height: iconSize, - ), + builder: + (child) => RoundedContainer( + padding: const EdgeInsets.all(6), + color: Theme.of(context) + .extension()! + .tokenSummaryIcon + .withOpacity(0.4), + radiusMultiplier: 10, + child: Center(child: child), + ), + child: + iconAssetPathSVG.startsWith("assets/") + ? SvgPicture.asset( + iconAssetPathSVG, + color: + Theme.of( + context, + ).extension()!.tokenSummaryIcon, + width: iconSize, + height: iconSize, + ) + : SvgPicture.file( + File(iconAssetPathSVG), + color: + Theme.of( + context, + ).extension()!.tokenSummaryIcon, + width: iconSize, + height: iconSize, + ), ), ), ), - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), Text( subLabel, style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .tokenSummaryTextPrimary, + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextPrimary, ), ), ], ); } } - -class CoinTickerTag extends ConsumerWidget { - const CoinTickerTag({ - super.key, - required this.walletId, - }); - - final String walletId; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return RoundedContainer( - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), - radiusMultiplier: 0.25, - color: Theme.of(context).extension()!.ethTagBG, - child: Text( - ref - .watch( - pWalletCoin(walletId), - ) - .ticker, - style: STextStyles.w600_12(context).copyWith( - color: Theme.of(context).extension()!.ethTagText, - ), - ), - ); - } -} diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index b6acc49c5..6063bd49f 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -53,9 +53,10 @@ class _TokenViewState extends ConsumerState { @override void initState() { - initialSyncStatus = ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced; + initialSyncStatus = + ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced; super.initState(); } @@ -108,14 +109,13 @@ class _TokenViewState extends ConsumerState { ), size: 24, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Flexible( child: Text( ref.watch( - pCurrentTokenWallet - .select((value) => value!.tokenContract.name), + pCurrentTokenWallet.select( + (value) => value!.tokenContract.name, + ), ), style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, @@ -135,9 +135,10 @@ class _TokenViewState extends ConsumerState { child: AppBarIconButton( icon: SvgPicture.asset( Assets.svg.verticalEllipsis, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: () { // todo: context menu @@ -146,7 +147,8 @@ class _TokenViewState extends ConsumerState { arguments: Tuple2( ref.watch( pCurrentTokenWallet.select( - (value) => value!.tokenContract.address), + (value) => value!.tokenContract.address, + ), ), widget.walletId, ), @@ -157,93 +159,90 @@ class _TokenViewState extends ConsumerState { ), ], ), - body: Container( - color: Theme.of(context).extension()!.background, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TokenSummary( - walletId: widget.walletId, - initialSyncStatus: initialSyncStatus, + body: SafeArea( + child: Container( + color: Theme.of(context).extension()!.background, + child: Column( + children: [ + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TokenSummary( + walletId: widget.walletId, + initialSyncStatus: initialSyncStatus, + ), ), - ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transactions", - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: STextStyles.itemSubtitle(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), ), - ), - CustomTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsV2View.routeName, - arguments: ( - walletId: widget.walletId, - contractAddress: ref.watch( - pCurrentTokenWallet.select( - (value) => value!.tokenContract.address, + CustomTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsV2View.routeName, + arguments: ( + walletId: widget.walletId, + contractAddress: ref.watch( + pCurrentTokenWallet.select( + (value) => value!.tokenContract.address, + ), ), ), - ), - ); - }, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ClipRRect( - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - bottom: Radius.circular( - // TokenView.navBarHeight / 2.0, - Constants.size.circularBorderRadius, + ); + }, ), - ), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( + ], + ), + ), + const SizedBox(height: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottom: Radius.circular( + // TokenView.navBarHeight / 2.0, Constants.size.circularBorderRadius, ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: TokenTransactionsList( - walletId: widget.walletId, - ), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TokenTransactionsList( + walletId: widget.walletId, + ), + ), + ], + ), ), ), ), ), - ), - ], + ], + ), ), ), ), 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 c2fc747ad..120b99b25 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 @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../models/balance.dart'; import '../../../providers/wallet/public_private_balance_state_provider.dart'; import '../../../providers/wallet/wallet_balance_toggle_state_provider.dart'; @@ -22,20 +23,10 @@ import '../../../utilities/text_styles.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; -enum _BalanceType { - available, - full, - lelantusAvailable, - lelantusFull, - sparkAvailable, - sparkFull; -} +enum _BalanceType { available, full, privateAvailable, privateFull } class WalletBalanceToggleSheet extends ConsumerWidget { - const WalletBalanceToggleSheet({ - super.key, - required this.walletId, - }); + const WalletBalanceToggleSheet({super.key, required this.walletId}); final String walletId; @@ -44,7 +35,10 @@ class WalletBalanceToggleSheet extends ConsumerWidget { final maxHeight = MediaQuery.of(context).size.height * 0.90; final coin = ref.watch(pWalletCoin(walletId)); - final isFiro = coin is Firo; + final isMweb = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final hasPrivate = isMweb || coin is Firo; final balance = ref.watch(pWalletBalance(walletId)); @@ -54,42 +48,26 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ? _BalanceType.available : _BalanceType.full; - Balance? balanceSecondary; - Balance? balanceTertiary; - if (isFiro) { - balanceSecondary = ref.watch(pWalletBalanceSecondary(walletId)); - balanceTertiary = ref.watch(pWalletBalanceTertiary(walletId)); - - switch (ref.watch(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - _bal = _bal == _BalanceType.available - ? _BalanceType.sparkAvailable - : _BalanceType.sparkFull; - break; - - case FiroType.lelantus: - _bal = _bal == _BalanceType.available - ? _BalanceType.lelantusAvailable - : _BalanceType.lelantusFull; - break; + Balance? balancePrivate; + if (hasPrivate) { + balancePrivate = + isMweb + ? ref.watch(pWalletBalanceSecondary(walletId)) + : ref.watch(pWalletBalanceTertiary(walletId)); - case FiroType.public: - // already set above - break; - } - - // hack to not show lelantus balance in ui if zero - if (balanceSecondary?.spendable.raw == BigInt.zero) { - balanceSecondary = null; + if (ref.watch(publicPrivateBalanceStateProvider.state).state == + BalanceType.private) { + _bal = + _bal == _BalanceType.available + ? _BalanceType.privateAvailable + : _BalanceType.privateFull; } } return Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: LimitedBox( maxHeight: maxHeight, @@ -107,9 +85,10 @@ class WalletBalanceToggleSheet extends ConsumerWidget { Center( child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -118,9 +97,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { height: 4, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( @@ -129,161 +106,97 @@ class WalletBalanceToggleSheet extends ConsumerWidget { textAlign: TextAlign.left, ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), BalanceSelector( - title: "Available${isFiro ? " public" : ""} balance", + title: "Available${hasPrivate ? " public" : ""} balance", coin: coin, balance: balance.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, value: _BalanceType.available, groupValue: _bal, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), BalanceSelector( - title: "Full${isFiro ? " public" : ""} balance", + title: "Full${hasPrivate ? " public" : ""} balance", coin: coin, balance: balance.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, value: _BalanceType.full, groupValue: _bal, ), - if (balanceSecondary != null) - const SizedBox( - height: 12, - ), - if (balanceSecondary != null) - BalanceSelector( - title: "Available Lelantus balance", - coin: coin, - balance: balanceSecondary.spendable, - onPressed: () { - ref.read(walletBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.available; - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.lelantus; - Navigator.of(context).pop(); - }, - onChanged: (_) { - ref.read(walletBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.available; - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.lelantus; - Navigator.of(context).pop(); - }, - value: _BalanceType.lelantusAvailable, - groupValue: _bal, - ), - if (balanceSecondary != null) - const SizedBox( - height: 12, - ), - if (balanceSecondary != null) + if (balancePrivate != null) const SizedBox(height: 12), + if (balancePrivate != null) BalanceSelector( - title: "Full Lelantus balance", + title: "Available Private balance", coin: coin, - balance: balanceSecondary.total, - onPressed: () { - ref.read(walletBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.full; - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.lelantus; - Navigator.of(context).pop(); - }, - onChanged: (_) { - ref.read(walletBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.full; - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.lelantus; - Navigator.of(context).pop(); - }, - value: _BalanceType.lelantusFull, - groupValue: _bal, - ), - if (balanceTertiary != null) - const SizedBox( - height: 12, - ), - if (balanceTertiary != null) - BalanceSelector( - title: "Available Spark balance", - coin: coin, - balance: balanceTertiary.spendable, + balance: balancePrivate.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, - value: _BalanceType.sparkAvailable, + value: _BalanceType.privateAvailable, groupValue: _bal, ), - if (balanceTertiary != null) - const SizedBox( - height: 12, - ), - if (balanceTertiary != null) + if (balancePrivate != null) const SizedBox(height: 12), + if (balancePrivate != null) BalanceSelector( - title: "Full Spark balance", + title: "Full Private balance", coin: coin, - balance: balanceTertiary.total, + balance: balancePrivate.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, - value: _BalanceType.sparkFull, + value: _BalanceType.privateFull, groupValue: _bal, ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), ], ), ), @@ -331,33 +244,28 @@ class BalanceSelector extends ConsumerWidget { width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of( + context, + ).extension()!.radioButtonIconEnabled, value: value, groupValue: groupValue, onChanged: onChanged, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: STextStyles.titleBold12(context), - ), - const SizedBox( - height: 2, - ), + Text(title, style: STextStyles.titleBold12(context)), + const SizedBox(height: 2), Text( ref.watch(pAmountFormatter(coin)).format(balance), style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 08d9152b8..68e0123bc 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -11,6 +11,7 @@ import 'dart:io'; import 'dart:typed_data'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -52,9 +53,7 @@ class WalletSummaryInfo extends ConsumerWidget { useSafeArea: true, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => WalletBalanceToggleSheet(walletId: walletId), ); @@ -64,9 +63,6 @@ class WalletSummaryInfo extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); - final externalCalls = ref.watch( - prefsChangeNotifierProvider.select((value) => value.externalCalls), - ); final coin = ref.watch(pWalletCoin(walletId)); final balance = ref.watch(pWalletBalance(walletId)); @@ -74,14 +70,23 @@ class WalletSummaryInfo extends ConsumerWidget { localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); - - final priceTuple = ref.watch( - priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin)), + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), ); - final _showAvailable = ref.watch(walletBalanceToggleStateProvider) == + ({double change24h, Decimal value})? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ); + } + + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider) == WalletBalanceToggleState.available; final Amount balanceToShow; @@ -89,23 +94,21 @@ class WalletSummaryInfo extends ConsumerWidget { final bool toggleBalance; - if (coin is Firo) { + if (coin is Firo || ref.watch(pWalletInfo(walletId)).isMwebEnabled) { toggleBalance = false; final type = ref.watch(publicPrivateBalanceStateProvider.state).state; title = "${_showAvailable ? "Available" : "Full"} ${type.name.capitalize()} balance"; switch (type) { - case FiroType.spark: - final balance = ref.watch(pWalletBalanceTertiary(walletId)); - balanceToShow = _showAvailable ? balance.spendable : balance.total; - break; - - case FiroType.lelantus: - final balance = ref.watch(pWalletBalanceSecondary(walletId)); + case BalanceType.private: + final balance = + coin is Firo + ? ref.watch(pWalletBalanceTertiary(walletId)) + : ref.watch(pWalletBalanceSecondary(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; - case FiroType.public: + case BalanceType.public: final balance = ref.watch(pWalletBalance(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; @@ -119,23 +122,23 @@ class WalletSummaryInfo extends ConsumerWidget { List? imageBytes; if (coin is Banano) { - imageBytes = (ref.watch(pWallets).getWallet(walletId) as BananoWallet) - .getMonkeyImageBytes(); + imageBytes = + (ref.watch(pWallets).getWallet(walletId) as BananoWallet) + .getMonkeyImageBytes(); } return ConditionalParent( condition: imageBytes != null, - builder: (child) => Stack( - children: [ - Positioned.fill( - left: 150.0, - child: SvgPicture.memory( - Uint8List.fromList(imageBytes!), - ), + builder: + (child) => Stack( + children: [ + Positioned.fill( + left: 150.0, + child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), + ), + child, + ], ), - child, - ], - ), child: Row( children: [ Expanded( @@ -164,20 +167,20 @@ class WalletSummaryInfo extends ConsumerWidget { Text( title, style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, ), ), if (!toggleBalance) ...[ - const SizedBox( - width: 4, - ), + const SizedBox(width: 4), SvgPicture.asset( Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, width: 8, height: 4, ), @@ -207,23 +210,21 @@ class WalletSummaryInfo extends ConsumerWidget { ref.watch(pAmountFormatter(coin)).format(balanceToShow), style: STextStyles.pageTitleH1(context).copyWith( fontSize: 24, - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, ), ), ), - if (externalCalls) + if (price != null) Text( - "${(priceTuple.item1 * balanceToShow.decimal).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "${(price.value * balanceToShow.decimal).toAmount(fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, ), ), ], @@ -232,9 +233,7 @@ class WalletSummaryInfo extends ConsumerWidget { Column( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 5afd82597..9fcbfc3f4 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -51,17 +52,11 @@ import '../sub_widgets/tx_icon.dart'; import 'transaction_details_view.dart'; import 'transaction_search_filter_view.dart'; -typedef _GroupedTransactions = ({ - String label, - DateTime startDate, - List transactions -}); +typedef _GroupedTransactions = + ({String label, DateTime startDate, List transactions}); class AllTransactionsView extends ConsumerStatefulWidget { - const AllTransactionsView({ - super.key, - required this.walletId, - }); + const AllTransactionsView({super.key, required this.walletId}); static const String routeName = "/allTransactions"; @@ -106,13 +101,14 @@ class _TransactionDetailsViewState extends ConsumerState { // debugPrint("FILTER: $filter"); final contacts = ref.read(addressBookServiceProvider).contacts; - final notes = ref - .read(mainDBProvider) - .isar - .transactionNotes - .where() - .walletIdEqualTo(walletId) - .findAllSync(); + final notes = + ref + .read(mainDBProvider) + .isar + .transactionNotes + .where() + .walletIdEqualTo(walletId) + .findAllSync(); return transactions.where((tx) { if (!filter.sent && !filter.received) { @@ -162,15 +158,16 @@ class _TransactionDetailsViewState extends ConsumerState { bool contains = false; // check if address book name contains - contains |= contacts - .where( - (e) => - e.addresses - .where((a) => a.address == tx.address.value?.value) - .isNotEmpty && - e.name.toLowerCase().contains(keyword), - ) - .isNotEmpty; + contains |= + contacts + .where( + (e) => + e.addresses + .where((a) => a.address == tx.address.value?.value) + .isNotEmpty && + e.name.toLowerCase().contains(keyword), + ) + .isNotEmpty; // check if address contains contains |= @@ -195,8 +192,9 @@ class _TransactionDetailsViewState extends ConsumerState { contains |= tx.type.name.toLowerCase().contains(keyword); // check if date contains - contains |= - Format.extractDateFrom(tx.timestamp).toLowerCase().contains(keyword); + contains |= Format.extractDateFrom( + tx.timestamp, + ).toLowerCase().contains(keyword); return contains; } @@ -210,13 +208,14 @@ class _TransactionDetailsViewState extends ConsumerState { } text = text.toLowerCase(); final contacts = ref.read(addressBookServiceProvider).contacts; - final notes = ref - .read(mainDBProvider) - .isar - .transactionNotes - .where() - .walletIdEqualTo(walletId) - .findAllSync(); + final notes = + ref + .read(mainDBProvider) + .isar + .transactionNotes + .where() + .walletIdEqualTo(walletId) + .findAllSync(); return transactions .where((tx) => _isKeywordMatch(tx, text, contacts, notes)) @@ -232,8 +231,11 @@ class _TransactionDetailsViewState extends ConsumerState { final date = DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000); final monthYear = "${Constants.monthMap[date.month]} ${date.year}"; if (map[monthYear] == null) { - map[monthYear] = - (label: monthYear, startDate: date, transactions: [tx]); + map[monthYear] = ( + label: monthYear, + startDate: date, + transactions: [tx], + ); } else { map[monthYear]!.transactions.add(tx); } @@ -250,97 +252,99 @@ class _TransactionDetailsViewState extends ConsumerState { return MasterScaffold( background: Theme.of(context).extension()!.background, isDesktop: isDesktop, - appBar: isDesktop - ? DesktopAppBar( - isCompactHeight: true, - background: Theme.of(context).extension()!.popupBG, - leading: Row( - children: [ - const SizedBox( - width: 32, - ), - 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, - ), - const SizedBox( - width: 12, - ), - Text( - "Transactions", - style: STextStyles.desktopH3(context), - ), - ], - ), - ) - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Transactions", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 20, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("transactionSearchFilterViewButton"), - size: 36, + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + const SizedBox(width: 32), + AppBarIconButton( + size: 32, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], - color: Theme.of(context) - .extension()! - .background, icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), - onPressed: () { - Navigator.of(context).pushNamed( - TransactionSearchFilterView.routeName, - arguments: - ref.read(pWallets).getWallet(walletId).info.coin, - ); - }, + onPressed: Navigator.of(context).pop, ), - ), + const SizedBox(width: 12), + Text("Transactions", style: STextStyles.desktopH3(context)), + ], ), - ], - ), + ) + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Transactions", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("transactionSearchFilterViewButton"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.filter, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + TransactionSearchFilterView.routeName, + arguments: + ref + .read(pWallets) + .getWallet(walletId) + .info + .coin, + ); + }, + ), + ), + ), + ], + ), body: Padding( padding: EdgeInsets.only( left: isDesktop ? 20 : 12, @@ -355,15 +359,10 @@ class _TransactionDetailsViewState extends ConsumerState { children: [ ConditionalParent( condition: isDesktop, - builder: (child) => SizedBox( - width: 570, - child: child, - ), + builder: (child) => SizedBox(width: 570, child: child), child: ConditionalParent( condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -378,15 +377,18 @@ class _TransactionDetailsViewState extends ConsumerState { _searchString = value; }); }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), decoration: standardInputDecoration( "Search...", searchFieldFocusNode, @@ -404,35 +406,33 @@ class _TransactionDetailsViewState extends ConsumerState { 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 = ""; - }); - }, - ), - ], + 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, + ) + : null, ), ), ), ), ), - if (isDesktop) - const SizedBox( - width: 20, - ), + if (isDesktop) const SizedBox(width: 20), if (isDesktop) SecondaryButton( buttonHeight: ButtonHeight.l, @@ -440,9 +440,10 @@ class _TransactionDetailsViewState extends ConsumerState { label: "Filter", icon: SvgPicture.asset( Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -453,9 +454,7 @@ class _TransactionDetailsViewState extends ConsumerState { showDialog( context: context, builder: (context) { - return TransactionSearchFilterView( - coin: coin, - ); + return TransactionSearchFilterView(coin: coin); }, ); } else { @@ -469,25 +468,14 @@ class _TransactionDetailsViewState extends ConsumerState { ], ), ), - if (isDesktop) - const SizedBox( - height: 8, - ), + if (isDesktop) const SizedBox(height: 8), if (isDesktop && ref.watch(transactionFilterProvider.state).state != null) const Padding( - padding: EdgeInsets.symmetric( - vertical: 8, - ), - child: Row( - children: [ - TransactionFilterOptionBar(), - ], - ), + padding: EdgeInsets.symmetric(vertical: 8), + child: Row(children: [TransactionFilterOptionBar()]), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: Consumer( builder: (_, ref, __) { @@ -495,38 +483,40 @@ class _TransactionDetailsViewState extends ConsumerState { ref.watch(transactionFilterProvider.state).state; return FutureBuilder( - future: ref - .watch(mainDBProvider) - .isar - .transactions - .buildQuery( - whereClauses: [ - IndexWhereClause.equalTo( - indexName: 'walletId', - value: [widget.walletId], - ), - ], - // TODO: [prio=med] add filters to wallet or cryptocurrency class - // eth tokens should all be on v2 txn now so this should not be needed here - // filter: widget.contractAddress != null - // ? FilterGroup.and([ - // FilterCondition.equalTo( - // property: r"contractAddress", - // value: widget.contractAddress!, - // ), - // const FilterCondition.equalTo( - // property: r"subType", - // value: TransactionSubType.ethToken, - // ), - // ]) - // : null, - sortBy: [ - const SortProperty( - property: "timestamp", - sort: Sort.desc, - ), - ], - ).findAll(), + future: + ref + .watch(mainDBProvider) + .isar + .transactions + .buildQuery( + whereClauses: [ + IndexWhereClause.equalTo( + indexName: 'walletId', + value: [widget.walletId], + ), + ], + // TODO: [prio=med] add filters to wallet or cryptocurrency class + // eth tokens should all be on v2 txn now so this should not be needed here + // filter: widget.contractAddress != null + // ? FilterGroup.and([ + // FilterCondition.equalTo( + // property: r"contractAddress", + // value: widget.contractAddress!, + // ), + // const FilterCondition.equalTo( + // property: r"subType", + // value: TransactionSubType.ethToken, + // ), + // ]) + // : null, + sortBy: [ + const SortProperty( + property: "timestamp", + sort: Sort.desc, + ), + ], + ) + .findAll(), builder: (_, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -555,43 +545,39 @@ class _TransactionDetailsViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (index != 0) - const SizedBox( - height: 12, - ), + if (index != 0) const SizedBox(height: 12), Text( month.label, style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (isDesktop) RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: ListView.separated( shrinkWrap: true, primary: false, - separatorBuilder: (context, _) => - Container( - height: 1, - color: Theme.of(context) - .extension()! - .background, - ), + separatorBuilder: + (context, _) => Container( + height: 1, + color: + Theme.of(context) + .extension()! + .background, + ), itemCount: month.transactions.length, - itemBuilder: (context, index) => - Padding( - padding: const EdgeInsets.all(4), - child: DesktopTransactionCardRow( - key: Key( - "transactionCard_key_${month.transactions[index].txid}", + itemBuilder: + (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: DesktopTransactionCardRow( + key: Key( + "transactionCard_key_${month.transactions[index].txid}", + ), + transaction: + month.transactions[index], + walletId: walletId, + ), ), - transaction: - month.transactions[index], - walletId: walletId, - ), - ), ), ), if (!isDesktop) @@ -659,10 +645,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - sent: false, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(sent: false); setState(() {}); } }, @@ -678,10 +664,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - received: false, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(received: false); setState(() {}); } }, @@ -698,10 +684,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - to: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(to: null); setState(() {}); } }, @@ -717,10 +703,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - from: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(from: null); setState(() {}); } }, @@ -737,10 +723,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - amount: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(amount: null); setState(() {}); } }, @@ -756,10 +742,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - keyword: "", - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(keyword: ""); setState(() {}); } }, @@ -780,9 +766,7 @@ class _TransactionFilterOptionBarState scrollDirection: Axis.horizontal, shrinkWrap: true, itemCount: items.length, - separatorBuilder: (_, __) => const SizedBox( - width: 16, - ), + separatorBuilder: (_, __) => const SizedBox(width: 16), itemBuilder: (context, index) => items[index], ), ); @@ -811,9 +795,7 @@ class TransactionFilterOptionBarItem extends StatelessWidget { borderRadius: BorderRadius.circular(1000), ), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - ), + padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -831,9 +813,7 @@ class TransactionFilterOptionBarItem extends StatelessWidget { ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), XIcon( width: 16, height: 16, @@ -915,17 +895,23 @@ class _DesktopTransactionCardRowState localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); final coin = ref.watch(pWalletCoin(walletId)); - final price = ref - .watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin)), - ) - .item1; + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + ?.value; + } late final String prefix; if (Util.isDesktop) { @@ -946,8 +932,9 @@ class _DesktopTransactionCardRowState color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: RawMaterialButton( shape: RoundedRectangleBorder( @@ -970,34 +957,28 @@ class _DesktopTransactionCardRowState if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TransactionDetailsView( - transaction: _transaction, - coin: coin, - walletId: walletId, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionDetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), + ), ); } else { unawaited( Navigator.of(context).pushNamed( TransactionDetailsView.routeName, - arguments: Tuple3( - _transaction, - coin, - walletId, - ), + arguments: Tuple3(_transaction, coin, walletId), ), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: Row( children: [ TxIcon( @@ -1005,21 +986,16 @@ class _DesktopTransactionCardRowState currentHeight: currentHeight, coin: coin, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( flex: 3, child: Text( _transaction.isCancelled ? "Cancelled" - : whatIsIt( - _transaction.type, - coin, - currentHeight, - ), - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + : whatIsIt(_transaction.type, coin, currentHeight), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), @@ -1038,20 +1014,19 @@ class _DesktopTransactionCardRowState final amount = _transaction.realAmount; return Text( "$prefix${ref.watch(pAmountFormatter(coin)).format(amount)}", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ); }, ), ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) + if (price != null) Expanded( flex: 4, child: Builder( @@ -1059,11 +1034,7 @@ class _DesktopTransactionCardRowState final amount = _transaction.realAmount; return Text( - "$prefix${(amount.decimal * price).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "$prefix${(amount.decimal * price!).toAmount(fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.desktopTextExtraExtraSmall(context), ); }, diff --git a/lib/pages/wallet_view/transaction_views/edit_note_view.dart b/lib/pages/wallet_view/transaction_views/edit_note_view.dart index 4d0c584eb..bcb6202ec 100644 --- a/lib/pages/wallet_view/transaction_views/edit_note_view.dart +++ b/lib/pages/wallet_view/transaction_views/edit_note_view.dart @@ -12,7 +12,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/isar/models/transaction_note.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/constants.dart'; @@ -28,11 +27,7 @@ import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; class EditNoteView extends ConsumerStatefulWidget { - const EditNoteView({ - super.key, - required this.txid, - required this.walletId, - }); + const EditNoteView({super.key, required this.txid, required this.walletId}); static const String routeName = "/editNote"; @@ -57,9 +52,7 @@ class _EditNoteViewState extends ConsumerState { _noteController = TextEditingController(); _note = ref.read( - pTransactionNote( - (txid: widget.txid, walletId: widget.walletId), - ), + pTransactionNote((txid: widget.txid, walletId: widget.walletId)), ); _noteController.text = _note?.value ?? ""; super.initState(); @@ -76,63 +69,56 @@ class _EditNoteViewState extends ConsumerState { Widget build(BuildContext context) { return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: child, - ), + builder: (child) => Background(child: child), child: Scaffold( - backgroundColor: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.background, - appBar: isDesktop - ? null - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Edit note", - style: STextStyles.navBarTitle(context), + backgroundColor: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.background, + appBar: + isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit note", + style: STextStyles.navBarTitle(context), + ), ), - ), body: MobileEditNoteScaffold( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (isDesktop) Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 12, - ), + padding: const EdgeInsets.only(left: 32, bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Edit note", - style: STextStyles.desktopH3(context), - ), + Text("Edit note", style: STextStyles.desktopH3(context)), const DesktopDialogCloseButton(), ], ), ), Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -141,14 +127,18 @@ class _EditNoteViewState extends ConsumerState { autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, controller: _noteController, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), focusNode: noteFieldFocusNode, decoration: standardInputDecoration( "Note", @@ -156,33 +146,35 @@ class _EditNoteViewState extends ConsumerState { context, desktopMed: isDesktop, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.only( - left: 16, - top: 11, - bottom: 12, - right: 5, - ) - : null, - 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 = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + 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, + ) + : null, ), ), ), @@ -195,7 +187,9 @@ class _EditNoteViewState extends ConsumerState { child: PrimaryButton( label: "Save", onPressed: () async { - await ref.read(mainDBProvider).putTransactionNote( + await ref + .read(mainDBProvider) + .putTransactionNote( _note?.copyWith(value: _noteController.text) ?? TransactionNote( walletId: widget.walletId, @@ -213,7 +207,9 @@ class _EditNoteViewState extends ConsumerState { if (!isDesktop) TextButton( onPressed: () async { - await ref.read(mainDBProvider).putTransactionNote( + await ref + .read(mainDBProvider) + .putTransactionNote( _note?.copyWith(value: _noteController.text) ?? TransactionNote( walletId: widget.walletId, @@ -228,10 +224,7 @@ class _EditNoteViewState extends ConsumerState { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - child: Text( - "Save", - style: STextStyles.button(context), - ), + child: Text("Save", style: STextStyles.button(context)), ), ], ), @@ -242,10 +235,7 @@ class _EditNoteViewState extends ConsumerState { } class MobileEditNoteScaffold extends StatelessWidget { - const MobileEditNoteScaffold({ - super.key, - required this.child, - }); + const MobileEditNoteScaffold({super.key, required this.child}); final Widget child; @@ -254,24 +244,24 @@ class MobileEditNoteScaffold extends StatelessWidget { if (Util.isDesktop) { return child; } else { - return Padding( - padding: const EdgeInsets.all(12), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 11ebd1b82..7dc810ac8 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -21,7 +22,6 @@ import 'package:url_launcher/url_launcher.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/ethereum/eth_contract.dart'; import '../../../notifications/show_flush_bar.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/address_book_service_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; @@ -99,11 +99,12 @@ class _TransactionDetailsViewState isTokenTx = _transaction.subType == TransactionSubType.ethToken; walletId = widget.walletId; - minConfirms = ref - .read(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms; + minConfirms = + ref + .read(pWallets) + .getWallet(widget.walletId) + .cryptoCurrency + .minConfirms; coin = widget.coin; amount = _transaction.realAmount; fee = _transaction.fee.toAmountAsRaw(fractionDigits: coin.fractionDigits); @@ -114,9 +115,12 @@ class _TransactionDetailsViewState amountPrefix = _transaction.type == TransactionType.outgoing ? "-" : "+"; } - ethContract = isTokenTx - ? ref.read(mainDBProvider).getEthContractSync(_transaction.otherData!) - : null; + ethContract = + isTokenTx + ? ref + .read(mainDBProvider) + .getEthContractSync(_transaction.otherData!) + : null; unit = isTokenTx ? ethContract!.symbol : coin.ticker; @@ -208,10 +212,14 @@ class _TransactionDetailsViewState return address; } try { - final contacts = ref.read(addressBookServiceProvider).contacts.where( - (element) => element.addresses - .where((element) => element.address == address) - .isNotEmpty, + final contacts = ref + .read(addressBookServiceProvider) + .contacts + .where( + (element) => + element.addresses + .where((element) => element.address == address) + .isNotEmpty, ); if (contacts.isNotEmpty) { return contacts.first.name; @@ -219,7 +227,7 @@ class _TransactionDetailsViewState return address; } } catch (e, s) { - Logging.instance.w("$e\n$s", error: e, stackTrace: s,); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); return address; } } @@ -240,8 +248,9 @@ class _TransactionDetailsViewState builder: (_, ref, __) { return Checkbox( value: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.hideBlockExplorerWarning), + prefsChangeNotifierProvider.select( + (value) => value.hideBlockExplorerWarning, + ), ), onChanged: (value) { if (value is bool) { @@ -267,23 +276,21 @@ class _TransactionDetailsViewState child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), onPressed: () { Navigator.of(context).pop(true); }, - child: Text( - "Continue", - style: STextStyles.button(context), - ), + child: Text("Continue", style: STextStyles.button(context)), ), ); } else { @@ -297,10 +304,7 @@ class _TransactionDetailsViewState Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Attention", - style: STextStyles.desktopH2(context), - ), + Text("Attention", style: STextStyles.desktopH2(context)), Row( children: [ Consumer( @@ -344,10 +348,7 @@ class _TransactionDetailsViewState buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(false); + Navigator.of(context, rootNavigator: true).pop(false); }, ), const SizedBox(width: 20), @@ -356,10 +357,7 @@ class _TransactionDetailsViewState buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(true); + Navigator.of(context, rootNavigator: true).pop(true); }, ), ], @@ -378,382 +376,392 @@ class _TransactionDetailsViewState Widget build(BuildContext context) { final currentHeight = ref.watch(pWalletChainHeight(walletId)); + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(_transaction.otherData!)?.value + : value.getPrice(coin)?.value, + ), + ); + } + return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: child, - ), + builder: (child) => Background(child: child), child: Scaffold( - backgroundColor: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.background, - appBar: isDesktop - ? null - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Transaction details", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: isDesktop - ? const EdgeInsets.only(left: 32) - : const EdgeInsets.all(12), - child: Column( - children: [ - if (isDesktop) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction details", - style: STextStyles.desktopH3(context), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: isDesktop - ? const EdgeInsets.only( - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(0), - child: ConditionalParent( - condition: isDesktop, - builder: (child) { - return RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, - padding: const EdgeInsets.all(0), - child: child, - ); + backgroundColor: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.background, + appBar: + isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); }, - child: SingleChildScrollView( - primary: isDesktop ? false : null, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: isDesktop + ), + title: Text( + "Transaction details", + style: STextStyles.navBarTitle(context), + ), + ), + body: ConditionalParent( + condition: !isDesktop, + builder: (child) => SafeArea(child: child), + child: Padding( + padding: + isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(12), + child: Column( + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction details", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: + isDesktop + ? const EdgeInsets.only(right: 32, bottom: 32) + : const EdgeInsets.all(0), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return RoundedWhiteContainer( + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, + padding: const EdgeInsets.all(0), + child: child, + ); + }, + child: SingleChildScrollView( + primary: isDesktop ? false : null, + child: Padding( + padding: + isDesktop ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), - child: Container( - decoration: isDesktop - ? BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - ), - ) - : null, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - if (isDesktop) - Row( - children: [ - TxIcon( - transaction: _transaction, - currentHeight: currentHeight, - coin: coin, - ), - const SizedBox( - width: 16, + : const EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: + isDesktop + ? BoxDecoration( + color: + Theme.of(context) + .extension()! + .backgroundAppBar, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants + .size + .circularBorderRadius, + ), ), - SelectableText( - _transaction.isCancelled - ? coin is Ethereum - ? "Failed" - : "Cancelled" - : whatIsIt( + ) + : null, + child: Padding( + padding: + isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + if (isDesktop) + Row( + children: [ + TxIcon( + transaction: _transaction, + currentHeight: currentHeight, + coin: coin, + ), + const SizedBox(width: 16), + SelectableText( + _transaction.isCancelled + ? coin is Ethereum + ? "Failed" + : "Cancelled" + : whatIsIt( _transaction, currentHeight, ), - style: - STextStyles.desktopTextMedium( - context, + style: + STextStyles.desktopTextMedium( + context, + ), ), - ), - ], - ), - Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - SelectableText( - "$amountPrefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.titleBold12( - context, - ), - ), - const SizedBox( - height: 2, + ], ), - if (ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.externalCalls, - ), - )) + Column( + crossAxisAlignment: + isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ SelectableText( - "$amountPrefix${(amount.decimal * ref.watch( - priceAnd24hChangeNotifierProvider - .select( - (value) => isTokenTx - ? value - .getTokenPrice( - _transaction - .otherData!, - ) - .item1 - : value - .getPrice( - coin, - ) - .item1, - ), - )).toAmount(fractionDigits: 2).fiatString( - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale, + "$amountPrefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.titleBold12( + context, ), - ), - )} ${ref.watch( - prefsChangeNotifierProvider - .select( - (value) => value.currency, - ), - )}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), ), - ], - ), - if (!isDesktop) - TxIcon( - transaction: _transaction, - currentHeight: currentHeight, - coin: coin, + const SizedBox(height: 2), + if (price != null) + SelectableText( + "$amountPrefix${(amount.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + ], ), - ], + if (!isDesktop) + TxIcon( + transaction: _transaction, + currentHeight: currentHeight, + coin: coin, + ), + ], + ), ), ), ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.isCancelled - ? coin is Ethereum - ? "Failed" - : "Cancelled" - : whatIsIt( - _transaction, - currentHeight, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: _transaction.type == - TransactionType.outgoing - ? Theme.of(context) - .extension()! - .accentColorOrange - : Theme.of(context) - .extension()! - .accentColorGreen, - ) - : STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - if (!((coin is Monero || coin is Wownero) && - _transaction.type == - TransactionType.outgoing) && - !((coin is Firo) && - _transaction.subType == - TransactionSubType.mint)) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), - if (!((coin is Monero || coin is Wownero) && - _transaction.type == - TransactionType.outgoing) && - !((coin is Firo) && - _transaction.subType == - TransactionSubType.mint)) + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - ConditionalParent( - condition: kDebugMode, - builder: (child) { - return Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - child, - CustomTextButton( - text: "Info", - onTap: () { - if (isDesktop) { - showDialog( - context: context, - builder: (_) => - DesktopDialog( - maxHeight: - double.infinity, - child: - AddressDetailsView( - addressId: - _transaction - .address - .value! - .id, - walletId: widget - .walletId, + Text( + "Status", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.isCancelled + ? coin is Ethereum + ? "Failed" + : "Cancelled" + : whatIsIt( + _transaction, + currentHeight, + ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + _transaction.type == + TransactionType + .outgoing + ? Theme.of(context) + .extension< + StackColors + >()! + .accentColorOrange + : Theme.of(context) + .extension< + StackColors + >()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + // ), + // ), + ], + ), + ), + if (!((coin is Monero || coin is Wownero) && + _transaction.type == + TransactionType.outgoing) && + !((coin is Firo) && + _transaction.subType == + TransactionSubType.mint)) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (!((coin is Monero || coin is Wownero) && + _transaction.type == + TransactionType.outgoing) && + !((coin is Firo) && + _transaction.subType == + TransactionSubType.mint)) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: kDebugMode, + builder: (child) { + return Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + child, + CustomTextButton( + text: "Info", + onTap: () { + if (isDesktop) { + showDialog( + context: context, + builder: + ( + _, + ) => DesktopDialog( + maxHeight: + double + .infinity, + child: AddressDetailsView( + addressId: + _transaction + .address + .value! + .id, + walletId: + widget + .walletId, + ), + ), + ); + } else { + Navigator.of( + context, + ).pushNamed( + AddressDetailsView + .routeName, + arguments: Tuple2( + _transaction + .address + .value! + .id, + widget.walletId, ), - ), - ); - } else { - Navigator.of(context) - .pushNamed( - AddressDetailsView - .routeName, - arguments: Tuple2( - _transaction.address - .value!.id, - widget.walletId, - ), - ); - } - }, - ), - ], - ); - }, - child: Text( - _transaction.type == - TransactionType.outgoing - ? "Sent to" - : "Receiving address", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, + ); + } + }, ), + ], + ); + }, + child: Text( + _transaction.type == + TransactionType.outgoing + ? "Sent to" + : "Receiving address", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), ), - ), - const SizedBox( - height: 8, - ), - _transaction.type == - TransactionType.incoming - ? FutureBuilder( + const SizedBox(height: 8), + _transaction.type == + TransactionType.incoming + ? FutureBuilder( future: fetchContactNameFor( _transaction - .address.value!.value, + .address + .value! + .value, ), builder: ( builderContext, AsyncSnapshot - snapshot, + snapshot, ) { String - addressOrContactName = - _transaction.address - .value!.value; + addressOrContactName = + _transaction + .address + .value! + .value; if (snapshot.connectionState == ConnectionState .done && @@ -763,145 +771,151 @@ class _TransactionDetailsViewState } return SelectableText( addressOrContactName, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, ) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + : STextStyles.itemSubtitle12( + context, + ), ); }, ) - : SelectableText( + : SelectableText( _transaction - .address.value!.value, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( + .address + .value! + .value, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, ) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + : STextStyles.itemSubtitle12( + context, + ), ), - ], + ], + ), ), - ), - if (isDesktop) - IconCopyButton( - data: _transaction.address.value!.value, + if (isDesktop) + IconCopyButton( + data: + _transaction.address.value!.value, + ), + ], + ), + ), + if (coin is Epiccash) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (coin is Epiccash) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "On chain note", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + const SizedBox(height: 8), + SelectableText( + _transaction.otherData ?? "", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), ), - ], + if (isDesktop) + IconCopyButton( + data: _transaction.otherData ?? "", + ), + ], + ), ), - ), - if (coin is Epiccash) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin is Epiccash) + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "On chain note", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + (coin is Epiccash) + ? "Local Note" + : "Note ", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, ) - : STextStyles.itemSubtitle( - context, - ), - ), - const SizedBox( - height: 8, - ), - SelectableText( - _transaction.otherData ?? "", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( + : STextStyles.itemSubtitle( context, ), - ), - ], - ), - ), - if (isDesktop) - IconCopyButton( - data: _transaction.otherData ?? "", - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - (coin is Epiccash) - ? "Local Note" - : "Note ", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), - ), - isDesktop - ? IconPencilButton( + ), + isDesktop + ? IconPencilButton( onPressed: () { showDialog( context: context, @@ -918,7 +932,7 @@ class _TransactionDetailsViewState ); }, ) - : GestureDetector( + : GestureDetector( onTap: () { Navigator.of(context).pushNamed( EditNoteView.routeName, @@ -934,14 +948,14 @@ class _TransactionDetailsViewState Assets.svg.pencil, width: 10, height: 10, - color: Theme.of(context) - .extension< - StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, + color: + Theme.of(context) + .extension< + StackColors + >()! + .infoItemIcons, ), + const SizedBox(width: 4), Text( "Edit", style: STextStyles.link2( @@ -951,262 +965,156 @@ class _TransactionDetailsViewState ], ), ), - ], - ), - const SizedBox( - height: 8, - ), - SelectableText( - ref - .watch( - pTransactionNote( - ( + ], + ), + const SizedBox(height: 8), + SelectableText( + ref + .watch( + pTransactionNote(( txid: _transaction.txid, - walletId: walletId - ), - ), - ) - ?.value ?? - "", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Date", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + walletId: walletId, + )), + ) + ?.value ?? + "", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - Format.extractDateFrom( - _transaction.timestamp, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) : STextStyles.itemSubtitle12( - context, - ), - ), - ], - ), - if (!isDesktop) - SelectableText( - Format.extractDateFrom( - _transaction.timestamp, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) - IconCopyButton( - data: Format.extractDateFrom( - _transaction.timestamp, - ), + context, + ), ), - ], + ], + ), ), - ), - if (coin is! NanoCurrency) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin is! NanoCurrency) + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Builder( - builder: (context) { - final String feeString = showFeePending - ? _transaction.isConfirmed( - currentHeight, - minConfirms, - ) - ? ref - .watch(pAmountFormatter(coin)) - .format( - fee, - withUnitName: isTokenTx, - ) - : "Pending" - : ref - .watch(pAmountFormatter(coin)) - .format( - fee, - withUnitName: isTokenTx, - ); - - return Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Transaction fee", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), - ), - ], - ), - if (!isDesktop) - SelectableText( - feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + Text( + "Date", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, ) - : STextStyles.itemSubtitle12( + : STextStyles.itemSubtitle( context, ), - ), + ), + if (isDesktop) + const SizedBox(height: 2), if (isDesktop) - IconCopyButton(data: feeString), + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), ], - ); - }, + ), + if (!isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (isDesktop) + IconCopyButton( + data: Format.extractDateFrom( + _transaction.timestamp, + ), + ), + ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - Builder( - builder: (context) { - final String height; - final String confirmations; - final confirms = _transaction.getConfirmations( - currentHeight, - ); - - if (widget.coin is Bitcoincash || - widget.coin is Ecash) { - height = _transaction.height != null && - _transaction.height! > 0 - ? "${_transaction.height!}" - : "Pending"; - confirmations = confirms.toString(); - } else if (widget.coin is Epiccash && - _transaction.slateId == null) { - confirmations = "Unknown"; - height = "Unknown"; - } else { - final confirmed = _transaction.isConfirmed( - currentHeight, - minConfirms, - ); - if (widget.coin is! Epiccash && confirmed) { - height = - "${_transaction.height == 0 ? "Unknown" : _transaction.height}"; - } else { - height = confirms > 0 - ? "${_transaction.height}" - : "Pending"; - } - - confirmations = confirms.toString(); - } - - return Column( - children: [ - RoundedWhiteContainer( - padding: isDesktop + if (coin is! NanoCurrency) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (coin is! NanoCurrency) + RoundedWhiteContainer( + padding: + isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), - child: Row( + child: Builder( + builder: (context) { + final String feeString = + showFeePending + ? _transaction.isConfirmed( + currentHeight, + minConfirms, + ) + ? ref + .watch( + pAmountFormatter(coin), + ) + .format( + fee, + withUnitName: isTokenTx, + ) + : "Pending" + : ref + .watch(pAmountFormatter(coin)) + .format( + fee, + withUnitName: isTokenTx, + ); + + return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: @@ -1217,614 +1125,750 @@ class _TransactionDetailsViewState CrossAxisAlignment.start, children: [ Text( - "Block height", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + "Transaction fee", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, - ).copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, ) - : STextStyles - .itemSubtitle12( + : STextStyles.itemSubtitle( context, ), + ), + if (isDesktop) + const SizedBox(height: 2), + if (isDesktop) + SelectableText( + feeString, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), if (!isDesktop) SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), - ), - if (isDesktop) - IconCopyButton(data: height), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Confirmations", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + feeString, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles.itemSubtitle( + : STextStyles.itemSubtitle12( context, ), - ), - if (isDesktop) - const SizedBox( - height: 2, + ), + if (isDesktop) + IconCopyButton(data: feeString), + ], + ); + }, + ), + ), + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + Builder( + builder: (context) { + final String height; + final String confirmations; + final confirms = _transaction + .getConfirmations(currentHeight); + + if (widget.coin is Bitcoincash || + widget.coin is Ecash) { + height = + _transaction.height != null && + _transaction.height! > 0 + ? "${_transaction.height!}" + : "Pending"; + confirmations = confirms.toString(); + } else if (widget.coin is Epiccash && + _transaction.slateId == null) { + confirmations = "Unknown"; + height = "Unknown"; + } else { + final confirmed = _transaction.isConfirmed( + currentHeight, + minConfirms, + ); + if (widget.coin is! Epiccash && confirmed) { + height = + "${_transaction.height == 0 ? "Unknown" : _transaction.height}"; + } else { + height = + confirms > 0 + ? "${_transaction.height}" + : "Pending"; + } + + confirmations = confirms.toString(); + } + + return Column( + children: [ + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Block height", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - if (isDesktop) - SelectableText( - confirmations, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + if (isDesktop) + const SizedBox(height: 2), + if (isDesktop) + SelectableText( + height, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + if (!isDesktop) + SelectableText( + height, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles - .itemSubtitle12( + : STextStyles.itemSubtitle12( context, ), + ), + if (isDesktop) + IconCopyButton(data: height), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Confirmations", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - ], - ), - if (!isDesktop) - SelectableText( - confirmations, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) + if (isDesktop) + const SizedBox(height: 2), + if (isDesktop) + SelectableText( + confirmations, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + if (!isDesktop) + SelectableText( + confirmations, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (isDesktop) + IconCopyButton(data: height), + ], + ), + ), + ], + ); + }, + ), + if (coin is Ethereum) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (coin is Ethereum) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Nonce", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + SelectableText( + _transaction.nonce.toString(), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) .extension< - StackColors>()! + StackColors + >()! .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), - ), - if (isDesktop) - IconCopyButton(data: height), - ], + ) + : STextStyles.itemSubtitle12( + context, + ), ), - ), - ], - ); - }, - ), - if (coin is Ethereum) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin is Ethereum) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Nonce", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), - ), - SelectableText( - _transaction.nonce.toString(), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - if (kDebugMode) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (kDebugMode) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Tx sub type", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), - ), - SelectableText( - _transaction.subType.toString(), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, + ], ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Transaction ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + ), + if (kDebugMode) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (kDebugMode) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Tx sub type", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, ) - : STextStyles.itemSubtitle( + : STextStyles.itemSubtitle( context, ), - ), - const SizedBox( - height: 8, - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.txid, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + ), + SelectableText( + _transaction.subType.toString(), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles.itemSubtitle12( + : STextStyles.itemSubtitle12( context, ), - ), - if (coin is! Epiccash) - const SizedBox( - height: 8, - ), - if (coin is! Epiccash) - CustomTextButton( - text: "Open in block explorer", - onTap: () async { - final uri = - getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); - - if (ref - .read( - prefsChangeNotifierProvider, - ) - .hideBlockExplorerWarning == - false) { - final shouldContinue = - await showExplorerWarning( - "${uri.scheme}://${uri.host}", - ); - - if (!shouldContinue) { - return; - } - } - - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - try { - await launchUrl( - uri, - mode: LaunchMode - .externalApplication, - ); - } catch (_) { - if (mounted) { - unawaited( - showDialog( - context: context, - builder: (_) => - StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), - ), - ); - } - } finally { - // Future.delayed( - // const Duration(seconds: 1), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - } - }, - ), - // ), - // ), - ], - ), + ), + ], ), - if (isDesktop) - const SizedBox( - width: 12, - ), - if (isDesktop) - IconCopyButton( - data: _transaction.txid, - ), - ], - ), - ), - // if ((coin is FiroTestNet || coin is Firo) && - // _transaction.subType == "mint") - // const SizedBox( - // height: 12, - // ), - // if ((coin is FiroTestNet || coin is Firo) && - // _transaction.subType == "mint") - // RoundedWhiteContainer( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "Mint Transaction ID", - // style: STextStyles.itemSubtitle(context), - // ), - // ], - // ), - // const SizedBox( - // height: 8, - // ), - // // Flexible( - // // child: FittedBox( - // // fit: BoxFit.scaleDown, - // // child: - // SelectableText( - // _transaction.otherData ?? "Unknown", - // style: STextStyles.itemSubtitle12(context), - // ), - // // ), - // // ), - // const SizedBox( - // height: 8, - // ), - // BlueTextButton( - // text: "Open in block explorer", - // onTap: () async { - // final uri = getBlockExplorerTransactionUrlFor( - // coin: coin, - // txid: _transaction.otherData ?? "Unknown", - // ); - // // ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = false; - // try { - // await launchUrl( - // uri, - // mode: LaunchMode.externalApplication, - // ); - // } catch (_) { - // unawaited(showDialog( - // context: context, - // builder: (_) => StackOkDialog( - // title: "Could not open in block explorer", - // message: - // "Failed to open \"${uri.toString()}\"", - // ), - // )); - // } finally { - // // Future.delayed( - // // const Duration(seconds: 1), - // // () => ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = true, - // // ); - // } - // }, - // ), - // ], - // ), - // ), - if (coin is Epiccash) + ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin is Epiccash) + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Slate ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.slateId ?? "Unknown", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), - ), - // ), - // ), - ], - ), - if (isDesktop) - const SizedBox( - width: 12, + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Transaction ID", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + const SizedBox(height: 8), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.txid, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (coin is! Epiccash) + const SizedBox(height: 8), + if (coin is! Epiccash) + CustomTextButton( + text: "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider, + ) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}", + ); + + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: + LaunchMode + .externalApplication, + ); + } catch (_) { + if (context.mounted) { + unawaited( + showDialog( + context: context, + builder: + ( + _, + ) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + ), + ); + } + } finally { + // Future.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, + ), + // ), + // ), + ], ), + ), + if (isDesktop) const SizedBox(width: 12), if (isDesktop) - IconCopyButton( - data: _transaction.slateId ?? "Unknown", - ), + IconCopyButton(data: _transaction.txid), ], ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), - ], + // if ((coin is FiroTestNet || coin is Firo) && + // _transaction.subType == "mint") + // const SizedBox( + // height: 12, + // ), + // if ((coin is FiroTestNet || coin is Firo) && + // _transaction.subType == "mint") + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Mint Transaction ID", + // style: STextStyles.itemSubtitle(context), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // // Flexible( + // // child: FittedBox( + // // fit: BoxFit.scaleDown, + // // child: + // SelectableText( + // _transaction.otherData ?? "Unknown", + // style: STextStyles.itemSubtitle12(context), + // ), + // // ), + // // ), + // const SizedBox( + // height: 8, + // ), + // BlueTextButton( + // text: "Open in block explorer", + // onTap: () async { + // final uri = getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction.otherData ?? "Unknown", + // ); + // // ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = false; + // try { + // await launchUrl( + // uri, + // mode: LaunchMode.externalApplication, + // ); + // } catch (_) { + // unawaited(showDialog( + // context: context, + // builder: (_) => StackOkDialog( + // title: "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // )); + // } finally { + // // Future.delayed( + // // const Duration(seconds: 1), + // // () => ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = true, + // // ); + // } + // }, + // ), + // ], + // ), + // ), + if (coin is Epiccash) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (coin is Epiccash) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Slate ID", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.slateId ?? "Unknown", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + // ), + // ), + ], + ), + if (isDesktop) const SizedBox(width: 12), + if (isDesktop) + IconCopyButton( + data: + _transaction.slateId ?? "Unknown", + ), + ], + ), + ), + if (!isDesktop) const SizedBox(height: 12), + ], + ), ), ), ), ), ), - ), - ], + ], + ), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: (coin is Epiccash && - _transaction.getConfirmations(currentHeight) < 1 && - _transaction.isCancelled == false) - ? ConditionalParent( - condition: isDesktop, - builder: (child) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - child: child, - ), - child: SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - Theme.of(context).extension()!.textError, + floatingActionButton: + (coin is Epiccash && + _transaction.getConfirmations(currentHeight) < 1 && + _transaction.isCancelled == false) + ? ConditionalParent( + condition: isDesktop, + builder: + (child) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: child, ), - ), - onPressed: () async { - final wallet = ref.read(pWallets).getWallet(walletId); + child: SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).extension()!.textError, + ), + ), + onPressed: () async { + final wallet = ref.read(pWallets).getWallet(walletId); + + if (wallet is EpiccashWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find Epic transaction ID", + context: context, + ), + ); + return; + } - if (wallet is EpiccashWallet) { - final String? id = _transaction.slateId; - if (id == null) { unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Could not find Epic transaction ID", + showDialog( + barrierDismissible: false, context: context, + builder: + (_) => + const CancellingTransactionProgressDialog(), ), ); - return; - } - - unawaited( - showDialog( - barrierDismissible: false, - context: context, - builder: (_) => - const CancellingTransactionProgressDialog(), - ), - ); - final result = - await wallet.cancelPendingTransactionAndPost(id); - if (mounted) { - // pop progress dialog - Navigator.of(context).pop(); + final result = await wallet + .cancelPendingTransactionAndPost(id); + if (context.mounted) { + // pop progress dialog + Navigator.of(context).pop(); - if (result.isEmpty) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Transaction cancelled", - onOkPressed: (_) { - wallet.refresh(); - Navigator.of(context).popUntil( - ModalRoute.withName( - WalletView.routeName, + if (result.isEmpty) { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Transaction cancelled", + onOkPressed: (_) { + wallet.refresh(); + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + }, ), - ); - }, - ), - ); - } else { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to cancel transaction", - message: result, - ), - ); + ); + } else { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + ), + ); + } } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "ERROR: Wallet type is not Epic Cash", + context: context, + ), + ); + return; } - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "ERROR: Wallet type is not Epic Cash", - context: context, - ), - ); - return; - } - }, - child: Text( - "Cancel Transaction", - style: STextStyles.button(context), + }, + child: Text( + "Cancel Transaction", + style: STextStyles.button(context), + ), ), ), - ), - ) - : null, + ) + : null, ), ); } @@ -1843,10 +1887,7 @@ class _Divider extends StatelessWidget { } class IconCopyButton extends StatelessWidget { - const IconCopyButton({ - super.key, - required this.data, - }); + const IconCopyButton({super.key, required this.data}); final String data; @@ -1860,9 +1901,7 @@ class IconCopyButton extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, elevation: 0, hoverElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), onPressed: () async { await Clipboard.setData(ClipboardData(text: data)); if (context.mounted) { @@ -1889,10 +1928,7 @@ class IconCopyButton extends StatelessWidget { } class IconPencilButton extends StatelessWidget { - const IconPencilButton({ - super.key, - this.onPressed, - }); + const IconPencilButton({super.key, this.onPressed}); final VoidCallback? onPressed; @@ -1906,9 +1942,7 @@ class IconPencilButton extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, elevation: 0, hoverElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), onPressed: () => onPressed?.call(), child: Padding( padding: const EdgeInsets.all(5), 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 2eebe5dbe..a337cd898 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 @@ -40,10 +40,7 @@ import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; class TransactionSearchFilterView extends ConsumerStatefulWidget { - const TransactionSearchFilterView({ - super.key, - required this.coin, - }); + const TransactionSearchFilterView({super.key, required this.coin}); static const String routeName = "/transactionSearchFilter"; @@ -82,18 +79,19 @@ class _TransactionSearchViewState _selectedFromDate = filterState.from; _keywordTextEditingController.text = filterState.keyword; - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); + _fromDateString = + _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); _toDateString = _selectedToDate == null ? "" : Format.formatDate(_selectedToDate!); - final String amount = filterState.amount == null - ? "" - : ref.read(pAmountFormatter(widget.coin)).format( - filterState.amount!, - withUnitName: false, - ); + final String amount = + filterState.amount == null + ? "" + : ref + .read(pAmountFormatter(widget.coin)) + .format(filterState.amount!, withUnitName: false); _amountTextEditingController.text = amount; } @@ -118,9 +116,10 @@ class _TransactionSearchViewState return Text( isDateSelected ? "From..." : _fromDateString, style: STextStyles.fieldLabel(context).copyWith( - color: isDateSelected - ? Theme.of(context).extension()!.textSubtitle2 - : Theme.of(context).extension()!.accentColorDark, + color: + isDateSelected + ? Theme.of(context).extension()!.textSubtitle2 + : Theme.of(context).extension()!.accentColorDark, ), ); } @@ -130,9 +129,10 @@ class _TransactionSearchViewState return Text( isDateSelected ? "To..." : _toDateString, style: STextStyles.fieldLabel(context).copyWith( - color: isDateSelected - ? Theme.of(context).extension()!.textSubtitle2 - : Theme.of(context).extension()!.accentColorDark, + color: + isDateSelected + ? Theme.of(context).extension()!.textSubtitle2 + : Theme.of(context).extension()!.accentColorDark, ), ); } @@ -145,13 +145,14 @@ class _TransactionSearchViewState const middleSeparatorWidth = 12.0; final isDesktop = Util.isDesktop; - final width = isDesktop - ? null - : (MediaQuery.of(context).size.width - - (middleSeparatorWidth + - (2 * middleSeparatorPadding) + - (2 * Constants.size.standardPadding))) / - 2; + final width = + isDesktop + ? null + : (MediaQuery.of(context).size.width - + (middleSeparatorWidth + + (2 * middleSeparatorPadding) + + (2 * Constants.size.standardPadding))) / + 2; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -172,7 +173,8 @@ class _TransactionSearchViewState _selectedFromDate = date; // flag to adjust date so from date is always before to date - final flag = _selectedToDate != null && + final flag = + _selectedToDate != null && !_selectedFromDate!.isBefore(_selectedToDate!); if (flag) { _selectedToDate = DateTime.fromMillisecondsSinceEpoch( @@ -182,13 +184,15 @@ class _TransactionSearchViewState setState(() { if (flag) { - _toDateString = _selectedToDate == null - ? "" - : Format.formatDate(_selectedToDate!); + _toDateString = + _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); } - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); + _fromDateString = + _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); }); } } @@ -196,15 +200,18 @@ class _TransactionSearchViewState child: Container( width: width, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), border: Border.all( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, width: 1, ), ), @@ -219,18 +226,15 @@ class _TransactionSearchViewState Assets.svg.calendar, height: 20, width: 20, - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - const SizedBox( - width: 10, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, ), + const SizedBox(width: 10), Align( alignment: Alignment.centerLeft, - child: FittedBox( - child: _dateFromText, - ), + child: FittedBox(child: _dateFromText), ), ], ), @@ -239,8 +243,9 @@ class _TransactionSearchViewState ), ), Padding( - padding: - const EdgeInsets.symmetric(horizontal: middleSeparatorPadding), + padding: const EdgeInsets.symmetric( + horizontal: middleSeparatorPadding, + ), child: Container( width: middleSeparatorWidth, // height: 1, @@ -263,7 +268,8 @@ class _TransactionSearchViewState _selectedToDate = date; // flag to adjust date so from date is always before to date - final flag = _selectedFromDate != null && + final flag = + _selectedFromDate != null && !_selectedToDate!.isAfter(_selectedFromDate!); if (flag) { _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( @@ -273,13 +279,15 @@ class _TransactionSearchViewState setState(() { if (flag) { - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); + _fromDateString = + _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); } - _toDateString = _selectedToDate == null - ? "" - : Format.formatDate(_selectedToDate!); + _toDateString = + _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); }); } } @@ -287,15 +295,18 @@ class _TransactionSearchViewState child: Container( width: width, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), border: Border.all( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, width: 1, ), ), @@ -310,18 +321,15 @@ class _TransactionSearchViewState Assets.svg.calendar, height: 20, width: 20, - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - const SizedBox( - width: 10, + color: + Theme.of( + context, + ).extension()!.textSubtitle2, ), + const SizedBox(width: 10), Align( alignment: Alignment.centerLeft, - child: FittedBox( - child: _dateToText, - ), + child: FittedBox(child: _dateToText), ), ], ), @@ -329,10 +337,7 @@ class _TransactionSearchViewState ), ), ), - if (isDesktop) - const SizedBox( - width: 24, - ), + if (isDesktop) const SizedBox(width: 24), ], ); } @@ -344,10 +349,7 @@ class _TransactionSearchViewState maxWidth: 576, maxHeight: double.infinity, child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, bottom: 32), child: _buildContent(context), ), ); @@ -375,22 +377,23 @@ class _TransactionSearchViewState style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: EdgeInsets.symmetric( - horizontal: Constants.size.standardPadding, - ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight( - child: _buildContent(context), + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: Constants.size.standardPadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: _buildContent(context)), ), - ), - ); - }, + ); + }, + ), ), ), ), @@ -415,9 +418,7 @@ class _TransactionSearchViewState const DesktopDialogCloseButton(), ], ), - SizedBox( - height: isDesktop ? 14 : 10, - ), + SizedBox(height: isDesktop ? 14 : 10), if (!isDesktop) Align( alignment: Alignment.centerLeft, @@ -428,10 +429,7 @@ class _TransactionSearchViewState ), ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), RoundedWhiteContainer( padding: EdgeInsets.all(isDesktop ? 0 : 12), child: Column( @@ -466,9 +464,7 @@ class _TransactionSearchViewState }, ), ), - const SizedBox( - width: 14, - ), + const SizedBox(width: 14), Align( alignment: Alignment.centerLeft, child: FittedBox( @@ -476,14 +472,16 @@ class _TransactionSearchViewState children: [ Text( "Sent", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextSmall( + context, + ) + : STextStyles.itemSubtitle12( + context, + ), ), - if (isDesktop) - const SizedBox( - height: 4, - ), + if (isDesktop) const SizedBox(height: 4), ], ), ), @@ -494,9 +492,7 @@ class _TransactionSearchViewState ), ], ), - SizedBox( - height: isDesktop ? 4 : 10, - ), + SizedBox(height: isDesktop ? 4 : 10), Row( children: [ GestureDetector( @@ -526,9 +522,7 @@ class _TransactionSearchViewState }, ), ), - const SizedBox( - width: 14, - ), + const SizedBox(width: 14), Align( alignment: Alignment.centerLeft, child: FittedBox( @@ -536,14 +530,16 @@ class _TransactionSearchViewState children: [ Text( "Received", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextSmall( + context, + ) + : STextStyles.itemSubtitle12( + context, + ), ), - if (isDesktop) - const SizedBox( - height: 4, - ), + if (isDesktop) const SizedBox(height: 4), ], ), ), @@ -554,9 +550,7 @@ class _TransactionSearchViewState ), ], ), - SizedBox( - height: isDesktop ? 4 : 10, - ), + SizedBox(height: isDesktop ? 4 : 10), Row( children: [ GestureDetector( @@ -586,9 +580,7 @@ class _TransactionSearchViewState }, ), ), - const SizedBox( - width: 14, - ), + const SizedBox(width: 14), Align( alignment: Alignment.centerLeft, child: FittedBox( @@ -596,14 +588,16 @@ class _TransactionSearchViewState children: [ Text( "Trades", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextSmall( + context, + ) + : STextStyles.itemSubtitle12( + context, + ), ), - if (isDesktop) - const SizedBox( - height: 4, - ), + if (isDesktop) const SizedBox(height: 4), ], ), ), @@ -617,41 +611,35 @@ class _TransactionSearchViewState ], ), ), - SizedBox( - height: isDesktop ? 32 : 24, - ), + SizedBox(height: isDesktop ? 32 : 24), Align( alignment: Alignment.centerLeft, child: FittedBox( child: Text( "Date", - style: isDesktop - ? STextStyles.labelExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), ), ), - SizedBox( - height: isDesktop ? 10 : 8, - ), + SizedBox(height: isDesktop ? 10 : 8), _buildDateRangePicker(), - SizedBox( - height: isDesktop ? 32 : 24, - ), + SizedBox(height: isDesktop ? 32 : 24), Align( alignment: Alignment.centerLeft, child: FittedBox( child: Text( "Amount", - style: isDesktop - ? STextStyles.labelExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), ), ), - SizedBox( - height: isDesktop ? 10 : 8, - ), + SizedBox(height: isDesktop ? 10 : 8), Padding( padding: EdgeInsets.only(right: isDesktop ? 32 : 0), child: ClipRRect( @@ -665,19 +653,21 @@ class _TransactionSearchViewState controller: _amountTextEditingController, focusNode: amountTextFieldFocusNode, onChanged: (_) => setState(() {}), - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), inputFormatters: [ AmountInputFormatter( decimals: widget.coin.fractionDigits, unit: ref.watch(pAmountUnit(widget.coin)), locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), ), ), // // regex to validate a crypto amount with 8 decimal places @@ -687,65 +677,67 @@ class _TransactionSearchViewState // ? newValue // : oldValue), ], - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), decoration: standardInputDecoration( "Enter ${widget.coin.ticker} amount...", keywordTextFieldFocusNode, context, desktopMed: isDesktop, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ) - : null, - suffixIcon: _amountTextEditingController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _amountTextEditingController.text = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: + _amountTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _amountTextEditingController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), - SizedBox( - height: isDesktop ? 32 : 24, - ), + SizedBox(height: isDesktop ? 32 : 24), Align( alignment: Alignment.centerLeft, child: FittedBox( child: Text( "Keyword", - style: isDesktop - ? STextStyles.labelExtraExtraSmall(context) - : STextStyles.smallMed12(context), + style: + isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), ), ), ), - SizedBox( - height: isDesktop ? 10 : 8, - ), + SizedBox(height: isDesktop ? 10 : 8), Padding( padding: EdgeInsets.only(right: isDesktop ? 32 : 0), child: ClipRRect( @@ -758,13 +750,16 @@ class _TransactionSearchViewState key: const Key("transactionSearchViewKeywordFieldKey"), controller: _keywordTextEditingController, focusNode: keywordTextFieldFocusNode, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), onChanged: (_) => setState(() {}), decoration: standardInputDecoration( "Type keyword...", @@ -772,39 +767,39 @@ class _TransactionSearchViewState context, desktopMed: isDesktop, ).copyWith( - contentPadding: isDesktop - ? const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ) - : null, - suffixIcon: _keywordTextEditingController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _keywordTextEditingController.text = ""; - }); - }, - ), - ], + contentPadding: + isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: + _keywordTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _keywordTextEditingController.text = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 32 : 20, - ), + SizedBox(height: isDesktop ? 32 : 20), Row( children: [ Expanded( @@ -816,9 +811,7 @@ class _TransactionSearchViewState if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed( - const Duration( - milliseconds: 75, - ), + const Duration(milliseconds: 75), ); } } @@ -855,9 +848,7 @@ class _TransactionSearchViewState // ), // ), // ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: isDesktop ? ButtonHeight.l : null, @@ -884,16 +875,10 @@ class _TransactionSearchViewState // ), // ), // ), - if (isDesktop) - const SizedBox( - width: 32, - ), + if (isDesktop) const SizedBox(width: 32), ], ), - if (!isDesktop) - const SizedBox( - height: 20, - ), + if (!isDesktop) const SizedBox(height: 20), ], ); } @@ -902,11 +887,14 @@ class _TransactionSearchViewState final amountText = _amountTextEditingController.text; Amount? amount; if (amountText.isNotEmpty && !(amountText == "," || amountText == ".")) { - amount = amountText.contains(",") - ? Decimal.parse(amountText.replaceFirst(",", ".")) - .toAmount(fractionDigits: widget.coin.fractionDigits) - : Decimal.parse(amountText) - .toAmount(fractionDigits: widget.coin.fractionDigits); + amount = + amountText.contains(",") + ? Decimal.parse( + amountText.replaceFirst(",", "."), + ).toAmount(fractionDigits: widget.coin.fractionDigits) + : Decimal.parse( + amountText, + ).toAmount(fractionDigits: widget.coin.fractionDigits); } final TransactionFilter filter = TransactionFilter( diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart index ed6e007d2..353264600 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -32,6 +33,7 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/format.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; +import '../../../../wallets/crypto_currency/coins/ethereum.dart'; import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; @@ -51,11 +53,8 @@ import '../transaction_search_filter_view.dart'; import 'transaction_v2_card.dart'; import 'transaction_v2_details_view.dart'; -typedef _GroupedTransactions = ({ - String label, - DateTime startDate, - List transactions -}); +typedef _GroupedTransactions = + ({String label, DateTime startDate, List transactions}); class AllTransactionsV2View extends ConsumerStatefulWidget { const AllTransactionsV2View({ @@ -108,13 +107,14 @@ class _AllTransactionsV2ViewState extends ConsumerState { // debugPrint("FILTER: $filter"); final contacts = ref.read(addressBookServiceProvider).contacts; - final notes = ref - .read(mainDBProvider) - .isar - .transactionNotes - .where() - .walletIdEqualTo(walletId) - .findAllSync(); + final notes = + ref + .read(mainDBProvider) + .isar + .transactionNotes + .where() + .walletIdEqualTo(walletId) + .findAllSync(); return transactions.where((tx) { if (!filter.sent && !filter.received) { @@ -160,23 +160,25 @@ class _AllTransactionsV2ViewState extends ConsumerState { bool contains = false; // check if address book name contains - contains |= contacts - .where( - (e) => - e.addresses - .map((e) => e.address) - .toSet() - .intersection(tx.associatedAddresses()) - .isNotEmpty && - e.name.toLowerCase().contains(keyword), - ) - .isNotEmpty; + contains |= + contacts + .where( + (e) => + e.addresses + .map((e) => e.address) + .toSet() + .intersection(tx.associatedAddresses()) + .isNotEmpty && + e.name.toLowerCase().contains(keyword), + ) + .isNotEmpty; // check if address contains - contains |= tx - .associatedAddresses() - .where((e) => e.toLowerCase().contains(keyword)) - .isNotEmpty; + contains |= + tx + .associatedAddresses() + .where((e) => e.toLowerCase().contains(keyword)) + .isNotEmpty; TransactionNote? note; final matchingNotes = notes.where((e) => e.txid == tx.txid); @@ -197,8 +199,9 @@ class _AllTransactionsV2ViewState extends ConsumerState { contains |= tx.type.name.toLowerCase().contains(keyword); // check if date contains - contains |= - Format.extractDateFrom(tx.timestamp).toLowerCase().contains(keyword); + contains |= Format.extractDateFrom( + tx.timestamp, + ).toLowerCase().contains(keyword); return contains; } @@ -212,13 +215,14 @@ class _AllTransactionsV2ViewState extends ConsumerState { } text = text.toLowerCase(); final contacts = ref.read(addressBookServiceProvider).contacts; - final notes = ref - .read(mainDBProvider) - .isar - .transactionNotes - .where() - .walletIdEqualTo(walletId) - .findAllSync(); + final notes = + ref + .read(mainDBProvider) + .isar + .transactionNotes + .where() + .walletIdEqualTo(walletId) + .findAllSync(); return transactions .where((tx) => _isKeywordMatch(tx, text, contacts, notes)) @@ -234,8 +238,11 @@ class _AllTransactionsV2ViewState extends ConsumerState { final date = DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000); final monthYear = "${Constants.monthMap[date.month]} ${date.year}"; if (map[monthYear] == null) { - map[monthYear] = - (label: monthYear, startDate: date, transactions: [tx]); + map[monthYear] = ( + label: monthYear, + startDate: date, + transactions: [tx], + ); } else { map[monthYear]!.transactions.add(tx); } @@ -252,96 +259,94 @@ class _AllTransactionsV2ViewState extends ConsumerState { return MasterScaffold( background: Theme.of(context).extension()!.background, isDesktop: isDesktop, - appBar: isDesktop - ? DesktopAppBar( - isCompactHeight: true, - background: Theme.of(context).extension()!.popupBG, - leading: Row( - children: [ - const SizedBox( - width: 32, - ), - 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, - ), - const SizedBox( - width: 12, - ), - Text( - "Transactions", - style: STextStyles.desktopH3(context), - ), - ], - ), - ) - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Transactions", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 20, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("transactionSearchFilterViewButton"), - size: 36, + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + const SizedBox(width: 32), + AppBarIconButton( + size: 32, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], - color: Theme.of(context) - .extension()! - .background, icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), - onPressed: () { - Navigator.of(context).pushNamed( - TransactionSearchFilterView.routeName, - arguments: ref.read(pWalletCoin(walletId)), - ); - }, + onPressed: Navigator.of(context).pop, ), - ), + const SizedBox(width: 12), + Text("Transactions", style: STextStyles.desktopH3(context)), + ], ), - ], - ), + ) + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Transactions", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("transactionSearchFilterViewButton"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.filter, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + TransactionSearchFilterView.routeName, + arguments: ref.read(pWalletCoin(walletId)), + ); + }, + ), + ), + ), + ], + ), body: Padding( padding: EdgeInsets.only( left: isDesktop ? 20 : 12, @@ -356,15 +361,10 @@ class _AllTransactionsV2ViewState extends ConsumerState { children: [ ConditionalParent( condition: isDesktop, - builder: (child) => SizedBox( - width: 570, - child: child, - ), + builder: (child) => SizedBox(width: 570, child: child), child: ConditionalParent( condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -379,15 +379,18 @@ class _AllTransactionsV2ViewState extends ConsumerState { _searchString = value; }); }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), decoration: standardInputDecoration( "Search...", searchFieldFocusNode, @@ -405,35 +408,33 @@ class _AllTransactionsV2ViewState extends ConsumerState { 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 = ""; - }); - }, - ), - ], + 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, + ) + : null, ), ), ), ), ), - if (isDesktop) - const SizedBox( - width: 20, - ), + if (isDesktop) const SizedBox(width: 20), if (isDesktop) SecondaryButton( buttonHeight: ButtonHeight.l, @@ -441,9 +442,10 @@ class _AllTransactionsV2ViewState extends ConsumerState { label: "Filter", icon: SvgPicture.asset( Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -468,25 +470,14 @@ class _AllTransactionsV2ViewState extends ConsumerState { ], ), ), - if (isDesktop) - const SizedBox( - height: 8, - ), + if (isDesktop) const SizedBox(height: 8), if (isDesktop && ref.watch(transactionFilterProvider.state).state != null) const Padding( - padding: EdgeInsets.symmetric( - vertical: 8, - ), - child: Row( - children: [ - TransactionFilterOptionBar(), - ], - ), + padding: EdgeInsets.symmetric(vertical: 8), + child: Row(children: [TransactionFilterOptionBar()]), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: Consumer( builder: (_, ref, __) { @@ -494,33 +485,35 @@ class _AllTransactionsV2ViewState extends ConsumerState { ref.watch(transactionFilterProvider.state).state; return FutureBuilder( - future: ref - .watch(mainDBProvider) - .isar - .transactionV2s - .buildQuery( - whereClauses: [ - IndexWhereClause.equalTo( - indexName: 'walletId', - value: [widget.walletId], - ), - ], - filter: widget.contractAddress == null - ? ref - .watch(pWallets) - .getWallet(widget.walletId) - .transactionFilterOperation - : ref - .read(pCurrentTokenWallet)! - .transactionFilterOperation, - sortBy: [ - const SortProperty( - property: "timestamp", - sort: Sort.desc, - ), - ], - ) - .findAll(), + future: + ref + .watch(mainDBProvider) + .isar + .transactionV2s + .buildQuery( + whereClauses: [ + IndexWhereClause.equalTo( + indexName: 'walletId', + value: [widget.walletId], + ), + ], + filter: + widget.contractAddress == null + ? ref + .watch(pWallets) + .getWallet(widget.walletId) + .transactionFilterOperation + : ref + .read(pCurrentTokenWallet)! + .transactionFilterOperation, + sortBy: [ + const SortProperty( + property: "timestamp", + sort: Sort.desc, + ), + ], + ) + .findAll(), builder: (_, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -549,43 +542,39 @@ class _AllTransactionsV2ViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (index != 0) - const SizedBox( - height: 12, - ), + if (index != 0) const SizedBox(height: 12), Text( month.label, style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (isDesktop) RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: ListView.separated( shrinkWrap: true, primary: false, - separatorBuilder: (context, _) => - Container( - height: 1, - color: Theme.of(context) - .extension()! - .background, - ), + separatorBuilder: + (context, _) => Container( + height: 1, + color: + Theme.of(context) + .extension()! + .background, + ), itemCount: month.transactions.length, - itemBuilder: (context, index) => - Padding( - padding: const EdgeInsets.all(4), - child: DesktopTransactionCardRow( - key: Key( - "transactionCard_key_${month.transactions[index].txid}", + itemBuilder: + (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: DesktopTransactionCardRow( + key: Key( + "transactionCard_key_${month.transactions[index].txid}", + ), + transaction: + month.transactions[index], + walletId: walletId, + ), ), - transaction: - month.transactions[index], - walletId: walletId, - ), - ), ), ), if (!isDesktop) @@ -652,10 +641,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - sent: false, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(sent: false); setState(() {}); } }, @@ -671,10 +660,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - received: false, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(received: false); setState(() {}); } }, @@ -691,10 +680,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - to: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(to: null); setState(() {}); } }, @@ -710,10 +699,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - from: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(from: null); setState(() {}); } }, @@ -730,10 +719,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - amount: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(amount: null); setState(() {}); } }, @@ -749,10 +738,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - keyword: "", - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(keyword: ""); setState(() {}); } }, @@ -773,9 +762,7 @@ class _TransactionFilterOptionBarState scrollDirection: Axis.horizontal, shrinkWrap: true, itemCount: items.length, - separatorBuilder: (_, __) => const SizedBox( - width: 16, - ), + separatorBuilder: (_, __) => const SizedBox(width: 16), itemBuilder: (context, index) => items[index], ), ); @@ -804,9 +791,7 @@ class TransactionFilterOptionBarItem extends StatelessWidget { borderRadius: BorderRadius.circular(1000), ), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - ), + padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -824,9 +809,7 @@ class TransactionFilterOptionBarItem extends StatelessWidget { ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), XIcon( width: 16, height: 16, @@ -865,23 +848,25 @@ class _DesktopTransactionCardRowState bool get isTokenTx => ethContract != null; String whatIsIt(TransactionV2 tx, int height) => tx.statusLabel( - currentChainHeight: height, - minConfirms: minConfirms, - minCoinbaseConfirms: ref + currentChainHeight: height, + minConfirms: minConfirms, + minCoinbaseConfirms: + ref .read(pWallets) .getWallet(widget.walletId) .cryptoCurrency .minCoinbaseConfirms, - ); + ); @override void initState() { walletId = widget.walletId; - minConfirms = ref - .read(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms; + minConfirms = + ref + .read(pWallets) + .getWallet(widget.walletId) + .cryptoCurrency + .minConfirms; _transaction = widget.transaction; if (_transaction.subType == TransactionSubType.ethToken) { @@ -901,17 +886,25 @@ class _DesktopTransactionCardRowState localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); final coin = ref.watch(pWalletCoin(walletId)); - final price = ref - .watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin)), - ) - .item1; + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(_transaction.contractAddress!)?.value + : value.getPrice(coin)?.value, + ), + ); + } late final String prefix; if (Util.isDesktop) { @@ -939,6 +932,7 @@ class _DesktopTransactionCardRowState case TransactionType.outgoing: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; @@ -970,6 +964,7 @@ class _DesktopTransactionCardRowState case TransactionType.unknown: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; } @@ -979,8 +974,9 @@ class _DesktopTransactionCardRowState color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: RawMaterialButton( shape: RoundedRectangleBorder( @@ -992,34 +988,28 @@ class _DesktopTransactionCardRowState if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TransactionV2DetailsView( - transaction: _transaction, - coin: coin, - walletId: walletId, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionV2DetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), + ), ); } else { unawaited( Navigator.of(context).pushNamed( TransactionV2DetailsView.routeName, - arguments: ( - tx: _transaction, - coin: coin, - walletId: walletId, - ), + arguments: (tx: _transaction, coin: coin, walletId: walletId), ), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: Row( children: [ TxIcon( @@ -1027,18 +1017,14 @@ class _DesktopTransactionCardRowState currentHeight: currentHeight, coin: coin, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( flex: 3, child: Text( - whatIsIt( - _transaction, - currentHeight, - ), - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + whatIsIt(_transaction, currentHeight), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), @@ -1062,24 +1048,18 @@ class _DesktopTransactionCardRowState flex: 6, child: Text( "$prefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) + if (price != null) Expanded( flex: 4, child: Text( - "$prefix${(amount.decimal * price).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "$prefix${(amount.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.desktopTextExtraExtraSmall(context), ), ), diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart index 34abbb43e..308210124 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart @@ -23,6 +23,7 @@ import '../../../../utilities/amount/amount_formatter.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; +import '../../../../wallets/crypto_currency/coins/ethereum.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; import '../../../../widgets/background.dart'; @@ -37,10 +38,7 @@ import '../../../../widgets/stack_dialog.dart'; import '../../../send_view/confirm_transaction_view.dart'; class BoostTransactionView extends ConsumerStatefulWidget { - const BoostTransactionView({ - super.key, - required this.transaction, - }); + const BoostTransactionView({super.key, required this.transaction}); static const String routeName = "/boostTransaction"; @@ -73,10 +71,11 @@ class _BoostTransactionViewState extends ConsumerState { if (_newRate <= rate) { await showDialog( context: context, - builder: (_) => const StackOkDialog( - title: "Error", - message: "New fee rate must be greater than the current rate.", - ), + builder: + (_) => const StackOkDialog( + title: "Error", + message: "New fee rate must be greater than the current rate.", + ), ); return; } @@ -99,11 +98,12 @@ class _BoostTransactionViewState extends ConsumerState { if (txData == null && mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "RBF send error", - message: ex?.toString() ?? "Unknown error found", - maxWidth: 600, - ), + builder: + (_) => StackOkDialog( + title: "RBF send error", + message: ex?.toString() ?? "Unknown error found", + maxWidth: 600, + ), ); return; } else { @@ -112,17 +112,18 @@ class _BoostTransactionViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmTransactionView( - txData: txData!, - walletId: walletId, - onSuccess: () {}, - // isPaynymTransaction: isPaynymSend, TODO ? - routeOnSuccessName: DesktopHomeView.routeName, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + txData: txData!, + walletId: walletId, + onSuccess: () {}, + // isPaynymTransaction: isPaynymSend, TODO ? + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), ), ); } else if (mounted) { @@ -130,12 +131,13 @@ class _BoostTransactionViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: txData!, - walletId: walletId, - // isPaynymTransaction: isPaynymSend, TODO ? - onSuccess: () {}, - ), + builder: + (_) => ConfirmTransactionView( + txData: txData!, + walletId: walletId, + // isPaynymTransaction: isPaynymSend, TODO ? + onSuccess: () {}, + ), settings: const RouteSettings( name: ConfirmTransactionView.routeName, ), @@ -159,6 +161,7 @@ class _BoostTransactionViewState extends ConsumerState { ); amount = _transaction.getAmountSentFromThisWallet( fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, + subtractFee: ref.read(pWalletCoin(walletId)) is! Ethereum, ); rate = (fee.raw ~/ BigInt.from(_transaction.vSize!)).toInt(); _newRate = rate + 1; @@ -169,61 +172,56 @@ class _BoostTransactionViewState extends ConsumerState { @override Widget build(BuildContext context) { final coin = ref.watch(pWalletCoin(walletId)); - final String feeString = ref.watch(pAmountFormatter(coin)).format( - fee, - ); - final String amountString = ref.watch(pAmountFormatter(coin)).format( - amount, - ); + final String feeString = ref.watch(pAmountFormatter(coin)).format(fee); + final String amountString = ref + .watch(pAmountFormatter(coin)) + .format(amount); final String feeRateString = "$rate sats/vByte"; return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Boost transaction", - style: STextStyles.navBarTitle(context), + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Boost transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea(child: child), ), ), - body: child, - ), - ), child: Padding( - padding: isDesktop - ? const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.only(left: 32, right: 32, bottom: 32) + : const EdgeInsets.all(12), child: ConditionalParent( condition: isDesktop, builder: (child) { return Column( children: [ RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, padding: const EdgeInsets.all(0), child: child, ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), PrimaryButton( buttonHeight: ButtonHeight.l, label: "Preview send", @@ -238,10 +236,11 @@ class _BoostTransactionViewState extends ConsumerState { children: [ ConditionalParent( condition: isDesktop, - builder: (child) => RoundedWhiteContainer( - padding: EdgeInsets.zero, - child: child, - ), + builder: + (child) => RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: child, + ), child: Column( children: [ DetailItem( @@ -277,15 +276,9 @@ class _BoostTransactionViewState extends ConsumerState { ), ), if (!isDesktop) const Spacer(), + if (!isDesktop) const SizedBox(height: 16), if (!isDesktop) - const SizedBox( - height: 16, - ), - if (!isDesktop) - PrimaryButton( - label: "Preview send", - onPressed: _previewTxn, - ), + PrimaryButton(label: "Preview send", onPressed: _previewTxn), ], ), ), @@ -305,9 +298,7 @@ class _Divider extends StatelessWidget { color: Theme.of(context).extension()!.backgroundAppBar, ); } else { - return const SizedBox( - height: 12, - ); + return const SizedBox(height: 12); } } } diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/fusion_group_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/fusion_group_details_view.dart index ef473b3bf..c861bbff6 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/fusion_group_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/fusion_group_details_view.dart @@ -10,8 +10,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; -import 'transaction_v2_list_item.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; @@ -21,6 +21,7 @@ import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/rounded_white_container.dart'; +import 'transaction_v2_list_item.dart'; class FusionGroupDetailsView extends ConsumerStatefulWidget { const FusionGroupDetailsView({ @@ -48,23 +49,15 @@ class _FusionGroupDetailsViewState BorderRadius get _borderRadiusFirst { return BorderRadius.only( - topLeft: Radius.circular( - Constants.size.circularBorderRadius, - ), - topRight: Radius.circular( - Constants.size.circularBorderRadius, - ), + topLeft: Radius.circular(Constants.size.circularBorderRadius), + topRight: Radius.circular(Constants.size.circularBorderRadius), ); } BorderRadius get _borderRadiusLast { return BorderRadius.only( - bottomLeft: Radius.circular( - Constants.size.circularBorderRadius, - ), - bottomRight: Radius.circular( - Constants.size.circularBorderRadius, - ), + bottomLeft: Radius.circular(Constants.size.circularBorderRadius), + bottomRight: Radius.circular(Constants.size.circularBorderRadius), ); } @@ -97,16 +90,14 @@ class _FusionGroupDetailsViewState ), Flexible( child: Padding( - padding: const EdgeInsets.only( - right: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(right: 32, bottom: 32), child: RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, padding: const EdgeInsets.all(0), child: ListView.separated( shrinkWrap: true, @@ -132,9 +123,10 @@ class _FusionGroupDetailsViewState return Container( width: double.infinity, height: 1.2, - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, ); }, itemCount: widget.transactions.length, @@ -164,29 +156,27 @@ class _FusionGroupDetailsViewState style: STextStyles.navBarTitle(context), ), ), - body: Padding( - padding: const EdgeInsets.all(16), - child: ListView.builder( - itemCount: widget.transactions.length, - itemBuilder: (context, index) { - BorderRadius? radius; - if (widget.transactions.length == 1) { - radius = BorderRadius.circular( - Constants.size.circularBorderRadius, - ); - } else if (index == widget.transactions.length - 1) { - radius = _borderRadiusLast; - } else if (index == 0) { - radius = _borderRadiusFirst; - } - final tx = widget.transactions[index]; + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: ListView.builder( + itemCount: widget.transactions.length, + itemBuilder: (context, index) { + BorderRadius? radius; + if (widget.transactions.length == 1) { + radius = BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == widget.transactions.length - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = widget.transactions[index]; - return TxListItem( - tx: tx, - coin: widget.coin, - radius: radius, - ); - }, + return TxListItem(tx: tx, coin: widget.coin, radius: radius); + }, + ), ), ), ), diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart index 34e5c662b..3fb0aaaab 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -20,15 +21,14 @@ import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../../../widgets/coin_ticker_tag.dart'; +import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../sub_widgets/tx_icon.dart'; import 'transaction_v2_details_view.dart'; class TransactionCardV2 extends ConsumerStatefulWidget { - const TransactionCardV2({ - super.key, - required this.transaction, - }); + const TransactionCardV2({super.key, required this.transaction}); final TransactionV2 transaction; @@ -47,25 +47,24 @@ class _TransactionCardStateV2 extends ConsumerState { bool get isTokenTx => tokenContract != null; - String whatIsIt( - CryptoCurrency coin, - int currentHeight, - ) => + String whatIsIt(CryptoCurrency coin, int currentHeight) => _transaction.isCancelled && coin is Ethereum ? "Failed" : _transaction.statusLabel( - currentChainHeight: currentHeight, - minConfirms: ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .minConfirms, - minCoinbaseConfirms: ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .minCoinbaseConfirms, - ); + currentChainHeight: currentHeight, + minConfirms: + ref + .read(pWallets) + .getWallet(walletId) + .cryptoCurrency + .minConfirms, + minCoinbaseConfirms: + ref + .read(pWallets) + .getWallet(walletId) + .cryptoCurrency + .minCoinbaseConfirms, + ); @override void initState() { @@ -106,18 +105,23 @@ class _TransactionCardStateV2 extends ConsumerState { localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); - final price = ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => isTokenTx - ? value.getTokenPrice(tokenContract!.address) - : value.getPrice(coin), - ), - ) - .item1; + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(tokenContract!.address)?.value + : value.getPrice(coin)?.value, + ), + ); + } final currentHeight = ref.watch(pWalletChainHeight(walletId)); @@ -134,6 +138,7 @@ class _TransactionCardStateV2 extends ConsumerState { case TransactionType.outgoing: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; @@ -165,6 +170,7 @@ class _TransactionCardStateV2 extends ConsumerState { case TransactionType.unknown: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; } @@ -174,8 +180,9 @@ class _TransactionCardStateV2 extends ConsumerState { color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: Padding( padding: const EdgeInsets.all(6), @@ -189,25 +196,22 @@ class _TransactionCardStateV2 extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TransactionV2DetailsView( - transaction: _transaction, - coin: coin, - walletId: walletId, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionV2DetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), + ), ); } else { unawaited( Navigator.of(context).pushNamed( TransactionV2DetailsView.routeName, - arguments: ( - tx: _transaction, - coin: coin, - walletId: walletId, - ), + arguments: (tx: _transaction, coin: coin, walletId: walletId), ), ); } @@ -221,9 +225,7 @@ class _TransactionCardStateV2 extends ConsumerState { coin: coin, currentHeight: currentHeight, ), - const SizedBox( - width: 14, - ), + const SizedBox(width: 14), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -235,18 +237,32 @@ class _TransactionCardStateV2 extends ConsumerState { Flexible( child: FittedBox( fit: BoxFit.scaleDown, - child: Text( - whatIsIt( - coin, - currentHeight, + child: ConditionalParent( + condition: + coin is Firo && + _transaction.isInstantLock && + !_transaction.isConfirmed( + currentHeight, + coin.minConfirms, + coin.minCoinbaseConfirms, + ), + builder: + (child) => Row( + children: [ + child, + + const SizedBox(width: 10), + const CoinTickerTag(ticker: "INSTANT"), + ], + ), + child: Text( + whatIsIt(coin, currentHeight), + style: STextStyles.itemSubtitle12(context), ), - style: STextStyles.itemSubtitle12(context), ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Flexible( child: FittedBox( fit: BoxFit.scaleDown, @@ -262,9 +278,7 @@ class _TransactionCardStateV2 extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // crossAxisAlignment: CrossAxisAlignment.end, @@ -278,29 +292,15 @@ class _TransactionCardStateV2 extends ConsumerState { ), ), ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) - const SizedBox( - width: 10, - ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) + if (price != null) const SizedBox(width: 10), + if (price != null) Flexible( child: FittedBox( fit: BoxFit.scaleDown, child: Builder( builder: (_) { return Text( - "$prefix${Amount.fromDecimal( - amount.decimal * price, - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "$prefix${Amount.fromDecimal(amount.decimal * price!, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.label(context), ); }, diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index 8f0852941..c2a7e9adf 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -23,7 +24,6 @@ import '../../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../../models/isar/models/ethereum/eth_contract.dart'; import '../../../../notifications/show_flush_bar.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/global/address_book_service_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; @@ -43,6 +43,7 @@ import '../../../../wallets/isar/models/spark_coin.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../widgets/background.dart'; @@ -133,43 +134,43 @@ class _TransactionV2DetailsViewState if (mounted) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Boost transaction", - style: STextStyles.desktopH3(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Boost transaction", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: SingleChildScrollView( + child: BoostTransactionView( + transaction: _transaction, + ), ), ), - const DesktopDialogCloseButton(), ], ), - Flexible( - child: SingleChildScrollView( - child: BoostTransactionView( - transaction: _transaction, - ), - ), - ), - ], - ), - ), + ), ); } } else { unawaited( - Navigator.of(context).pushNamed( - BoostTransactionView.routeName, - arguments: _transaction, - ), + Navigator.of( + context, + ).pushNamed(BoostTransactionView.routeName, arguments: _transaction), ); } } finally { @@ -185,13 +186,15 @@ class _TransactionV2DetailsViewState final wallet = ref.read(pWallets).getWallet(walletId); - hasTxKeyProbably = wallet is LibMoneroWallet && + hasTxKeyProbably = + (wallet is LibMoneroWallet || wallet is LibSalviumWallet) && (_transaction.type == TransactionType.outgoing || _transaction.type == TransactionType.sentToSelf); if (_transaction.type case TransactionType.sentToSelf || TransactionType.outgoing) { - supportsRbf = _transaction.subType == TransactionSubType.none && + supportsRbf = + _transaction.subType == TransactionSubType.none && wallet is RbfInterface; } else { supportsRbf = false; @@ -230,6 +233,7 @@ class _TransactionV2DetailsViewState case TransactionType.unknown: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; @@ -240,81 +244,86 @@ class _TransactionV2DetailsViewState ); break; } - data = _transaction.outputs - .map( - (e) => ( - addresses: e.addresses, - amount: Amount( - rawValue: e.value, - fractionDigits: coin.fractionDigits, + data = + _transaction.outputs + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), ) - ), - ) - .toList(); + .toList(); } else if (_transaction.subType == TransactionSubType.cashFusion) { amount = _transaction.getAmountReceivedInThisWallet( fractionDigits: fractionDigits, ); - data = _transaction.outputs - .where((e) => e.walletOwns) - .map( - (e) => ( - addresses: e.addresses, - amount: Amount( - rawValue: e.value, - fractionDigits: coin.fractionDigits, - ) - ), - ) - .toList(); - } else { - switch (_transaction.type) { - case TransactionType.outgoing: - amount = _transaction.getAmountSentFromThisWallet( - fractionDigits: fractionDigits, - ); - data = _transaction.outputs - .where((e) => !e.walletOwns) + data = + _transaction.outputs + .where((e) => e.walletOwns) .map( (e) => ( addresses: e.addresses, amount: Amount( rawValue: e.value, fractionDigits: coin.fractionDigits, - ) + ), ), ) .toList(); + } else { + switch (_transaction.type) { + case TransactionType.outgoing: + amount = _transaction.getAmountSentFromThisWallet( + fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, + ); + data = + _transaction.outputs + .where((e) => !e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), + ) + .toList(); break; case TransactionType.incoming: case TransactionType.sentToSelf: if (_transaction.subType == TransactionSubType.sparkMint || _transaction.subType == TransactionSubType.sparkSpend) { - _sparkMemo = ref - .read(mainDBProvider) - .isar - .sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .memoIsNotEmpty() - .and() - .heightEqualTo(_transaction.height) - .anyOf( - _transaction.outputs - .where( - (e) => - e.walletOwns && - e.addresses.isEmpty && - e.scriptPubKeyHex.length >= 488, - ) - .map((e) => e.scriptPubKeyHex.substring(2, 488)) - .toList(), - (q, element) => q.serializedCoinB64StartsWith(element), - ) - .memoProperty() - .findFirstSync(); + _sparkMemo = + ref + .read(mainDBProvider) + .isar + .sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .memoIsNotEmpty() + .and() + .heightEqualTo(_transaction.height) + .anyOf( + _transaction.outputs + .where( + (e) => + e.walletOwns && + e.addresses.isEmpty && + e.scriptPubKeyHex.length >= 488, + ) + .map((e) => e.scriptPubKeyHex.substring(2, 488)) + .toList(), + (q, element) => q.serializedCoinB64StartsWith(element), + ) + .memoProperty() + .findFirstSync(); } if (_transaction.subType == TransactionSubType.sparkMint) { @@ -338,36 +347,39 @@ class _TransactionV2DetailsViewState fractionDigits: fractionDigits, ); } - data = _transaction.outputs - .where((e) => e.walletOwns) - .map( - (e) => ( - addresses: e.addresses, - amount: Amount( - rawValue: e.value, - fractionDigits: coin.fractionDigits, + data = + _transaction.outputs + .where((e) => e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), ) - ), - ) - .toList(); + .toList(); break; case TransactionType.unknown: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); - data = _transaction.inputs - .where((e) => e.walletOwns) - .map( - (e) => ( - addresses: e.addresses, - amount: Amount( - rawValue: e.value, - fractionDigits: coin.fractionDigits, + data = + _transaction.inputs + .where((e) => e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), ) - ), - ) - .toList(); + .toList(); break; } } @@ -381,24 +393,29 @@ class _TransactionV2DetailsViewState } String whatIsIt(TransactionV2 tx, int height) => tx.statusLabel( - currentChainHeight: height, - minConfirms: minConfirms, - minCoinbaseConfirms: ref + currentChainHeight: height, + minConfirms: minConfirms, + minCoinbaseConfirms: + ref .read(pWallets) .getWallet(walletId) .cryptoCurrency .minCoinbaseConfirms, - ); + ); Future fetchContactNameFor(String address) async { if (address.isEmpty) { return address; } try { - final contacts = ref.read(addressBookServiceProvider).contacts.where( - (element) => element.addresses - .where((element) => element.address == address) - .isNotEmpty, + final contacts = ref + .read(addressBookServiceProvider) + .contacts + .where( + (element) => + element.addresses + .where((element) => element.address == address) + .isNotEmpty, ); if (contacts.isNotEmpty) { return contacts.first.name; @@ -406,7 +423,7 @@ class _TransactionV2DetailsViewState return address; } } catch (e, s) { - Logging.instance.w("$e\n$s", error: e, stackTrace: s,); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); return address; } } @@ -427,8 +444,9 @@ class _TransactionV2DetailsViewState builder: (_, ref, __) { return Checkbox( value: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.hideBlockExplorerWarning), + prefsChangeNotifierProvider.select( + (value) => value.hideBlockExplorerWarning, + ), ), onChanged: (value) { if (value is bool) { @@ -454,23 +472,21 @@ class _TransactionV2DetailsViewState child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), onPressed: () { Navigator.of(context).pop(true); }, - child: Text( - "Continue", - style: STextStyles.button(context), - ), + child: Text("Continue", style: STextStyles.button(context)), ), ); } else { @@ -484,10 +500,7 @@ class _TransactionV2DetailsViewState Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Attention", - style: STextStyles.desktopH2(context), - ), + Text("Attention", style: STextStyles.desktopH2(context)), Row( children: [ Consumer( @@ -531,10 +544,7 @@ class _TransactionV2DetailsViewState buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(false); + Navigator.of(context, rootNavigator: true).pop(false); }, ), const SizedBox(width: 20), @@ -543,10 +553,7 @@ class _TransactionV2DetailsViewState buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(true); + Navigator.of(context, rootNavigator: true).pop(true); }, ), ], @@ -585,449 +592,492 @@ class _TransactionV2DetailsViewState coin.minCoinbaseConfirms, ); + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(_transaction.contractAddress!)?.value + : value.getPrice(coin)?.value, + ), + ); + } + return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: child, - ), + builder: (child) => Background(child: child), child: Scaffold( - backgroundColor: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.background, - appBar: isDesktop - ? null - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - // if (FocusScope.of(context).hasFocus) { - // FocusScope.of(context).unfocus(); - // await Future.delayed(Duration(milliseconds: 50)); - // } - Navigator.of(context).pop(); - }, - ), - title: Text( - "Transaction details", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: isDesktop - ? const EdgeInsets.only(left: 32) - : const EdgeInsets.all(12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (isDesktop) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction details", - style: STextStyles.desktopH3(context), - ), - const DesktopDialogCloseButton(), - ], - ), - Flexible( - child: Padding( - padding: isDesktop - ? const EdgeInsets.only( - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(0), - child: ConditionalParent( - condition: isDesktop, - builder: (child) { - return RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, - padding: const EdgeInsets.all(0), - child: child, - ); + backgroundColor: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.background, + appBar: + isDesktop + ? null + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); }, - child: SingleChildScrollView( - primary: isDesktop ? false : null, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(4), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - RoundedWhiteContainer( - padding: isDesktop + ), + title: Text( + "Transaction details", + style: STextStyles.navBarTitle(context), + ), + ), + body: ConditionalParent( + condition: !isDesktop, + builder: (child) => SafeArea(child: child), + child: Padding( + padding: + isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction details", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: Padding( + padding: + isDesktop + ? const EdgeInsets.only(right: 32, bottom: 32) + : const EdgeInsets.all(0), + child: ConditionalParent( + condition: isDesktop, + builder: (child) { + return RoundedWhiteContainer( + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, + padding: const EdgeInsets.all(0), + child: child, + ); + }, + child: SingleChildScrollView( + primary: isDesktop ? false : null, + child: Padding( + padding: + isDesktop ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), - child: Container( - decoration: isDesktop - ? BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - ), - ) - : null, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - if (isDesktop) - Row( - children: [ - TxIcon( - transaction: _transaction, - currentHeight: currentHeight, - coin: coin, - ), - const SizedBox( - width: 16, + : const EdgeInsets.all(4), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), + child: Container( + decoration: + isDesktop + ? BoxDecoration( + color: + Theme.of(context) + .extension()! + .backgroundAppBar, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants + .size + .circularBorderRadius, + ), ), - SelectableText( - whatIsIt( - _transaction, - currentHeight, + ) + : null, + child: Padding( + padding: + isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + if (isDesktop) + Row( + children: [ + TxIcon( + transaction: _transaction, + currentHeight: currentHeight, + coin: coin, ), - style: - STextStyles.desktopTextMedium( - context, + const SizedBox(width: 16), + SelectableText( + whatIsIt( + _transaction, + currentHeight, + ), + style: + STextStyles.desktopTextMedium( + context, + ), ), - ), - ], - ), - Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - SelectableText( - "$amountPrefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.titleBold12( - context, - ), - ), - const SizedBox( - height: 2, + ], ), - if (ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.externalCalls, - ), - )) + Column( + crossAxisAlignment: + isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ SelectableText( - "$amountPrefix${(amount.decimal * ref.watch( - priceAnd24hChangeNotifierProvider - .select( - (value) => value - .getPrice( - coin, - ) - .item1, - ), - )).toAmount(fractionDigits: 2).fiatString( - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale, + "$amountPrefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.titleBold12( + context, ), - ), - )} ${ref.watch( - prefsChangeNotifierProvider - .select( - (value) => value.currency, - ), - )}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), ), - ], - ), - if (!isDesktop) - TxIcon( - transaction: _transaction, - currentHeight: currentHeight, - coin: coin, + const SizedBox(height: 2), + if (price != null) + Builder( + builder: (context) { + final total = + (amount.decimal * price!) + .toAmount( + fractionDigits: 2, + ); + final formatted = total + .fiatString( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => + value + .locale, + ), + ), + ); + final ticker = ref.watch( + prefsChangeNotifierProvider + .select( + (value) => + value.currency, + ), + ); + return SelectableText( + "$amountPrefix$formatted $ticker", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ); + }, + ), + ], ), - ], + if (!isDesktop) + TxIcon( + transaction: _transaction, + currentHeight: currentHeight, + coin: coin, + ), + ], + ), ), ), ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Status", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - whatIsIt( - _transaction, - currentHeight, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: _transaction.type == - TransactionType - .outgoing && - _transaction.subType != - TransactionSubType - .cashFusion - ? Theme.of(context) - .extension()! - .accentColorOrange - : Theme.of(context) - .extension()! - .accentColorGreen, - ) - : STextStyles.itemSubtitle12(context), - ), - // ), - // ), - ], - ), - ), - if (!((coin is Monero || coin is Wownero) && - _transaction.type == - TransactionType.outgoing) && - !((coin is Firo) && - _transaction.subType == - TransactionSubType.mint)) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), - if (!((coin is Monero || coin is Wownero) && - _transaction.type == - TransactionType.outgoing) && - !((coin is Firo) && - _transaction.subType == - TransactionSubType.mint)) + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - ConditionalParent( - condition: kDebugMode, - builder: (child) { - return Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - child, - // CustomTextButton( - // text: "Info", - // onTap: () async { - // final adr = await ref - // .read(mainDBProvider) - // .getAddress(walletId, - // addresses.first); - // if (adr != null && - // mounted) { - // if (isDesktop) { - // await showDialog< - // void>( - // context: context, - // builder: (_) => - // DesktopDialog( - // maxHeight: double - // .infinity, - // child: - // AddressDetailsView( - // addressId: - // adr.id, - // walletId: widget - // .walletId, - // ), - // ), - // ); - // } else { - // await Navigator.of( - // context) - // .pushNamed( - // AddressDetailsView - // .routeName, - // arguments: Tuple2( - // adr.id, - // widget.walletId, - // ), - // ); - // } - // } - // }, - // ) - ], - ); - }, - child: Text( - outputLabel, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + Text( + "Status", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + whatIsIt(_transaction, currentHeight), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + _transaction.type == + TransactionType + .outgoing && + _transaction + .subType != + TransactionSubType + .cashFusion + ? Theme.of(context) + .extension< + StackColors + >()! + .accentColorOrange + : Theme.of(context) + .extension< + StackColors + >()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + // ), + // ), + ], + ), + ), + if (!((coin is Monero || coin is Wownero) && + _transaction.type == + TransactionType.outgoing) && + !((coin is Firo) && + _transaction.subType == + TransactionSubType.mint)) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (!((coin is Monero || coin is Wownero) && + _transaction.type == + TransactionType.outgoing) && + !((coin is Firo) && + _transaction.subType == + TransactionSubType.mint)) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: kDebugMode, + builder: (child) { + return Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + child, + // CustomTextButton( + // text: "Info", + // onTap: () async { + // final adr = await ref + // .read(mainDBProvider) + // .getAddress(walletId, + // addresses.first); + // if (adr != null && + // mounted) { + // if (isDesktop) { + // await showDialog< + // void>( + // context: context, + // builder: (_) => + // DesktopDialog( + // maxHeight: double + // .infinity, + // child: + // AddressDetailsView( + // addressId: + // adr.id, + // walletId: widget + // .walletId, + // ), + // ), + // ); + // } else { + // await Navigator.of( + // context) + // .pushNamed( + // AddressDetailsView + // .routeName, + // arguments: Tuple2( + // adr.id, + // widget.walletId, + // ), + // ); + // } + // } + // }, + // ) + ], + ); + }, + child: Text( + outputLabel, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), ), - ), - const SizedBox( - height: 8, - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (data.length == 1 && - data.first.addresses.length == - 1) - FutureBuilder( - future: fetchContactNameFor( - data.first.addresses.first, - ), - builder: ( - builderContext, - AsyncSnapshot - snapshot, - ) { - String - addressOrContactName = - data.first.addresses - .first; - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - addressOrContactName = - snapshot.data!; - } - return SelectableText( - addressOrContactName, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), - ); - }, - ) - else - for (int i = 0; - i < data.length; - i++) - ConditionalParent( - condition: i > 0, - builder: (child) => Column( - crossAxisAlignment: - CrossAxisAlignment - .stretch, - children: [ - const _Divider(), - child, - ], + const SizedBox(height: 8), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (data.length == 1 && + data + .first + .addresses + .length == + 1) + FutureBuilder( + future: fetchContactNameFor( + data + .first + .addresses + .first, ), - child: Padding( - padding: - const EdgeInsets.all( - 8.0, - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - ...data[i] + builder: ( + builderContext, + AsyncSnapshot + snapshot, + ) { + String + addressOrContactName = + data + .first .addresses - .map( - (e) { + .first; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ); + }, + ) + else + for ( + int i = 0; + i < data.length; + i++ + ) + ConditionalParent( + condition: i > 0, + builder: + (child) => Column( + crossAxisAlignment: + CrossAxisAlignment + .stretch, + children: [ + const _Divider(), + child, + ], + ), + child: Padding( + padding: + const EdgeInsets.all( + 8.0, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + ...data[i].addresses.map(( + e, + ) { return FutureBuilder( future: fetchContactNameFor( - e, - ), + e, + ), builder: ( builderContext, AsyncSnapshot< - String> - snapshot, + String + > + snapshot, ) { final String - addressOrContactName; + addressOrContactName; if (snapshot.connectionState == ConnectionState .done && @@ -1044,121 +1094,121 @@ class _TransactionV2DetailsViewState return OutputCard( address: addressOrContactName, - amount: data[ - i] - .amount, + amount: + data[i] + .amount, coin: coin, ); }, ); - }, - ), - ], + }), + ], + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), - ), - // if (isDesktop) - // IconCopyButton( - // data: addresses.first, - // ), - ], + // if (isDesktop) + // IconCopyButton( + // data: addresses.first, + // ), + ], + ), + ), + if (coin is Epiccash) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (coin is Epiccash) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "On chain note", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + const SizedBox(height: 8), + SelectableText( + _transaction.onChainNote ?? "", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + ), + if (isDesktop) + IconCopyButton( + data: _transaction.onChainNote ?? "", + ), + ], + ), ), - ), - if (coin is Epiccash) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin is Epiccash) + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "On chain note", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - const SizedBox( - height: 8, - ), - SelectableText( - _transaction.onChainNote ?? "", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + (coin is Epiccash) + ? "Local Note" + : "Note ", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, ) - : STextStyles.itemSubtitle12( + : STextStyles.itemSubtitle( context, ), - ), - ], - ), - ), - if (isDesktop) - IconCopyButton( - data: _transaction.onChainNote ?? "", - ), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - (coin is Epiccash) - ? "Local Note" - : "Note ", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), - ), - isDesktop - ? IconPencilButton( + ), + isDesktop + ? IconPencilButton( onPressed: () { showDialog( context: context, @@ -1175,7 +1225,7 @@ class _TransactionV2DetailsViewState ); }, ) - : GestureDetector( + : GestureDetector( onTap: () { Navigator.of(context).pushNamed( EditNoteView.routeName, @@ -1191,14 +1241,14 @@ class _TransactionV2DetailsViewState Assets.svg.pencil, width: 10, height: 10, - color: Theme.of(context) - .extension< - StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, + color: + Theme.of(context) + .extension< + StackColors + >()! + .infoItemIcons, ), + const SizedBox(width: 4), Text( "Edit", style: STextStyles.link2( @@ -1208,325 +1258,212 @@ class _TransactionV2DetailsViewState ], ), ), - ], - ), - const SizedBox( - height: 8, - ), - SelectableText( - ref - .watch( - pTransactionNote( - ( - txid: (coin is Epiccash) - ? _transaction.slateId - .toString() - : _transaction.txid, - walletId: walletId - ), - ), - ) - ?.value ?? - "", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - if (_sparkMemo != null) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (_sparkMemo != null) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - "Memo", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), SelectableText( - _sparkMemo!, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + ref + .watch( + pTransactionNote(( + txid: + (coin is Epiccash) + ? _transaction.slateId + .toString() + : _transaction.txid, + walletId: walletId, + )), + ) + ?.value ?? + "", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( + if (_sparkMemo != null) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (_sparkMemo != null) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Date", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - Format.extractDateFrom( - _transaction.timestamp, + Row( + children: [ + Text( + "Memo", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + ], + ), + const SizedBox(height: 8), + SelectableText( + _sparkMemo!, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles.itemSubtitle12( + : STextStyles.itemSubtitle12( context, ), - ), + ), ], ), - if (!isDesktop) - SelectableText( - Format.extractDateFrom( - _transaction.timestamp, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) - IconCopyButton( - data: Format.extractDateFrom( - _transaction.timestamp, - ), - ), - ], - ), - ), - if (coin is! NanoCurrency && - !(coin is Xelis && _transaction.type == TransactionType.incoming) - ) + ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin is! NanoCurrency && - !(coin is Xelis && _transaction.type == TransactionType.incoming) - ) + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Builder( - builder: (context) { - final String feeString = showFeePending - ? _transaction.isConfirmed( - currentHeight, - minConfirms, - coin.minCoinbaseConfirms, - ) - ? ref - .watch(pAmountFormatter(coin)) - .format( - fee, - ) - : "Pending" - : ref - .watch(pAmountFormatter(coin)) - .format( - fee, - ); - - return Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Transaction fee", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), - ), - if (supportsRbf && !confirmedTxn) - const SizedBox( - height: 8, - ), - if (supportsRbf && !confirmedTxn) - CustomTextButton( - text: "Boost transaction", - onTap: _boostPressed, - ), - ], - ), - if (!isDesktop) - SelectableText( - feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + Text( + "Date", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, ) - : STextStyles.itemSubtitle12( + : STextStyles.itemSubtitle( context, ), - ), + ), if (isDesktop) - IconCopyButton(data: feeString), + const SizedBox(height: 2), + if (isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), ], - ); - }, + ), + if (!isDesktop) + SelectableText( + Format.extractDateFrom( + _transaction.timestamp, + ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (isDesktop) + IconCopyButton( + data: Format.extractDateFrom( + _transaction.timestamp, + ), + ), + ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - Builder( - builder: (context) { - final String height; - final String confirmations; - final confirms = _transaction.getConfirmations( - currentHeight, - ); - - if (widget.coin is Bitcoincash || - widget.coin is Ecash) { - height = _transaction.height != null && - _transaction.height! > 0 - ? "${_transaction.height!}" - : "Pending"; - confirmations = confirms.toString(); - } else if (widget.coin is Epiccash && - _transaction.slateId == null) { - confirmations = "Unknown"; - height = "Unknown"; - } else { - final confirmed = _transaction.isConfirmed( - currentHeight, - minConfirms, - coin.minCoinbaseConfirms); - if (widget.coin is! Epiccash && confirmed) { - height = - "${_transaction.height == 0 ? "Unknown" : _transaction.height}"; - } else { - height = confirms > 0 - ? "${_transaction.height}" - : "Pending"; - } - - confirmations = confirms.toString(); - } - - return Column( - children: [ - RoundedWhiteContainer( - padding: isDesktop + if (coin is! NanoCurrency && + !(coin is Xelis && + _transaction.type == + TransactionType.incoming)) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (coin is! NanoCurrency && + !(coin is Xelis && + _transaction.type == + TransactionType.incoming)) + RoundedWhiteContainer( + padding: + isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), - child: Row( + child: Builder( + builder: (context) { + final String feeString = + showFeePending + ? _transaction.isConfirmed( + currentHeight, + minConfirms, + coin.minCoinbaseConfirms, + ) + ? ref + .watch( + pAmountFormatter(coin), + ) + .format(fee) + : "Pending" + : ref + .watch(pAmountFormatter(coin)) + .format(fee); + + return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: @@ -1537,606 +1474,950 @@ class _TransactionV2DetailsViewState CrossAxisAlignment.start, children: [ Text( - "Block height", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) - SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + "Transaction fee", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, - ).copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, ) - : STextStyles - .itemSubtitle12( + : STextStyles.itemSubtitle( context, ), + ), + if (isDesktop) + const SizedBox(height: 2), + if (isDesktop) + SelectableText( + feeString, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (supportsRbf && !confirmedTxn) + const SizedBox(height: 8), + if (supportsRbf && !confirmedTxn) + CustomTextButton( + text: "Boost transaction", + onTap: _boostPressed, ), ], ), if (!isDesktop) SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), - ), - if (isDesktop) - IconCopyButton(data: height), - ], - ), - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Confirmations", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + feeString, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles.itemSubtitle( + : STextStyles.itemSubtitle12( context, ), + ), + if (isDesktop) + IconCopyButton(data: feeString), + ], + ); + }, + ), + ), + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + Builder( + builder: (context) { + final String height; + final String confirmations; + final confirms = _transaction + .getConfirmations(currentHeight); + + if (widget.coin is Bitcoincash || + widget.coin is Ecash) { + height = + _transaction.height != null && + _transaction.height! > 0 + ? "${_transaction.height!}" + : "Pending"; + confirmations = confirms.toString(); + } else if (widget.coin is Epiccash && + _transaction.slateId == null) { + confirmations = "Unknown"; + height = "Unknown"; + } else { + final confirmed = _transaction.isConfirmed( + currentHeight, + minConfirms, + coin.minCoinbaseConfirms, + ); + if (widget.coin is! Epiccash && confirmed) { + height = + "${_transaction.height == 0 ? "Unknown" : _transaction.height}"; + } else { + height = + confirms > 0 + ? "${_transaction.height}" + : "Pending"; + } + + confirmations = confirms.toString(); + } + + return Column( + children: [ + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Block height", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + if (isDesktop) + const SizedBox(height: 2), + if (isDesktop) + SelectableText( + height, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + if (!isDesktop) + SelectableText( + height, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), - if (isDesktop) - const SizedBox( - height: 2, + if (isDesktop) + IconCopyButton(data: height), + ], + ), + ), + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Confirmations", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - if (isDesktop) - SelectableText( - confirmations, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + if (isDesktop) + const SizedBox(height: 2), + if (isDesktop) + SelectableText( + confirmations, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + if (!isDesktop) + SelectableText( + confirmations, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles - .itemSubtitle12( + : STextStyles.itemSubtitle12( context, ), + ), + if (isDesktop) + IconCopyButton(data: height), + ], + ), + ), + ], + ); + }, + ), + if (coin is Ethereum && + _transaction.type != TransactionType.incoming) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (coin is Ethereum && + _transaction.type != TransactionType.incoming) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Nonce", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, ), - ], - ), - if (!isDesktop) - SelectableText( - confirmations, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) + ), + SelectableText( + _transaction.nonce.toString(), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + ), + if (kDebugMode) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (kDebugMode) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Tx sub type", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + SelectableText( + _transaction.subType.toString(), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) .extension< - StackColors>()! + StackColors + >()! .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), - ), - if (isDesktop) - IconCopyButton(data: height), - ], + ) + : STextStyles.itemSubtitle12( + context, + ), ), - ), - ], - ); - }, - ), - - if (kDebugMode) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (kDebugMode) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Tx sub type", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), - ), - SelectableText( - _transaction.subType.toString(), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - ], + ], + ), + ), + if (hasTxKeyProbably) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (hasTxKeyProbably) + TxKeyWidget( + walletId: walletId, + txid: _transaction.txid, ), - ), - if (hasTxKeyProbably) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), - if (hasTxKeyProbably) - TxKeyWidget( - walletId: walletId, - txid: _transaction.txid, - ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( + : const SizedBox(height: 12), + + _transaction.txid.startsWith("mweb_outputId_") && + _transaction.subType == + TransactionSubType.mweb + ? RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - ConditionalParent( - condition: !isDesktop, - builder: (child) => Row( + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - Expanded(child: child), - SimpleCopyButton( - data: _transaction.txid, + ConditionalParent( + condition: !isDesktop, + builder: + (child) => Row( + children: [ + Expanded(child: child), + SimpleCopyButton( + data: _transaction + .txid + .replaceFirst( + "mweb_outputId_", + "", + ), + ), + ], + ), + child: Text( + "MWEB Output ID", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + ), + const SizedBox(height: 8), + SelectableText( + _transaction.txid.replaceFirst( + "mweb_outputId_", + "", + ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), + // if (coin is Litecoin && + // coin.network == + // CryptoCurrencyNetwork + // .main) + // const SizedBox(height: 8), + // if (coin is Litecoin && + // coin.network == + // CryptoCurrencyNetwork + // .main) + // CustomTextButton( + // text: + // "Open in block explorer", + // onTap: () async { + // final uri = + // getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction + // .txid + // .replaceFirst( + // "mweb_outputId_", + // "", + // ), + // ); + // + // if (ref + // .read( + // prefsChangeNotifierProvider, + // ) + // .hideBlockExplorerWarning == + // false) { + // final shouldContinue = + // await showExplorerWarning( + // "${uri.scheme}://${uri.host}", + // ); + // + // if (!shouldContinue) { + // return; + // } + // } + // try { + // await launchUrl( + // uri, + // mode: + // LaunchMode + // .externalApplication, + // ); + // } catch (_) { + // if (context.mounted) { + // unawaited( + // showDialog( + // context: context, + // builder: + // ( + // _, + // ) => StackOkDialog( + // title: + // "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // ), + // ); + // } + // } + // }, + // ), ], ), - child: Text( - "Transaction ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), ), - const SizedBox( - height: 8, - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.txid, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, + if (isDesktop) + const SizedBox(width: 12), + if (isDesktop) + IconCopyButton( + data: _transaction.txid + .replaceFirst( + "mweb_outputId_", + "", ), - ), - if (coin is! Epiccash) - const SizedBox( - height: 8, - ), - if (coin is! Epiccash) - CustomTextButton( - text: "Open in block explorer", - onTap: () async { - final uri = - getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); - - if (ref - .read( - prefsChangeNotifierProvider, - ) - .hideBlockExplorerWarning == - false) { - final shouldContinue = - await showExplorerWarning( - "${uri.scheme}://${uri.host}", - ); - - if (!shouldContinue) { - return; - } - } - - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - try { - await launchUrl( - uri, - mode: LaunchMode - .externalApplication, - ); - } catch (_) { - if (mounted) { - unawaited( - showDialog( - context: context, - builder: (_) => - StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), - ), - ); - } - } finally { - // Future.delayed( - // const Duration(seconds: 1), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - } - }, ), - // ), - // ), ], ), - ), - if (isDesktop) - const SizedBox( - width: 12, - ), - if (isDesktop) - IconCopyButton( - data: _transaction.txid, - ), - ], - ), - ), - // if ((coin is FiroTestNet || coin is Firo) && - // _transaction.subType == "mint") - // const SizedBox( - // height: 12, - // ), - // if ((coin is FiroTestNet || coin is Firo) && - // _transaction.subType == "mint") - // RoundedWhiteContainer( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "Mint Transaction ID", - // style: STextStyles.itemSubtitle(context), - // ), - // ], - // ), - // const SizedBox( - // height: 8, - // ), - // // Flexible( - // // child: FittedBox( - // // fit: BoxFit.scaleDown, - // // child: - // SelectableText( - // _transaction.otherData ?? "Unknown", - // style: STextStyles.itemSubtitle12(context), - // ), - // // ), - // // ), - // const SizedBox( - // height: 8, - // ), - // BlueTextButton( - // text: "Open in block explorer", - // onTap: () async { - // final uri = getBlockExplorerTransactionUrlFor( - // coin: coin, - // txid: _transaction.otherData ?? "Unknown", - // ); - // // ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = false; - // try { - // await launchUrl( - // uri, - // mode: LaunchMode.externalApplication, - // ); - // } catch (_) { - // unawaited(showDialog( - // context: context, - // builder: (_) => StackOkDialog( - // title: "Could not open in block explorer", - // message: - // "Failed to open \"${uri.toString()}\"", - // ), - // )); - // } finally { - // // Future.delayed( - // // const Duration(seconds: 1), - // // () => ref - // // .read( - // // shouldShowLockscreenOnResumeStateProvider - // // .state) - // // .state = true, - // // ); - // } - // }, - // ), - // ], - // ), - // ), - if (coin is Epiccash) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (coin is Epiccash) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Column( + ) + : RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - Text( - "Slate ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: !isDesktop, + builder: + (child) => Row( + children: [ + Expanded(child: child), + SimpleCopyButton( + data: + _transaction.txid, + ), + ], + ), + child: Text( + "Transaction ID", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - ), - // Flexible( - // child: FittedBox( - // fit: BoxFit.scaleDown, - // child: - SelectableText( - _transaction.slateId ?? "Unknown", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, + ), + const SizedBox(height: 8), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.txid, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (coin is! Epiccash) + const SizedBox(height: 8), + if (coin is! Epiccash) + CustomTextButton( + text: + "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: + _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider, + ) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}", + ); + + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: + LaunchMode + .externalApplication, + ); + } catch (_) { + if (context.mounted) { + unawaited( + showDialog( + context: context, + builder: + ( + _, + ) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + ), + ); + } + } finally { + // Future.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, ), + // ), + // ), + ], + ), ), - // ), - // ), + if (isDesktop) + const SizedBox(width: 12), + if (isDesktop) + IconCopyButton( + data: _transaction.txid, + ), ], ), - if (isDesktop) - const SizedBox( - width: 12, - ), - if (isDesktop) - IconCopyButton( - data: _transaction.slateId ?? "Unknown", + ), + // if ((coin is FiroTestNet || coin is Firo) && + // _transaction.subType == "mint") + // const SizedBox( + // height: 12, + // ), + // if ((coin is FiroTestNet || coin is Firo) && + // _transaction.subType == "mint") + // RoundedWhiteContainer( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Mint Transaction ID", + // style: STextStyles.itemSubtitle(context), + // ), + // ], + // ), + // const SizedBox( + // height: 8, + // ), + // // Flexible( + // // child: FittedBox( + // // fit: BoxFit.scaleDown, + // // child: + // SelectableText( + // _transaction.otherData ?? "Unknown", + // style: STextStyles.itemSubtitle12(context), + // ), + // // ), + // // ), + // const SizedBox( + // height: 8, + // ), + // BlueTextButton( + // text: "Open in block explorer", + // onTap: () async { + // final uri = getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction.otherData ?? "Unknown", + // ); + // // ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = false; + // try { + // await launchUrl( + // uri, + // mode: LaunchMode.externalApplication, + // ); + // } catch (_) { + // unawaited(showDialog( + // context: context, + // builder: (_) => StackOkDialog( + // title: "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // )); + // } finally { + // // Future.delayed( + // // const Duration(seconds: 1), + // // () => ref + // // .read( + // // shouldShowLockscreenOnResumeStateProvider + // // .state) + // // .state = true, + // // ); + // } + // }, + // ), + // ], + // ), + // ), + if (coin is Epiccash) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), + if (coin is Epiccash) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Slate ID", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: + SelectableText( + _transaction.slateId ?? "Unknown", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + // ), + // ), + ], ), - ], + if (isDesktop) const SizedBox(width: 12), + if (isDesktop) + IconCopyButton( + data: + _transaction.slateId ?? "Unknown", + ), + ], + ), ), - ), - if (!isDesktop) - const SizedBox( - height: 12, - ), - // if (whatIsIt( - // _transaction, - // currentHeight, - // ) != - // "Sending") - // isDesktop - // ? const _Divider() - // : const SizedBox( - // height: 12, - // ), - ], + if (!isDesktop) const SizedBox(height: 12), + // if (whatIsIt( + // _transaction, + // currentHeight, + // ) != + // "Sending") + // isDesktop + // ? const _Divider() + // : const SizedBox( + // height: 12, + // ), + ], + ), ), ), ), ), ), - ), - ], + ], + ), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: (coin is Epiccash && - _transaction.getConfirmations(currentHeight) < 1 && - _transaction.isCancelled == false) - ? ConditionalParent( - condition: isDesktop, - builder: (child) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - child: child, - ), - child: SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - Theme.of(context).extension()!.textError, + floatingActionButton: + (coin is Epiccash && + _transaction.getConfirmations(currentHeight) < 1 && + _transaction.isCancelled == false) + ? ConditionalParent( + condition: isDesktop, + builder: + (child) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: child, ), - ), - onPressed: () async { - final wallet = ref.read(pWallets).getWallet(walletId); + child: SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).extension()!.textError, + ), + ), + onPressed: () async { + final wallet = ref.read(pWallets).getWallet(walletId); + + if (wallet is EpiccashWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find Epic transaction ID", + context: context, + ), + ); + return; + } - if (wallet is EpiccashWallet) { - final String? id = _transaction.slateId; - if (id == null) { unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Could not find Epic transaction ID", + showDialog( + barrierDismissible: false, context: context, + builder: + (_) => + const CancellingTransactionProgressDialog(), ), ); - return; - } - - unawaited( - showDialog( - barrierDismissible: false, - context: context, - builder: (_) => - const CancellingTransactionProgressDialog(), - ), - ); - - final result = - await wallet.cancelPendingTransactionAndPost(id); - if (mounted) { - // pop progress dialog - Navigator.of(context).pop(); - if (result.isEmpty) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Transaction cancelled", - onOkPressed: (_) { - wallet.refresh(); - Navigator.of(context).popUntil( - ModalRoute.withName( - WalletView.routeName, + final result = await wallet + .cancelPendingTransactionAndPost(id); + if (context.mounted) { + // pop progress dialog + Navigator.of(context).pop(); + + if (result.isEmpty) { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Transaction cancelled", + onOkPressed: (_) { + wallet.refresh(); + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + }, ), - ); - }, - ), - ); - } else { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to cancel transaction", - message: result, - ), - ); + ); + } else { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + ), + ); + } } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "ERROR: Wallet type is not Epic Cash", + context: context, + ), + ); + return; } - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "ERROR: Wallet type is not Epic Cash", - context: context, - ), - ); - return; - } - }, - child: Text( - "Cancel Transaction", - style: STextStyles.button(context), + }, + child: Text( + "Cancel Transaction", + style: STextStyles.button(context), + ), ), ), - ), - ) - : null, + ) + : null, ), ); } @@ -2161,38 +2442,44 @@ class OutputCard extends ConsumerWidget { children: [ Text( "Address", - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), SelectableText( address, - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 10, + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ) + : STextStyles.itemSubtitle12(context), ), + const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Amount", - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), SelectableText( ref.watch(pAmountFormatter(coin)).format(amount), - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ) + : STextStyles.itemSubtitle12(context), ), ], ), @@ -2214,10 +2501,7 @@ class _Divider extends StatelessWidget { } class IconCopyButton extends StatelessWidget { - const IconCopyButton({ - super.key, - required this.data, - }); + const IconCopyButton({super.key, required this.data}); final String data; @@ -2231,9 +2515,7 @@ class IconCopyButton extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, elevation: 0, hoverElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), onPressed: () async { await Clipboard.setData(ClipboardData(text: data)); if (context.mounted) { @@ -2260,10 +2542,7 @@ class IconCopyButton extends StatelessWidget { } class IconPencilButton extends StatelessWidget { - const IconPencilButton({ - super.key, - this.onPressed, - }); + const IconPencilButton({super.key, this.onPressed}); final VoidCallback? onPressed; @@ -2277,9 +2556,7 @@ class IconPencilButton extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, elevation: 0, hoverElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), onPressed: () => onPressed?.call(), child: Padding( padding: const EdgeInsets.all(5), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 41422c7e0..a479ae4eb 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -22,7 +22,6 @@ import '../../app_config.dart'; import '../../frost_route_generator.dart'; import '../../models/isar/exchange_cache/currency.dart'; import '../../notifications/show_flush_bar.dart'; -import '../../pages_desktop_specific/lelantus_coins/lelantus_coins_view.dart'; import '../../pages_desktop_specific/spark_coins/spark_coins_view.dart'; import '../../providers/global/active_wallet_provider.dart'; import '../../providers/global/auto_swb_service_provider.dart'; @@ -54,8 +53,10 @@ import '../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; @@ -98,7 +99,7 @@ import '../send_view/frost_ms/frost_send_view.dart'; import '../send_view/send_view.dart'; import '../settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import '../settings_views/wallet_settings_view/wallet_settings_view.dart'; -import '../special/firo_rescan_recovery_error_dialog.dart'; +import '../spark_names/spark_names_home_view.dart'; import '../token_view/my_tokens_view.dart'; import 'sub_widgets/transactions_list.dart'; import 'sub_widgets/wallet_summary.dart'; @@ -143,36 +144,6 @@ class _WalletViewState extends ConsumerState { late StreamSubscription _nodeStatusSubscription; bool _rescanningOnOpen = false; - bool _lelantusRescanRecovery = false; - - Future _firoRescanRecovery() async { - final success = await (ref.read(pWallets).getWallet(walletId) as FiroWallet) - .firoRescanRecovery(); - - if (success) { - // go into wallet - WidgetsBinding.instance.addPostFrameCallback( - (_) => setState(() { - _rescanningOnOpen = false; - _lelantusRescanRecovery = false; - }), - ); - } else { - // show error message dialog w/ options - if (mounted) { - final shouldRetry = await Navigator.of(context).pushNamed( - FiroRescanRecoveryErrorView.routeName, - arguments: walletId, - ); - - if (shouldRetry is bool && shouldRetry) { - await _firoRescanRecovery(); - } - } else { - return await _firoRescanRecovery(); - } - } - } @override void initState() { @@ -194,13 +165,7 @@ class _WalletViewState extends ConsumerState { isSparkWallet = wallet is SparkInterface; - if (coin is Firo && (wallet as FiroWallet).lelantusCoinIsarRescanRequired) { - _rescanningOnOpen = true; - _lelantusRescanRecovery = true; - _firoRescanRecovery(); - } else { - wallet.refresh(); - } + wallet.refresh(); if (wallet.refreshMutex.isLocked) { _currentSyncStatus = WalletSyncStatus.syncing; @@ -218,41 +183,39 @@ class _WalletViewState extends ConsumerState { eventBus = widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - // switch (event.newStatus) { - // case WalletSyncStatus.unableToSync: - // break; - // case WalletSyncStatus.synced: - // break; - // case WalletSyncStatus.syncing: - // break; - // } - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); - - _nodeStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - // switch (event.newStatus) { - // case NodeConnectionStatus.disconnected: - // break; - // case NodeConnectionStatus.connected: - // break; - // } - setState(() { - _currentNodeStatus = event.newStatus; - }); - } - }, - ); + _syncStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case WalletSyncStatus.unableToSync: + // break; + // case WalletSyncStatus.synced: + // break; + // case WalletSyncStatus.syncing: + // break; + // } + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }); + + _nodeStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case NodeConnectionStatus.disconnected: + // break; + // case NodeConnectionStatus.connected: + // break; + // } + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }); super.initState(); } @@ -267,7 +230,7 @@ class _WalletViewState extends ConsumerState { // DateTime? _cachedTime; Future _onWillPop() async { - if (_rescanningOnOpen || _lelantusRescanRecovery) { + if (_rescanningOnOpen) { return false; } @@ -379,9 +342,7 @@ class _WalletViewState extends ConsumerState { callerRouteName: WalletView.routeName, ); - await Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - ); + await Navigator.of(context).pushNamed(FrostStepScaffold.routeName); } Future _onExchangePressed(BuildContext context) async { @@ -390,24 +351,28 @@ class _WalletViewState extends ConsumerState { if (coin.network.isTestNet) { await showDialog( context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for test net coins", - ), + builder: + (_) => const StackOkDialog( + title: "Exchange not available for test net coins", + ), ); } else { Future _future; + final isar = await ExchangeDataLoadingService.instance.isar; try { - _future = ExchangeDataLoadingService.instance.isar.currencies - .where() - .tickerEqualToAnyExchangeNameName(coin.ticker) - .findFirst(); + _future = + isar.currencies + .where() + .tickerEqualToAnyExchangeNameName(coin.ticker) + .findFirst(); } catch (_) { _future = ExchangeDataLoadingService.instance.loadAll().then( - (_) => ExchangeDataLoadingService.instance.isar.currencies + (_) => + isar.currencies .where() .tickerEqualToAnyExchangeNameName(coin.ticker) .findFirst(), - ); + ); } final currency = await showLoading( @@ -436,9 +401,10 @@ class _WalletViewState extends ConsumerState { if (coin.network.isTestNet) { await showDialog( context: context, - builder: (_) => const StackOkDialog( - title: "Buy not available for test net coins", - ), + builder: + (_) => const StackOkDialog( + title: "Buy not available for test net coins", + ), ); } else { if (mounted) { @@ -458,28 +424,29 @@ class _WalletViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Anonymizing balance", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: + (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Anonymizing balance", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); - final firoWallet = ref.read(pWallets).getWallet(walletId) as FiroWallet; + final wallet = ref.read(pWallets).getWallet(walletId); - final Amount publicBalance = firoWallet.info.cachedBalance.spendable; + final Amount publicBalance = wallet.info.cachedBalance.spendable; if (publicBalance <= Amount.zero) { shouldPop = true; if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(WalletView.routeName)); unawaited( showFloatingFlushBar( type: FlushBarType.info, - message: "No funds available to anonymize!", + message: "No funds available to privatize!", context: context, ), ); @@ -488,17 +455,20 @@ class _WalletViewState extends ConsumerState { } try { - // await firoWallet.anonymizeAllLelantus(); - await firoWallet.anonymizeAllSpark(); + if (wallet is MwebInterface && wallet.info.isMwebEnabled) { + await wallet.anonymizeAllMweb(); + } else { + await (wallet as FiroWallet).anonymizeAllSpark(); + } shouldPop = true; if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(WalletView.routeName)); unawaited( showFloatingFlushBar( type: FlushBarType.success, - message: "Anonymize transaction submitted", + message: "Privatize transaction submitted", context: context, ), ); @@ -506,15 +476,16 @@ class _WalletViewState extends ConsumerState { } catch (e) { shouldPop = true; if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(WalletView.routeName)); await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Anonymize all failed", - message: "Reason: $e", - ), + builder: + (_) => StackOkDialog( + title: "Privatize all failed", + message: "Reason: $e", + ), ); } } @@ -549,37 +520,41 @@ class _WalletViewState extends ConsumerState { eventBus: null, textColor: Theme.of(context).extension()!.textDark, - actionButton: _lelantusRescanRecovery - ? null - : SecondaryButton( - label: "Cancel", - onPressed: () async { - await showDialog( - context: context, - builder: (context) => StackDialog( - title: "Warning!", - message: "Skipping this process can completely" - " break your wallet. It is only meant to be done in" - " emergency situations where the migration fails" - " and will not let you continue. Still skip?", - leftButton: SecondaryButton( - label: "Cancel", - onPressed: - Navigator.of(context, rootNavigator: true) - .pop, - ), - rightButton: SecondaryButton( - label: "Ok", - onPressed: () { - Navigator.of(context, rootNavigator: true) - .pop(); - setState(() => _rescanningOnOpen = false); - }, - ), + actionButton: SecondaryButton( + label: "Cancel", + onPressed: () async { + await showDialog( + context: context, + builder: + (context) => StackDialog( + title: "Warning!", + message: + "Skipping this process can completely" + " break your wallet. It is only meant to be done in" + " emergency situations where the migration fails" + " and will not let you continue. Still skip?", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: + Navigator.of( + context, + rootNavigator: true, + ).pop, ), - ); - }, - ), + rightButton: SecondaryButton( + label: "Ok", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + setState(() => _rescanningOnOpen = false); + }, + ), + ), + ); + }, + ), ), ), ], @@ -605,15 +580,11 @@ class _WalletViewState extends ConsumerState { title: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: Text( ref.watch(pWalletName(walletId)), @@ -625,15 +596,8 @@ class _WalletViewState extends ConsumerState { ), actions: [ const Padding( - padding: EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: SmallTorIcon(), - ), + padding: EdgeInsets.only(top: 10, bottom: 10, right: 10), + child: AspectRatio(aspectRatio: 1, child: SmallTorIcon()), ), Padding( padding: const EdgeInsets.only( @@ -649,9 +613,10 @@ class _WalletViewState extends ConsumerState { key: const Key("walletViewRadioButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: _buildNetworkIcon(_currentSyncStatus), onPressed: () { Navigator.of(context).pushNamed( @@ -680,91 +645,105 @@ class _WalletViewState extends ConsumerState { key: const Key("walletViewAlertsButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: ref.watch( - notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor(walletId), - ), - ) - ? SvgPicture.file( - File( - ref.watch( - themeProvider.select( - (value) => value.assets.bellNew, - ), - ), - ), - width: 20, - height: 20, - color: ref.watch( + color: + Theme.of( + context, + ).extension()!.background, + icon: + ref.watch( notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor( - walletId, - ), + (value) => value + .hasUnreadNotificationsFor(walletId), ), ) - ? null - : Theme.of(context) - .extension()! - .topNavIconPrimary, - ) - : SvgPicture.asset( - Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch( - notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor( - walletId, + ? SvgPicture.file( + File( + ref.watch( + themeProvider.select( + (value) => value.assets.bellNew, + ), ), ), + width: 20, + height: 20, + color: + ref.watch( + notificationsProvider.select( + (value) => value + .hasUnreadNotificationsFor( + walletId, + ), + ), + ) + ? null + : Theme.of(context) + .extension()! + .topNavIconPrimary, ) - ? null - : Theme.of(context) - .extension()! - .topNavIconPrimary, - ), + : SvgPicture.asset( + Assets.svg.bell, + width: 20, + height: 20, + color: + ref.watch( + notificationsProvider.select( + (value) => value + .hasUnreadNotificationsFor( + walletId, + ), + ), + ) + ? null + : Theme.of(context) + .extension()! + .topNavIconPrimary, + ), onPressed: () { // reset unread state ref.refresh(unreadNotificationsStateProvider); Navigator.of(context) .pushNamed( - NotificationsView.routeName, - arguments: walletId, - ) + NotificationsView.routeName, + arguments: walletId, + ) .then((_) { - final Set unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - final List> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { - futures.add( - ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.elementAt(i), - false, - ), - ); - } - - // wait for multiple to update if any - Future.wait(futures).then((_) { - // only notify listeners once - ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.last, - true, + final Set unreadNotificationIds = + ref + .read( + unreadNotificationsStateProvider + .state, + ) + .state; + if (unreadNotificationIds.isEmpty) return; + + final List> futures = []; + for ( + int i = 0; + i < unreadNotificationIds.length - 1; + i++ + ) { + futures.add( + ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.elementAt(i), + false, + ), ); - }); - }); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.last, + true, + ); + }); + }); }, ), ), @@ -783,14 +762,16 @@ class _WalletViewState extends ConsumerState { key: const Key("walletViewSettingsButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.bars, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -818,30 +799,29 @@ class _WalletViewState extends ConsumerState { Theme.of(context).extension()!.background, child: Column( children: [ - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: WalletSummary( walletId: walletId, aspectRatio: 1.75, - initialSyncStatus: ref - .watch(pWallets) - .getWallet(walletId) - .refreshMutex - .isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, + initialSyncStatus: + ref + .watch(pWallets) + .getWallet(walletId) + .refreshMutex + .isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, ), ), ), - if (isSparkWallet) - const SizedBox( - height: 10, - ), - if (isSparkWallet) + if (isSparkWallet || + ref.watch(pWalletInfo(walletId)).isMwebEnabled) + const SizedBox(height: 10), + if (isSparkWallet || + ref.watch(pWalletInfo(walletId)).isMwebEnabled) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( @@ -856,51 +836,59 @@ class _WalletViewState extends ConsumerState { onPressed: () async { await showDialog( context: context, - builder: (context) => StackDialog( - title: "Attention!", - message: - "You're about to anonymize all of your public funds.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + builder: + (context) => StackDialog( + title: "Attention!", + message: + "You're about to privatize all of your public funds.", + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + "Cancel", + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), + ), ), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(); + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(); - unawaited(attemptAnonymize()); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle( - context, + unawaited(attemptAnonymize()); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle( + context, + ), + child: Text( + "Continue", + style: STextStyles.button( + context, + ), ), - child: Text( - "Continue", - style: - STextStyles.button(context), + ), ), - ), - ), ); }, child: Text( - "Anonymize funds", - style: - STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + "Privatize funds", + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), ), ), @@ -908,9 +896,7 @@ class _WalletViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( @@ -918,11 +904,13 @@ class _WalletViewState extends ConsumerState { children: [ Text( "Transactions", - style: - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + style: STextStyles.itemSubtitle( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, ), ), CustomTextButton( @@ -943,9 +931,7 @@ class _WalletViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -970,11 +956,7 @@ class _WalletViewState extends ConsumerState { Colors.transparent, Colors.white, ], - stops: [ - 0.0, - 0.8, - 1.0, - ], + stops: [0.0, 0.8, 1.0], ).createShader(bounds); }, child: Container( @@ -989,17 +971,20 @@ class _WalletViewState extends ConsumerState { CrossAxisAlignment.stretch, children: [ Expanded( - child: ref - .read(pWallets) - .getWallet(widget.walletId) - .isarTransactionVersion == - 2 - ? TransactionsV2List( - walletId: widget.walletId, - ) - : TransactionsList( - walletId: walletId, - ), + child: + ref + .read(pWallets) + .getWallet( + widget.walletId, + ) + .isarTransactionVersion == + 2 + ? TransactionsV2List( + walletId: widget.walletId, + ) + : TransactionsList( + walletId: walletId, + ), ), ], ), @@ -1013,271 +998,273 @@ class _WalletViewState extends ConsumerState { ), ), ), - WalletNavigationBar( - items: [ - WalletNavigationBarItemData( - label: "Receive", - icon: const ReceiveNavIcon(), - onTap: () { - if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - ReceiveView.routeName, - arguments: walletId, - ), - ); - } - }, - ), - if (ref.watch(pWalletCoin(walletId)) is FrostCurrency) - WalletNavigationBarItemData( - label: "Sign", - icon: const FrostSignNavIcon(), - onTap: () => _onFrostSignPressed(context), - ), - if (!viewOnly) - WalletNavigationBarItemData( - label: "Send", - icon: const SendNavIcon(), - onTap: () { - // not sure what this is supposed to accomplish? - // switch (ref - // .read(walletBalanceToggleStateProvider.state) - // .state) { - // case WalletBalanceToggleState.full: - // ref - // .read(publicPrivateBalanceStateProvider.state) - // .state = "Public"; - // break; - // case WalletBalanceToggleState.available: - // ref - // .read(publicPrivateBalanceStateProvider.state) - // .state = "Private"; - // break; - // } - Navigator.of(context).pushNamed( - wallet is BitcoinFrostWallet - ? FrostSendView.routeName - : SendView.routeName, - arguments: ( - walletId: walletId, - coin: coin, - ), - ); - }, - ), - if (!viewOnly && - Constants.enableExchange && - ref.watch(pWalletCoin(walletId)) is! FrostCurrency && - AppConfig.hasFeature(AppFeature.swap) && - showExchange) - WalletNavigationBarItemData( - label: "Swap", - icon: const ExchangeNavIcon(), - onTap: () => _onExchangePressed(context), - ), - if (Constants.enableExchange && - ref.watch(pWalletCoin(walletId)) is! FrostCurrency && - AppConfig.hasFeature(AppFeature.buy) && - showExchange) - WalletNavigationBarItemData( - label: "Buy", - icon: const BuyNavIcon(), - onTap: () => _onBuyPressed(context), - ), - ], - moreItems: [ - if (ref.watch( - pWallets.select( - (value) => value - .getWallet(widget.walletId) - .cryptoCurrency - .hasTokenSupport, - ), - )) + SafeArea( + child: WalletNavigationBar( + items: [ WalletNavigationBarItemData( - label: "Tokens", - icon: const CoinControlNavIcon(), + label: "Receive", + icon: const ReceiveNavIcon(), onTap: () { - Navigator.of(context).pushNamed( - MyTokensView.routeName, - arguments: walletId, - ); + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + ReceiveView.routeName, + arguments: walletId, + ), + ); + } }, ), - if (coin is Banano) - WalletNavigationBarItemData( - icon: SvgPicture.asset( - Assets.svg.monkey, - height: 20, - width: 20, - color: Theme.of(context) - .extension()! - .bottomNavIconIcon, + if (ref.watch(pWalletCoin(walletId)) is FrostCurrency) + WalletNavigationBarItemData( + label: "Sign", + icon: const FrostSignNavIcon(), + onTap: () => _onFrostSignPressed(context), ), - label: "MonKey", - onTap: () { - Navigator.of(context).pushNamed( - MonkeyView.routeName, - arguments: widget.walletId, - ); - }, - ), - if (wallet is CoinControlInterface && - ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.enableCoinControl, + if (!viewOnly) + WalletNavigationBarItemData( + label: "Send", + icon: const SendNavIcon(), + onTap: () { + // not sure what this is supposed to accomplish? + // switch (ref + // .read(walletBalanceToggleStateProvider.state) + // .state) { + // case WalletBalanceToggleState.full: + // ref + // .read(publicPrivateBalanceStateProvider.state) + // .state = "Public"; + // break; + // case WalletBalanceToggleState.available: + // ref + // .read(publicPrivateBalanceStateProvider.state) + // .state = "Private"; + // break; + // } + Navigator.of(context).pushNamed( + wallet is BitcoinFrostWallet + ? FrostSendView.routeName + : SendView.routeName, + arguments: (walletId: walletId, coin: coin), + ); + }, + ), + if (!viewOnly && + Constants.enableExchange && + ref.watch(pWalletCoin(walletId)) is! FrostCurrency && + AppConfig.hasFeature(AppFeature.swap) && + showExchange) + WalletNavigationBarItemData( + label: "Swap", + icon: const ExchangeNavIcon(), + onTap: () => _onExchangePressed(context), + ), + if (Constants.enableExchange && + ref.watch(pWalletCoin(walletId)) is! FrostCurrency && + wallet is! FiroWallet && + AppConfig.hasFeature(AppFeature.buy) && + showExchange) + WalletNavigationBarItemData( + label: "Buy", + icon: const BuyNavIcon(), + onTap: () => _onBuyPressed(context), + ), + if (wallet is SparkInterface) + WalletNavigationBarItemData( + label: "Names", + icon: const PaynymNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + SparkNamesHomeView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + moreItems: [ + if (ref.watch( + pWallets.select( + (value) => + value + .getWallet(widget.walletId) + .cryptoCurrency + .hasTokenSupport, + ), + )) + WalletNavigationBarItemData( + label: "Tokens", + icon: const CoinControlNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + MyTokensView.routeName, + arguments: walletId, + ); + }, + ), + if (coin is Banano) + WalletNavigationBarItemData( + icon: SvgPicture.asset( + Assets.svg.monkey, + height: 20, + width: 20, + color: + Theme.of( + context, + ).extension()!.bottomNavIconIcon, ), - )) - WalletNavigationBarItemData( - label: "Coin control", - icon: const CoinControlNavIcon(), - onTap: () { - Navigator.of(context).pushNamed( - CoinControlView.routeName, - arguments: Tuple2( - widget.walletId, - CoinControlViewType.manage, + label: "MonKey", + onTap: () { + Navigator.of(context).pushNamed( + MonkeyView.routeName, + arguments: widget.walletId, + ); + }, + ), + if (wallet is CoinControlInterface && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, ), - ); - }, - ), - if (wallet is FiroWallet && - ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.advancedFiroFeatures, - ), - )) - WalletNavigationBarItemData( - label: "Lelantus coins", - icon: const CoinControlNavIcon(), - onTap: () { - Navigator.of(context).pushNamed( - LelantusCoinsView.routeName, - arguments: widget.walletId, - ); - }, - ), - if (wallet is FiroWallet && - ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.advancedFiroFeatures, - ), - )) - WalletNavigationBarItemData( - label: "Spark coins", - icon: const CoinControlNavIcon(), - onTap: () { - Navigator.of(context).pushNamed( - SparkCoinsView.routeName, - arguments: widget.walletId, - ); - }, - ), - if (wallet is NamecoinWallet) - WalletNavigationBarItemData( - label: "Domains", - icon: const PaynymNavIcon(), - onTap: () { - Navigator.of(context).pushNamed( - NamecoinNamesHomeView.routeName, - arguments: widget.walletId, - ); - }, - ), - if (!viewOnly && wallet is PaynymInterface) - WalletNavigationBarItemData( - label: "PayNym", - icon: const PaynymNavIcon(), - onTap: () async { - unawaited( - showDialog( - context: context, - builder: (context) => const LoadingIndicator( - width: 100, + )) + WalletNavigationBarItemData( + label: "Coin control", + icon: const CoinControlNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + CoinControlView.routeName, + arguments: Tuple2( + widget.walletId, + CoinControlViewType.manage, ), + ); + }, + ), + if (wallet is FiroWallet && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.advancedFiroFeatures, ), - ); - - final wallet = - ref.read(pWallets).getWallet(widget.walletId); - - final paynymInterface = wallet as PaynymInterface; - - final code = await paynymInterface.getPaymentCode( - isSegwit: false, - ); - - final account = await ref - .read(paynymAPIProvider) - .nym(code.toString()); - - Logging.instance.d("my nym account: $account"); - - if (context.mounted) { - Navigator.of(context).pop(); - - // check if account exists and for matching code to see if claimed - if (account.value != null && - account.value!.nonSegwitPaymentCode.claimed - // && - // account.value!.segwit - ) { - ref.read(myPaynymAccountStateProvider.state).state = - account.value!; - - await Navigator.of(context).pushNamed( - PaynymHomeView.routeName, - arguments: widget.walletId, - ); - } else { - await Navigator.of(context).pushNamed( - PaynymClaimView.routeName, - arguments: widget.walletId, - ); + )) + WalletNavigationBarItemData( + label: "Spark coins", + icon: const CoinControlNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + SparkCoinsView.routeName, + arguments: widget.walletId, + ); + }, + ), + if (wallet is NamecoinWallet) + WalletNavigationBarItemData( + label: "Domains", + icon: const PaynymNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + NamecoinNamesHomeView.routeName, + arguments: widget.walletId, + ); + }, + ), + if (!viewOnly && wallet is PaynymInterface) + WalletNavigationBarItemData( + label: "PayNym", + icon: const PaynymNavIcon(), + onTap: () async { + unawaited( + showDialog( + context: context, + builder: + (context) => + const LoadingIndicator(width: 100), + ), + ); + + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId); + + final paynymInterface = wallet as PaynymInterface; + + final code = await paynymInterface.getPaymentCode( + isSegwit: false, + ); + + final account = await ref + .read(paynymAPIProvider) + .nym(code.toString()); + + Logging.instance.d("my nym account: $account"); + + if (context.mounted) { + Navigator.of(context).pop(); + + // check if account exists and for matching code to see if claimed + if (account.value != null && + account.value!.nonSegwitPaymentCode.claimed + // && + // account.value!.segwit + ) { + ref + .read(myPaynymAccountStateProvider.state) + .state = account.value!; + + await Navigator.of(context).pushNamed( + PaynymHomeView.routeName, + arguments: widget.walletId, + ); + } else { + await Navigator.of(context).pushNamed( + PaynymClaimView.routeName, + arguments: widget.walletId, + ); + } } - } - }, - ), - if (ref.watch( - pWallets.select( - (value) => - value.getWallet(widget.walletId) is OrdinalsInterface, - ), - )) - WalletNavigationBarItemData( - label: "Ordinals", - icon: const OrdinalsNavIcon(), - onTap: () { - Navigator.of(context).pushNamed( - OrdinalsView.routeName, - arguments: widget.walletId, - ); - }, - ), - if (wallet is CashFusionInterface && !viewOnly) - WalletNavigationBarItemData( - label: "Fusion", - icon: const FusionNavIcon(), - onTap: () { - Navigator.of(context).pushNamed( - CashFusionView.routeName, - arguments: walletId, - ); - }, - ), - if (wallet is LibMoneroWallet && !viewOnly) - WalletNavigationBarItemData( - label: "Churn", - icon: const ChurnNavIcon(), - onTap: () { - Navigator.of(context).pushNamed( - ChurningView.routeName, - arguments: walletId, - ); - }, - ), - ], + }, + ), + if (ref.watch( + pWallets.select( + (value) => + value.getWallet(widget.walletId) + is OrdinalsInterface, + ), + )) + WalletNavigationBarItemData( + label: "Ordinals", + icon: const OrdinalsNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + OrdinalsView.routeName, + arguments: widget.walletId, + ); + }, + ), + if (wallet is CashFusionInterface && !viewOnly) + WalletNavigationBarItemData( + label: "Fusion", + icon: const FusionNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + CashFusionView.routeName, + arguments: walletId, + ); + }, + ), + if ((wallet is LibMoneroWallet || + wallet is LibSalviumWallet) && + !viewOnly) + WalletNavigationBarItemData( + label: "Churn", + icon: const ChurnNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + ChurningView.routeName, + arguments: walletId, + ); + }, + ), + ], + ), ), ], ), diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index d9f730804..aad2dc5db 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -10,6 +10,7 @@ import 'dart:io'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -67,53 +68,63 @@ class _FavoriteCardState extends ConsumerState { prefsChangeNotifierProvider.select((value) => value.externalCalls), ); + Decimal? price; + if (externalCalls) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin)?.value, + ), + ); + } return ConditionalParent( condition: Util.isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) { - setState(() { - _hovering = true; - }); - }, - onExit: (_) { - setState(() { - _hovering = false; - }); - }, - child: AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: _hovering ? 1.05 : 1, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: _hovering - ? BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - boxShadow: [ - Theme.of(context) - .extension()! - .standardBoxShadow, - Theme.of(context) - .extension()! - .standardBoxShadow, - Theme.of(context) - .extension()! - .standardBoxShadow, - ], - ) - : BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: child, + builder: + (child) => MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: _hovering ? 1.05 : 1, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: + _hovering + ? BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + boxShadow: [ + Theme.of( + context, + ).extension()!.standardBoxShadow, + Theme.of( + context, + ).extension()!.standardBoxShadow, + Theme.of( + context, + ).extension()!.standardBoxShadow, + ], + ) + : BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: child, + ), + ), ), - ), - ), child: GestureDetector( onTap: () async { final wallet = ref.read(pWallets).getWallet(walletId); @@ -133,8 +144,9 @@ class _FavoriteCardState extends ConsumerState { final Future loadFuture; if (wallet is ExternalWallet) { - loadFuture = - wallet.init().then((value) async => await (wallet).open()); + loadFuture = wallet.init().then( + (value) async => await (wallet).open(), + ); } else { loadFuture = wallet.init(); } @@ -147,15 +159,13 @@ class _FavoriteCardState extends ConsumerState { if (mounted) { if (Util.isDesktop) { - await Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: walletId, - ); + await Navigator.of( + context, + ).pushNamed(DesktopWalletView.routeName, arguments: walletId); } else { - await Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: walletId, - ); + await Navigator.of( + context, + ).pushNamed(WalletView.routeName, arguments: walletId); } } }, @@ -183,17 +193,16 @@ class _FavoriteCardState extends ConsumerState { child: Text( ref.watch(pWalletName(walletId)), style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, ), overflow: TextOverflow.fade, ), ), SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), @@ -202,36 +211,29 @@ class _FavoriteCardState extends ConsumerState { ), Builder( builder: (context) { - final balance = ref.watch( - pWalletBalance(walletId), - ); + final balance = ref.watch(pWalletBalance(walletId)); Amount total = balance.total; if (coin is Firo) { - total += ref - .watch( - pWalletBalanceSecondary(walletId), - ) - .total; - total += ref - .watch( - pWalletBalanceTertiary(walletId), - ) - .total; + total += + ref.watch(pWalletBalanceSecondary(walletId)).total; + total += + ref.watch(pWalletBalanceTertiary(walletId)).total; + } else if (ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + )) { + total += + ref.watch(pWalletBalanceSecondary(walletId)).total; } Amount fiatTotal = Amount.zero; - if (externalCalls && total > Amount.zero) { - fiatTotal = (total.decimal * - ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ) - .item1) - .toAmount(fractionDigits: 2); + if (externalCalls && + total > Amount.zero && + price != null) { + fiatTotal = (total.decimal * price).toAmount( + fractionDigits: 2, + ); } return Column( @@ -243,35 +245,26 @@ class _FavoriteCardState extends ConsumerState { ref.watch(pAmountFormatter(coin)).format(total), style: STextStyles.titleBold12(context).copyWith( fontSize: 16, - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of(context) + .extension()! + .textFavoriteCard, ), ), ), - if (externalCalls) - const SizedBox( - height: 4, - ), - if (externalCalls) + if (externalCalls && price != null) + const SizedBox(height: 4), + if (externalCalls && price != null) Text( - "${fiatTotal.fiatString( - locale: ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), - ), - )} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: - STextStyles.itemSubtitle12(context).copyWith( + "${fiatTotal.fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.itemSubtitle12( + context, + ).copyWith( fontSize: 10, - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of(context) + .extension()! + .textFavoriteCard, ), ), ], @@ -300,11 +293,6 @@ class CardOverlayStack extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - background, - child, - ], - ); + return Stack(children: [background, child]); } } 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 938baac71..64d9ecbc9 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -46,8 +46,9 @@ class WalletListItem extends ConsumerWidget { // debugPrint("BUILD: $runtimeType"); final walletCountString = walletCount == 1 ? "$walletCount wallet" : "$walletCount wallets"; - final currency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final currency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); return RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -57,8 +58,9 @@ class WalletListItem extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), onPressed: () async { // Check if Tor is enabled... @@ -66,11 +68,10 @@ class WalletListItem extends ConsumerWidget { // ... and if the coin supports Tor. if (!coin.torSupport) { // If not, show a Tor warning dialog. - final shouldContinue = await showDialog( + final shouldContinue = + await showDialog( context: context, - builder: (_) => TorWarningDialog( - coin: coin, - ), + builder: (_) => TorWarningDialog(coin: coin), ) ?? false; if (!shouldContinue) { @@ -100,8 +101,9 @@ class WalletListItem extends ConsumerWidget { final Future loadFuture; if (wallet is ExternalWallet) { - loadFuture = - wallet.init().then((value) async => await (wallet).open()); + loadFuture = wallet.init().then( + (value) async => await (wallet).open(), + ); } else { loadFuture = wallet.init(); } @@ -113,63 +115,67 @@ class WalletListItem extends ConsumerWidget { ); if (context.mounted) { unawaited( - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: wallet.walletId, - ), + Navigator.of( + context, + ).pushNamed(WalletView.routeName, arguments: wallet.walletId), ); } } else { unawaited( - Navigator.of(context).pushNamed( - WalletsOverview.routeName, - arguments: coin, - ), + Navigator.of( + context, + ).pushNamed(WalletsOverview.routeName, arguments: coin), ); } }, child: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 28, height: 28, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Expanded( child: Consumer( builder: (_, ref, __) { - final tuple = ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin)), - ); - final calls = - ref.watch(prefsChangeNotifierProvider).externalCalls; + Color percentChangedColor = + Theme.of(context).extension()!.textDark; + String? priceString; + double? percentChange; + if (ref.watch( + prefsChangeNotifierProvider.select((s) => s.externalCalls), + )) { + final price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ); - final priceString = - tuple.item1.toAmount(fractionDigits: 2).fiatString( + if (price != null) { + priceString = price.value + .toAmount(fractionDigits: 2) + .fiatString( locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), ), ); + percentChange = price.change24h; - final double percentChange = tuple.item2; - - var percentChangedColor = - Theme.of(context).extension()!.textDark; - if (percentChange > 0) { - percentChangedColor = Theme.of(context) - .extension()! - .accentColorGreen; - } else if (percentChange < 0) { - percentChangedColor = Theme.of(context) - .extension()! - .accentColorRed; + if (percentChange > 0) { + percentChangedColor = + Theme.of( + context, + ).extension()!.accentColorGreen; + } else if (percentChange < 0) { + percentChangedColor = + Theme.of( + context, + ).extension()!.accentColorRed; + } + } } return Column( @@ -182,16 +188,14 @@ class WalletListItem extends ConsumerWidget { style: STextStyles.titleBold12(context), ), const Spacer(), - if (calls) + if (priceString != null) Text( "$priceString $currency/${coin.ticker}", style: STextStyles.itemSubtitle(context), ), ], ), - const SizedBox( - height: 1, - ), + const SizedBox(height: 1), Row( children: [ Text( @@ -199,12 +203,12 @@ class WalletListItem extends ConsumerWidget { style: STextStyles.itemSubtitle(context), ), const Spacer(), - if (calls) + if (percentChange != null) Text( "${percentChange.toStringAsFixed(2)}%", - style: STextStyles.itemSubtitle(context).copyWith( - color: percentChangedColor, - ), + style: STextStyles.itemSubtitle( + context, + ).copyWith(color: percentChangedColor), ), ], ), diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart index c66fb387d..a5e0ba924 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -16,6 +16,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import '../../db/isar/main_db.dart'; +import '../../models/input.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; @@ -41,6 +42,9 @@ import '../../widgets/toggle.dart'; import 'utxo_row.dart'; final desktopUseUTXOs = StateProvider((ref) => {}); +final pDesktopUseUTXOs = Provider( + (ref) => ref.watch(desktopUseUTXOs).map((e) => StandardInput(e)).toSet(), +); class DesktopCoinControlUseDialog extends ConsumerStatefulWidget { const DesktopCoinControlUseDialog({ @@ -124,21 +128,22 @@ class _DesktopCoinControlUseDialogState ); } - final Amount selectedSum = _selectedUTXOs.map((e) => e.value).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: coin.fractionDigits, - ), - (value, element) => value += Amount( - rawValue: BigInt.from(element), - fractionDigits: coin.fractionDigits, - ), + final Amount selectedSum = _selectedUTXOs + .map((e) => e.value) + .fold( + Amount(rawValue: BigInt.zero, fractionDigits: coin.fractionDigits), + (value, element) => + value += Amount( + rawValue: BigInt.from(element), + fractionDigits: coin.fractionDigits, + ), ); - final enableApply = widget.amountToSend == null - ? selectedChanged(_selectedUTXOs) - : selectedChanged(_selectedUTXOs) && - widget.amountToSend! <= selectedSum; + final enableApply = + widget.amountToSend == null + ? selectedChanged(_selectedUTXOs) + : selectedChanged(_selectedUTXOs) && + widget.amountToSend! <= selectedSum; return DesktopDialog( maxWidth: 700, @@ -147,14 +152,8 @@ class _DesktopCoinControlUseDialogState children: [ Row( children: [ - const AppBarBackButton( - size: 40, - iconSize: 24, - ), - Text( - "Coin control", - style: STextStyles.desktopH3(context), - ), + const AppBarBackButton(size: 40, iconSize: 24), + Text("Coin control", style: STextStyles.desktopH3(context)), ], ), Expanded( @@ -164,24 +163,24 @@ class _DesktopCoinControlUseDialogState children: [ RoundedContainer( color: Colors.transparent, - borderColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "This option allows you to control, freeze, and utilize " "outputs at your discretion.", - style: - STextStyles.desktopTextExtraExtraSmall(context), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ], ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( @@ -199,11 +198,13 @@ class _DesktopCoinControlUseDialogState _searchString = value; }); }, - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -223,44 +224,47 @@ class _DesktopCoinControlUseDialogState height: 20, ), ), - 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 = ""; - }); - }, - ), - ], + suffixIcon: + _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, ), - ), - ) - : null, + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SizedBox( height: 56, width: 240, child: Toggle( isOn: _filter == CCFilter.frozen, - onColor: Theme.of(context) - .extension()! - .rateTypeToggleDesktopColorOn, - offColor: Theme.of(context) - .extension()! - .rateTypeToggleDesktopColorOff, + onColor: + Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOn, + offColor: + Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOff, onIcon: Assets.svg.coinControl.unBlocked, onText: "Available", offIcon: Assets.svg.coinControl.blocked, @@ -281,9 +285,7 @@ class _DesktopCoinControlUseDialogState }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), JDropdownIconButton( redrawOnScreenSizeChanged: true, groupValue: _sort, @@ -299,164 +301,169 @@ class _DesktopCoinControlUseDialogState ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Expanded( - child: _list != null - ? ListView.separated( - shrinkWrap: true, - primary: false, - itemCount: _list!.length, - separatorBuilder: (context, _) => const SizedBox( - height: 10, - ), - itemBuilder: (context, index) { - final utxo = MainDB.instance.isar.utxos - .where() - .idEqualTo(_list![index]) - .findFirstSync()!; - final data = UtxoRowData(utxo.id, false); - data.selected = _selectedUTXOsData.contains(data); + child: + _list != null + ? ListView.separated( + shrinkWrap: true, + primary: false, + itemCount: _list!.length, + separatorBuilder: + (context, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final utxo = + MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; + final data = UtxoRowData(utxo.id, false); + data.selected = _selectedUTXOsData.contains( + data, + ); - return UtxoRow( - key: Key( - "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", - ), - data: data, - compact: true, - walletId: widget.walletId, - onSelectionChanged: (value) { - setState(() { - if (data.selected) { - _selectedUTXOsData.add(value); - _selectedUTXOs.add(utxo); + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", + ), + data: data, + compact: true, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOsData.add(value); + _selectedUTXOs.add(utxo); + } else { + _selectedUTXOsData.remove(value); + _selectedUTXOs.remove(utxo); + } + }); + }, + ); + }, + ) + : ListView.separated( + itemCount: _map!.entries.length, + separatorBuilder: + (context, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final entry = _map!.entries.elementAt(index); + final _controller = RotateIconController(); + + return Expandable2( + border: + Theme.of(context) + .extension()! + .backgroundAppBar, + background: + Theme.of( + context, + ).extension()!.popupBG, + animationDurationMultiplier: + 0.2 * entry.value.length, + onExpandWillChange: (state) { + if (state == Expandable2State.expanded) { + _controller.forward?.call(); } else { - _selectedUTXOsData.remove(value); - _selectedUTXOs.remove(utxo); + _controller.reverse?.call(); } - }); - }, - ); - }, - ) - : ListView.separated( - itemCount: _map!.entries.length, - separatorBuilder: (context, _) => const SizedBox( - height: 10, - ), - itemBuilder: (context, index) { - final entry = _map!.entries.elementAt(index); - final _controller = RotateIconController(); - - return Expandable2( - border: Theme.of(context) - .extension()! - .backgroundAppBar, - background: Theme.of(context) - .extension()! - .popupBG, - animationDurationMultiplier: - 0.2 * entry.value.length, - onExpandWillChange: (state) { - if (state == Expandable2State.expanded) { - _controller.forward?.call(); - } else { - _controller.reverse?.call(); - } - }, - header: RoundedContainer( - padding: const EdgeInsets.all(20), - color: Colors.transparent, - child: Row( - children: [ - SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), + }, + header: RoundedContainer( + padding: const EdgeInsets.all(20), + color: Colors.transparent, + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 24, + height: 24, ), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Expanded( - flex: 3, - child: Text( - entry.key, - style: STextStyles.w600_14(context), + const SizedBox(width: 12), + Expanded( + flex: 3, + child: Text( + entry.key, + style: STextStyles.w600_14(context), + ), ), - ), - Expanded( - child: Text( - "${entry.value.length} " - "output${entry.value.length > 1 ? "s" : ""}", - style: STextStyles - .desktopTextExtraExtraSmall( - context, + Expanded( + child: Text( + "${entry.value.length} " + "output${entry.value.length > 1 ? "s" : ""}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ), - ), - RotateIcon( - animationDurationMultiplier: - 0.2 * entry.value.length, - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 14, - color: Theme.of(context) - .extension()! - .textSubtitle1, + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: + Theme.of(context) + .extension()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, ), - curve: Curves.easeInOut, - controller: _controller, - ), - ], + ], + ), ), - ), - children: entry.value.map( - (id) { - final utxo = MainDB.instance.isar.utxos - .where() - .idEqualTo(id) - .findFirstSync()!; - final data = UtxoRowData(utxo.id, false); - data.selected = - _selectedUTXOsData.contains(data); + children: + entry.value.map((id) { + final utxo = + MainDB.instance.isar.utxos + .where() + .idEqualTo(id) + .findFirstSync()!; + final data = UtxoRowData( + utxo.id, + false, + ); + data.selected = _selectedUTXOsData + .contains(data); - return UtxoRow( - key: Key( - "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", - ), - data: data, - compact: true, - compactWithBorder: false, - raiseOnSelected: false, - walletId: widget.walletId, - onSelectionChanged: (value) { - setState(() { - if (data.selected) { - _selectedUTXOsData.add(value); - _selectedUTXOs.add(utxo); - } else { - _selectedUTXOsData.remove(value); - _selectedUTXOs.remove(utxo); - } - }); - }, - ); - }, - ).toList(), - ); - }, - ), - ), - const SizedBox( - height: 16, + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", + ), + data: data, + compact: true, + compactWithBorder: false, + raiseOnSelected: false, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOsData.add(value); + _selectedUTXOs.add(utxo); + } else { + _selectedUTXOsData.remove( + value, + ); + _selectedUTXOs.remove(utxo); + } + }); + }, + ); + }).toList(), + ); + }, + ), ), + const SizedBox(height: 16), RoundedContainer( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, padding: EdgeInsets.zero, child: ConditionalParent( condition: widget.amountToSend != null, @@ -467,9 +474,10 @@ class _DesktopCoinControlUseDialogState child, Container( height: 1.2, - color: Theme.of(context) - .extension()! - .popupBG, + color: + Theme.of( + context, + ).extension()!.popupBG, ), Padding( padding: const EdgeInsets.all(16), @@ -481,26 +489,26 @@ class _DesktopCoinControlUseDialogState "Amount to send", style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), ), SelectableText( - "${widget.amountToSend!.decimal.toStringAsFixed( - coin.fractionDigits, - )}" + "${widget.amountToSend!.decimal.toStringAsFixed(coin.fractionDigits)}" " ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), ), ], ), @@ -518,23 +526,23 @@ class _DesktopCoinControlUseDialogState style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), SelectableText( - "${selectedSum.decimal.toStringAsFixed( - coin.fractionDigits, - )} ${coin.ticker}", + "${selectedSum.decimal.toStringAsFixed(coin.fractionDigits)} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: widget.amountToSend == null - ? Theme.of(context) - .extension()! - .textDark - : selectedSum < widget.amountToSend! + color: + widget.amountToSend == null + ? Theme.of( + context, + ).extension()!.textDark + : selectedSum < widget.amountToSend! ? Theme.of(context) .extension()! .accentColorRed @@ -548,18 +556,17 @@ class _DesktopCoinControlUseDialogState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( child: SecondaryButton( enabled: _selectedUTXOsData.isNotEmpty, buttonHeight: ButtonHeight.l, - label: _selectedUTXOsData.isEmpty - ? "Clear selection" - : "Clear selection (${_selectedUTXOsData.length})", + label: + _selectedUTXOsData.isEmpty + ? "Clear selection" + : "Clear selection (${_selectedUTXOsData.length})", onPressed: () { setState(() { _selectedUTXOsData.clear(); @@ -568,9 +575,7 @@ class _DesktopCoinControlUseDialogState }, ), ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), Expanded( child: PrimaryButton( enabled: enableApply, @@ -586,9 +591,7 @@ class _DesktopCoinControlUseDialogState ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart index c487ff991..d2a053d74 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart @@ -19,7 +19,7 @@ import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; import '../../db/isar/main_db.dart'; -import '../../models/exchange/change_now/exchange_transaction_status.dart'; +import '../../models/exchange/change_now/cn_exchange_transaction_status.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/isar/stack_theme.dart'; @@ -104,41 +104,32 @@ class _DesktopAllTradesViewState extends ConsumerState { background: Theme.of(context).extension()!.popupBG, leading: Row( children: [ - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), AppBarIconButton( size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, width: 18, height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: Navigator.of(context).pop, ), - const SizedBox( - width: 12, - ), - Text( - "Trades", - style: STextStyles.desktopH3(context), - ), + const SizedBox(width: 12), + Text("Trades", style: STextStyles.desktopH3(context)), ], ), ), body: Padding( - padding: const EdgeInsets.only( - left: 20, - top: 20, - right: 20, - ), + padding: const EdgeInsets.only(left: 20, top: 20, right: 20), child: Column( children: [ Row( @@ -159,11 +150,13 @@ class _DesktopAllTradesViewState extends ConsumerState { _searchString = value; }); }, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -183,35 +176,34 @@ class _DesktopAllTradesViewState extends ConsumerState { height: 20, ), ), - 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 = ""; - }); - }, - ), - ], + 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, + ) + : null, ), ), ), ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: Consumer( builder: (_, ref, __) { @@ -237,38 +229,36 @@ class _DesktopAllTradesViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (index != 0) - const SizedBox( - height: 12, - ), + if (index != 0) const SizedBox(height: 12), Text( month.item1, style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: ListView.separated( shrinkWrap: true, primary: false, - separatorBuilder: (context, _) => Container( - height: 1, - color: Theme.of(context) - .extension()! - .background, - ), + separatorBuilder: + (context, _) => Container( + height: 1, + color: + Theme.of(context) + .extension()! + .background, + ), itemCount: month.item2.length, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.all(4), - child: DesktopTradeRowCard( - key: Key( - "transactionCard_key_${month.item2[index].tradeId}", + itemBuilder: + (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: DesktopTradeRowCard( + key: Key( + "transactionCard_key_${month.item2[index].tradeId}", + ), + tradeId: month.item2[index].tradeId, + ), ), - tradeId: month.item2[index].tradeId, - ), - ), ), ), ], @@ -287,10 +277,7 @@ class _DesktopAllTradesViewState extends ConsumerState { } class DesktopTradeRowCard extends ConsumerStatefulWidget { - const DesktopTradeRowCard({ - super.key, - required this.tradeId, - }); + const DesktopTradeRowCard({super.key, required this.tradeId}); final String tradeId; @@ -347,8 +334,9 @@ class _DesktopTradeRowCardState extends ConsumerState { @override Widget build(BuildContext context) { - final String? txid = - ref.read(tradeSentFromStackLookupProvider).getTxidForTradeId(tradeId); + final String? txid = ref + .read(tradeSentFromStackLookupProvider) + .getTxidForTradeId(tradeId); final List? walletIds = ref .read(tradeSentFromStackLookupProvider) .getWalletIdsForTradeId(tradeId); @@ -360,8 +348,9 @@ class _DesktopTradeRowCardState extends ConsumerState { color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: RawMaterialButton( shape: RoundedRectangleBorder( @@ -374,25 +363,27 @@ class _DesktopTradeRowCardState extends ConsumerState { //todo: check if print needed // debugPrint("name: ${manager.walletName}"); - final tx = await MainDB.instance - .getTransactions(walletIds.first) - .filter() - .txidEqualTo(txid) - .findFirst(); + final tx = + await MainDB.instance + .getTransactions(walletIds.first) + .filter() + .txidEqualTo(txid) + .findFirst(); if (mounted) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: tx, - walletName: ref.read(pWalletName(walletIds.first)), - walletId: walletIds.first, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: ref.read(pWalletName(walletIds.first)), + walletId: walletIds.first, + ), + ), ); } @@ -400,62 +391,67 @@ class _DesktopTradeRowCardState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), + builder: + (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3( + context, + ), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], ), - ], - ), - ), - Flexible( - child: SingleChildScrollView( - primary: false, - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: tx, - walletName: ref - .read(pWalletName(walletIds.first)), - walletId: walletIds.first, ), - ), + Flexible( + child: SingleChildScrollView( + primary: false, + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: ref.read( + pWalletName(walletIds.first), + ), + walletId: walletIds.first, + ), + ), + ), + ], ), - ], + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), ), - ), - const RouteSettings( - name: TradeDetailsView.routeName, - ), - ), - ]; - }, - ), + ]; + }, + ), ), ); } @@ -463,70 +459,69 @@ class _DesktopTradeRowCardState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), + builder: + (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], ), - ], - ), - ), - Flexible( - child: SingleChildScrollView( - primary: false, - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: null, - walletName: null, - walletId: walletIds?.first, ), - ), + Flexible( + child: SingleChildScrollView( + primary: false, + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: null, + walletName: null, + walletId: walletIds?.first, + ), + ), + ), + ], ), - ], + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), ), - ), - const RouteSettings( - name: TradeDetailsView.routeName, - ), - ), - ]; - }, - ), + ]; + }, + ), ), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: Row( children: [ Container( @@ -540,9 +535,7 @@ class _DesktopTradeRowCardState extends ConsumerState { File( _fetchIconAssetForStatus( trade.status, - ref.watch( - themeAssetsProvider, - ), + ref.watch(themeAssetsProvider), ), ), width: 32, @@ -550,15 +543,14 @@ class _DesktopTradeRowCardState extends ConsumerState { ), ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( flex: 3, child: Text( "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), @@ -576,8 +568,9 @@ class _DesktopTradeRowCardState extends ConsumerState { flex: 6, child: Text( "-${Decimal.tryParse(trade.payInAmount)?.toStringAsFixed(8) ?? "..."} ${trade.payInCurrency.toUpperCase()}", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index c6769e68a..23e925c0c 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -20,14 +20,18 @@ import '../../../models/exchange/response_objects/trade.dart'; import '../../../pages/exchange_view/send_from_view.dart'; import '../../../providers/exchange/exchange_form_state_provider.dart'; import '../../../providers/global/trades_service_provider.dart'; +import '../../../providers/global/wallets_provider.dart'; import '../../../route_generator.dart'; import '../../../services/exchange/exchange_response.dart'; import '../../../services/notifications_api.dart'; +import '../../../services/wallets.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/enums/exchange_rate_type_enum.dart'; import '../../../utilities/text_styles.dart'; +import '../../../wallets/wallet/intermediate/external_wallet.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_loading_overlay.dart'; import '../../../widgets/desktop/desktop_dialog.dart'; @@ -47,14 +51,11 @@ final ssss = StateProvider((_) => null); final desktopExchangeModelProvider = ChangeNotifierProvider( - (ref) => ref.watch(ssss.state).state, -); + (ref) => ref.watch(ssss.state).state, + ); class StepScaffold extends ConsumerStatefulWidget { - const StepScaffold({ - super.key, - required this.initialStep, - }); + const StepScaffold({super.key, required this.initialStep}); final int initialStep; @@ -79,19 +80,19 @@ class _StepScaffoldState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Creating a trade", - eventBus: null, + builder: + (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Creating a trade", + eventBus: null, + ), + ), ), - ), - ), ), ); @@ -99,12 +100,18 @@ class _StepScaffoldState extends ConsumerState { .read(efExchangeProvider) .createTrade( from: ref.read(desktopExchangeModelProvider)!.sendTicker, + fromNetwork: + ref.read(desktopExchangeModelProvider)!.sendCurrency.network, to: ref.read(desktopExchangeModelProvider)!.receiveTicker, - fixedRate: ref.read(desktopExchangeModelProvider)!.rateType != + toNetwork: + ref.read(desktopExchangeModelProvider)!.receiveCurrency.network, + fixedRate: + ref.read(desktopExchangeModelProvider)!.rateType != ExchangeRateType.estimated, - amount: ref.read(desktopExchangeModelProvider)!.reversed - ? ref.read(desktopExchangeModelProvider)!.receiveAmount - : ref.read(desktopExchangeModelProvider)!.sendAmount, + amount: + ref.read(desktopExchangeModelProvider)!.reversed + ? ref.read(desktopExchangeModelProvider)!.receiveAmount + : ref.read(desktopExchangeModelProvider)!.sendAmount, addressTo: ref.read(desktopExchangeModelProvider)!.recipientAddress!, extraId: null, addressRefund: ref.read(desktopExchangeModelProvider)!.refundAddress!, @@ -131,10 +138,11 @@ class _StepScaffoldState extends ConsumerState { showDialog( context: context, barrierDismissible: true, - builder: (_) => SimpleDesktopDialog( - title: "Failed to create trade", - message: message ?? "", - ), + builder: + (_) => SimpleDesktopDialog( + title: "Failed to create trade", + message: message ?? "", + ), ), ); } @@ -142,10 +150,9 @@ class _StepScaffoldState extends ConsumerState { } // save trade to hive - await ref.read(tradesServiceProvider).add( - trade: response.value!, - shouldNotifyListeners: true, - ); + await ref + .read(tradesServiceProvider) + .add(trade: response.value!, shouldNotifyListeners: true); String status = response.value!.status; @@ -206,38 +213,58 @@ class _StepScaffoldState extends ConsumerState { void sendFromStack() { final trade = ref.read(desktopExchangeModelProvider)!.trade!; final address = trade.payInAddress; - final coin = AppConfig.getCryptoCurrencyForTicker(trade.payInCurrency) ?? + final coin = + AppConfig.getCryptoCurrencyForTicker(trade.payInCurrency) ?? AppConfig.getCryptoCurrencyByPrettyName(trade.payInCurrency); - final amount = Decimal.parse(trade.payInAmount).toAmount( - fractionDigits: coin.fractionDigits, - ); + final amount = Decimal.parse( + trade.payInAmount, + ).toAmount(fractionDigits: coin.fractionDigits); showDialog( context: context, - builder: (context) => Navigator( - initialRoute: SendFromView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - SendFromView( - coin: coin, - trade: trade, - amount: amount, - address: address, - shouldPopRoot: true, - fromDesktopStep4: true, - ), - const RouteSettings( - name: SendFromView.routeName, - ), - ), - ]; - }, - ), + builder: + (context) => Navigator( + initialRoute: SendFromView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + SendFromView( + coin: coin, + trade: trade, + amount: amount, + address: address, + shouldPopRoot: true, + fromDesktopStep4: true, + ), + const RouteSettings(name: SendFromView.routeName), + ), + ]; + }, + ), ); } + bool isWalletCoinAndCanSendWithoutWalletOpened( + String ticker, + Wallets walletsInstance, + ) { + try { + final coin = AppConfig.getCryptoCurrencyForTicker(ticker); + return walletsInstance.wallets + .where( + (e) => + e.info.coin == coin && + (e is! ExternalWallet || + e is MwebInterface), // ltc mweb is external but swaps + // should not use mweb, hence the odd logic check here + ) + .isNotEmpty; + } catch (_) { + return false; + } + } + @override void initState() { duration = const Duration(milliseconds: 250); @@ -248,6 +275,18 @@ class _StepScaffoldState extends ConsumerState { @override Widget build(BuildContext context) { final model = ref.watch(desktopExchangeModelProvider); + + final bool canSendFromStack; + if (currentStep != 4) { + // set to true anyways to show back button + canSendFromStack = true; + } else { + canSendFromStack = isWalletCoinAndCanSendWithoutWalletOpened( + model?.sendTicker ?? "", + ref.read(pWallets), + ); + } + return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -258,13 +297,11 @@ class _StepScaffoldState extends ConsumerState { children: [ currentStep != 4 ? AppBarBackButton( - isCompact: true, - iconSize: 23, - onPressed: onBack, - ) - : const SizedBox( - width: 32, - ), + isCompact: true, + iconSize: 23, + onPressed: onBack, + ) + : const SizedBox(width: 32), Text( "Exchange ${model?.sendTicker.toUpperCase()} to ${model?.receiveTicker.toUpperCase()}", style: STextStyles.desktopH3(context), @@ -279,31 +316,19 @@ class _StepScaffoldState extends ConsumerState { ), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: DesktopExchangeStepsIndicator( - currentStep: currentStep, - ), - ), - const SizedBox( - height: 32, + padding: const EdgeInsets.symmetric(horizontal: 32), + child: DesktopExchangeStepsIndicator(currentStep: currentStep), ), + const SizedBox(height: 32), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: FadeStack( index: currentStep - 1, children: [ const DesktopStep1(), - DesktopStep2( - enableNextChanged: updateEnableNext, - ), + DesktopStep2(enableNextChanged: updateEnableNext), const DesktopStep3(), const DesktopStep4(), ], @@ -318,38 +343,41 @@ class _StepScaffoldState extends ConsumerState { ), child: Row( children: [ + canSendFromStack + ? Expanded( + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 250), + crossFadeState: + currentStep == 4 + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: SecondaryButton( + label: "Back", + buttonHeight: ButtonHeight.l, + onPressed: onBack, + ), + secondChild: SecondaryButton( + label: "Send from ${AppConfig.appName}", + buttonHeight: ButtonHeight.l, + onPressed: sendFromStack, + ), + ), + ) + : const Spacer(), + const SizedBox(width: 16), Expanded( child: AnimatedCrossFade( duration: const Duration(milliseconds: 250), - crossFadeState: currentStep == 4 - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: SecondaryButton( - label: "Back", - buttonHeight: ButtonHeight.l, - onPressed: onBack, - ), - secondChild: SecondaryButton( - label: "Send from ${AppConfig.appName}", - buttonHeight: ButtonHeight.l, - onPressed: sendFromStack, - ), - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 250), - crossFadeState: currentStep == 4 - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + currentStep == 4 + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: AnimatedCrossFade( duration: const Duration(milliseconds: 250), - crossFadeState: currentStep == 3 - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + currentStep == 3 + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: PrimaryButton( label: "Next", enabled: currentStep != 2 ? true : enableNext, @@ -394,9 +422,7 @@ class _StepScaffoldState extends ConsumerState { "Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toStringAsFixed(8)))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))} to this address", style: STextStyles.desktopH3(context), ), - const SizedBox( - height: 48, - ), + const SizedBox(height: 48), Center( child: QR( // TODO: grab coin uri scheme from somewhere @@ -409,9 +435,7 @@ class _StepScaffoldState extends ConsumerState { size: 290, ), ), - const SizedBox( - height: 48, - ), + const SizedBox(height: 48), SecondaryButton( label: "Cancel", width: 310, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index fd7c72a23..c7ba641ca 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -59,23 +59,23 @@ class _DesktopStep2State extends ConsumerState { void selectRecipientAddressFromStack() async { try { - final coin = AppConfig.getCryptoCurrencyForTicker( - ref.read(desktopExchangeModelProvider)!.receiveTicker, - )!; + final coin = + AppConfig.getCryptoCurrencyForTicker( + ref.read(desktopExchangeModelProvider)!.receiveTicker, + )!; final info = await showDialog?>( context: context, barrierColor: Colors.transparent, - builder: (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Padding( - padding: const EdgeInsets.all(32), - child: DesktopChooseFromStack( - coin: coin, + builder: + (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack(coin: coin), + ), ), - ), - ), ); if (info is Tuple2) { @@ -83,44 +83,40 @@ class _DesktopStep2State extends ConsumerState { ref.read(desktopExchangeModelProvider)!.recipientAddress = info.item2; } } catch (e, s) { - Logging.instance.i("$e\n$s", error: e, stackTrace: s,); + Logging.instance.i("$e\n$s", error: e, stackTrace: s); } - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); } void selectRefundAddressFromStack() async { try { - final coin = AppConfig.getCryptoCurrencyForTicker( - ref.read(desktopExchangeModelProvider)!.sendTicker, - )!; + final coin = + AppConfig.getCryptoCurrencyForTicker( + ref.read(desktopExchangeModelProvider)!.sendTicker, + )!; final info = await showDialog?>( context: context, barrierColor: Colors.transparent, - builder: (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Padding( - padding: const EdgeInsets.all(32), - child: DesktopChooseFromStack( - coin: coin, + builder: + (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack(coin: coin), + ), ), - ), - ), ); if (info is Tuple2) { _refundController.text = info.item1; ref.read(desktopExchangeModelProvider)!.refundAddress = info.item2; } } catch (e, s) { - Logging.instance.i("$e\n$s", error: e, stackTrace: s,); + Logging.instance.i("$e\n$s", error: e, stackTrace: s); } - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); } void selectRecipientFromAddressBook() async { @@ -131,43 +127,36 @@ class _DesktopStep2State extends ConsumerState { final entry = await showDialog( context: context, barrierColor: Colors.transparent, - builder: (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Address book", - style: STextStyles.desktopH3(context), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - const DesktopDialogCloseButton(), + Expanded(child: AddressBookAddressChooser(coin: coin)), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: coin, - ), - ), - ], - ), - ), + ), ); if (entry != null) { _toController.text = entry.address; ref.read(desktopExchangeModelProvider)!.recipientAddress = entry.address; - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); } } @@ -179,43 +168,36 @@ class _DesktopStep2State extends ConsumerState { final entry = await showDialog( context: context, barrierColor: Colors.transparent, - builder: (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Address book", - style: STextStyles.desktopH3(context), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - const DesktopDialogCloseButton(), + Expanded(child: AddressBookAddressChooser(coin: coin)), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: coin, - ), - ), - ], - ), - ), + ), ); if (entry != null) { _refundController.text = entry.address; ref.read(desktopExchangeModelProvider)!.refundAddress = entry.address; - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); } } @@ -242,33 +224,41 @@ class _DesktopStep2State extends ConsumerState { doesRefundAddress = ref.read(efExchangeProvider).supportsRefundAddress; if (!doesRefundAddress) { - // hack: set to empty to not throw null unwrap error later - ref.read(desktopExchangeModelProvider)!.refundAddress = ""; + WidgetsBinding.instance.addPostFrameCallback((_) { + // hack: set to empty to not throw null unwrap error later + ref.read(desktopExchangeModelProvider)!.refundAddress = ""; + }); } final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state; if (tuple != null) { if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() == tuple.item2.ticker.toLowerCase()) { - _toController.text = ref - .read(pWallets) - .getWallet(tuple.item1) - .info - .cachedReceivingAddress; - - ref.read(desktopExchangeModelProvider)!.recipientAddress = - _toController.text; + _toController.text = + ref + .read(pWallets) + .getWallet(tuple.item1) + .info + .cachedReceivingAddress; + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(desktopExchangeModelProvider)!.recipientAddress = + _toController.text; + }); } else { if (doesRefundAddress && ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() == tuple.item2.ticker.toUpperCase()) { - _refundController.text = ref - .read(pWallets) - .getWallet(tuple.item1) - .info - .cachedReceivingAddress; - ref.read(desktopExchangeModelProvider)!.refundAddress = - _refundController.text; + _refundController.text = + ref + .read(pWallets) + .getWallet(tuple.item1) + .info + .cachedReceivingAddress; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(desktopExchangeModelProvider)!.refundAddress = + _refundController.text; + }); } } } @@ -297,32 +287,30 @@ class _DesktopStep2State extends ConsumerState { style: STextStyles.desktopTextMedium(context), textAlign: TextAlign.center, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Text( "Enter your recipient and refund addresses", style: STextStyles.desktopTextExtraExtraSmall(context), textAlign: TextAlign.center, ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Recipient Wallet", style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), ), if (AppConfig.isStackCoin( ref.watch( - desktopExchangeModelProvider - .select((value) => value!.receiveTicker), + desktopExchangeModelProvider.select( + (value) => value!.receiveTicker, + ), ), )) CustomTextButton( @@ -331,9 +319,7 @@ class _DesktopStep2State extends ConsumerState { ), ], ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -359,9 +345,7 @@ class _DesktopStep2State extends ConsumerState { onChanged: (value) { ref.read(desktopExchangeModelProvider)!.recipientAddress = _toController.text; - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); }, decoration: standardInputDecoration( "Enter the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} payout address", @@ -376,57 +360,56 @@ class _DesktopStep2State extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _toController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _toController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _toController.text.isNotEmpty ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _toController.text = ""; + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _toController.text = ""; + ref + .read(desktopExchangeModelProvider)! + .recipientAddress = _toController.text; + widget.enableNextChanged.call(_next()); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + _toController.text = content; ref .read(desktopExchangeModelProvider)! .recipientAddress = _toController.text; - widget.enableNextChanged.call( - _next(), - ); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = data.text!.trim(); - _toController.text = content; - ref - .read(desktopExchangeModelProvider)! - .recipientAddress = _toController.text; - widget.enableNextChanged.call( - _next(), - ); - } - }, - child: _toController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + widget.enableNextChanged.call(_next()); + } + }, + child: + _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_toController.text.isEmpty && AppConfig.isStackCoin( ref.watch( - desktopExchangeModelProvider - .select((value) => value!.receiveTicker), + desktopExchangeModelProvider.select( + (value) => value!.receiveTicker, + ), ), )) TextFieldIconButton( @@ -441,9 +424,7 @@ class _DesktopStep2State extends ConsumerState { ), ), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), RoundedWhiteContainer( borderColor: Theme.of(context).extension()!.background, child: Text( @@ -451,10 +432,7 @@ class _DesktopStep2State extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall(context), ), ), - if (doesRefundAddress) - const SizedBox( - height: 24, - ), + if (doesRefundAddress) const SizedBox(height: 24), if (doesRefundAddress) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -462,15 +440,17 @@ class _DesktopStep2State extends ConsumerState { Text( "Refund Wallet (required)", style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), if (AppConfig.isStackCoin( ref.watch( - desktopExchangeModelProvider - .select((value) => value!.sendTicker), + desktopExchangeModelProvider.select( + (value) => value!.sendTicker, + ), ), )) CustomTextButton( @@ -479,10 +459,7 @@ class _DesktopStep2State extends ConsumerState { ), ], ), - if (doesRefundAddress) - const SizedBox( - height: 10, - ), + if (doesRefundAddress) const SizedBox(height: 10), if (doesRefundAddress) ClipRRect( borderRadius: BorderRadius.circular( @@ -508,9 +485,7 @@ class _DesktopStep2State extends ConsumerState { onChanged: (value) { ref.read(desktopExchangeModelProvider)!.refundAddress = _refundController.text; - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); }, decoration: standardInputDecoration( "Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address", @@ -525,60 +500,59 @@ class _DesktopStep2State extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _refundController.text.isEmpty - ? const EdgeInsets.only(right: 16) - : const EdgeInsets.only(right: 0), + padding: + _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _refundController.text.isNotEmpty ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _refundController.text = ""; + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _refundController.text = ""; + ref + .read(desktopExchangeModelProvider)! + .refundAddress = _refundController.text; + + widget.enableNextChanged.call(_next()); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + + _refundController.text = content; ref .read(desktopExchangeModelProvider)! .refundAddress = _refundController.text; - widget.enableNextChanged.call( - _next(), - ); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = data.text!.trim(); - - _refundController.text = content; - ref - .read(desktopExchangeModelProvider)! - .refundAddress = _refundController.text; - - widget.enableNextChanged.call( - _next(), - ); - } - }, - child: _refundController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + widget.enableNextChanged.call(_next()); + } + }, + child: + _refundController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_refundController.text.isEmpty && AppConfig.isStackCoin( ref.watch( - desktopExchangeModelProvider - .select((value) => value!.sendTicker), + desktopExchangeModelProvider.select( + (value) => value!.sendTicker, + ), ), )) TextFieldIconButton( @@ -593,10 +567,7 @@ class _DesktopStep2State extends ConsumerState { ), ), ), - if (doesRefundAddress) - const SizedBox( - height: 10, - ), + if (doesRefundAddress) const SizedBox(height: 10), if (doesRefundAddress) RoundedWhiteContainer( borderColor: Theme.of(context).extension()!.background, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart index dd30adee2..6e18c5086 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart @@ -23,9 +23,7 @@ import '../step_scaffold.dart'; import 'desktop_step_item.dart'; class DesktopStep4 extends ConsumerStatefulWidget { - const DesktopStep4({ - super.key, - }); + const DesktopStep4({super.key}); @override ConsumerState createState() => _DesktopStep4State(); @@ -56,8 +54,9 @@ class _DesktopStep4State extends ConsumerState { return; } - final statusResponse = - await ref.read(efExchangeProvider).updateTrade(trade); + final statusResponse = await ref + .read(efExchangeProvider) + .updateTrade(trade); String status = "Waiting"; if (statusResponse.value != null) { status = statusResponse.value!.status; @@ -99,16 +98,12 @@ class _DesktopStep4State extends ConsumerState { "Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to the address below", style: STextStyles.desktopTextMedium(context), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Text( "Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to the address below. Once it is received, ${ref.watch(desktopExchangeModelProvider.select((value) => value!.trade?.exchangeName))} will send the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", style: STextStyles.desktopTextExtraExtraSmall(context), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), RoundedContainer( color: Theme.of(context).extension()!.warningBackground, child: RichText( @@ -116,9 +111,10 @@ class _DesktopStep4State extends ConsumerState { text: "You must send at least ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toString()))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))}. ", style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: + Theme.of( + context, + ).extension()!.warningForeground, fontSize: 14, ), children: [ @@ -126,9 +122,10 @@ class _DesktopStep4State extends ConsumerState { text: "If you send less than ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toString()))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))}, your transaction may not be converted and it may not be refunded.", style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: + Theme.of( + context, + ).extension()!.warningForeground, fontSize: 14, ), ), @@ -136,9 +133,7 @@ class _DesktopStep4State extends ConsumerState { ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), RoundedWhiteContainer( borderColor: Theme.of(context).extension()!.background, padding: const EdgeInsets.all(0), @@ -146,11 +141,14 @@ class _DesktopStep4State extends ConsumerState { children: [ DesktopStepItem( vertical: true, + copyableValue: true, label: "Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} to this address", - value: ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.payInAddress), + value: + ref.watch( + desktopExchangeModelProvider.select( + (value) => value!.trade?.payInAddress, + ), ) ?? "Error", ), @@ -159,22 +157,26 @@ class _DesktopStep4State extends ConsumerState { color: Theme.of(context).extension()!.background, ), if (ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.payInExtraId), + desktopExchangeModelProvider.select( + (value) => value!.trade?.payInExtraId, + ), ) != null) DesktopStepItem( vertical: true, label: "Memo", - value: ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.payInExtraId), + value: + ref.watch( + desktopExchangeModelProvider.select( + (value) => value!.trade?.payInExtraId, + ), ) ?? "Error", ), if (ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.payInExtraId), + desktopExchangeModelProvider.select( + (value) => value!.trade?.payInExtraId, + ), ) != null) Container( @@ -192,9 +194,11 @@ class _DesktopStep4State extends ConsumerState { ), DesktopStepItem( label: "Trade ID", - value: ref.watch( - desktopExchangeModelProvider - .select((value) => value!.trade?.tradeId), + value: + ref.watch( + desktopExchangeModelProvider.select( + (value) => value!.trade?.tradeId, + ), ) ?? "Error", ), @@ -213,8 +217,9 @@ class _DesktopStep4State extends ConsumerState { ), Text( _statusString, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context) .extension()! .colorForStatus(_statusString), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart index 352e353fb..6349350d1 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_item.dart @@ -13,6 +13,7 @@ import 'package:flutter/material.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/conditional_parent.dart'; +import '../../../../widgets/custom_buttons/simple_copy_button.dart'; class DesktopStepItem extends StatelessWidget { const DesktopStepItem({ @@ -21,12 +22,14 @@ class DesktopStepItem extends StatelessWidget { required this.value, this.padding = const EdgeInsets.all(16), this.vertical = false, + this.copyableValue = false, }); final String label; final String value; final EdgeInsets padding; final bool vertical; + final bool copyableValue; @override Widget build(BuildContext context) { @@ -34,35 +37,69 @@ class DesktopStepItem extends StatelessWidget { padding: padding, child: ConditionalParent( condition: vertical, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - const SizedBox( - height: 2, - ), - Text( - value, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark, - ), + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: copyableValue, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [child, SimpleCopyButton(data: value)], + ), + child: child, + ), + const SizedBox(height: 2), + copyableValue + ? SelectableText( + value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ) + : Text( + value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], ), - ], - ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - label, - style: STextStyles.desktopTextExtraExtraSmall(context), - ), + Text(label, style: STextStyles.desktopTextExtraExtraSmall(context)), if (!vertical) - Text( - value, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark, - ), - ), + copyableValue + ? SelectableText( + value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context).extension()!.textDark, + ), + ) + : Text( + value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context).extension()!.textDark, + ), + ), ], ), ), diff --git a/lib/pages_desktop_specific/desktop_home_view.dart b/lib/pages_desktop_specific/desktop_home_view.dart index 980b563a0..d29aeb4bc 100644 --- a/lib/pages_desktop_specific/desktop_home_view.dart +++ b/lib/pages_desktop_specific/desktop_home_view.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -22,6 +24,8 @@ import '../providers/ui/unread_notifications_provider.dart'; import '../route_generator.dart'; import '../themes/stack_colors.dart'; import '../utilities/enums/backup_frequency_type.dart'; +import '../utilities/idle_monitor.dart'; +import '../utilities/prefs.dart'; import '../widgets/background.dart'; import 'address_book_view/desktop_address_book.dart'; import 'desktop_buy/desktop_buy_view.dart'; @@ -29,6 +33,7 @@ import 'desktop_exchange/desktop_exchange_view.dart'; import 'desktop_menu.dart'; import 'my_stack_view/my_stack_view.dart'; import 'notifications/desktop_notifications_view.dart'; +import 'password/desktop_unlock_app_dialog.dart'; import 'settings/desktop_settings_view.dart'; import 'settings/settings_menu/desktop_about_view.dart'; import 'settings/settings_menu/desktop_support_view.dart'; @@ -45,14 +50,60 @@ class DesktopHomeView extends ConsumerStatefulWidget { class _DesktopHomeViewState extends ConsumerState { final GlobalKey myStackViewNavKey = GlobalKey(); late final Navigator myStackViewNav; + IdleMonitor? _idleMonitor; + + void _onIdle() async { + final context = myStackViewNavKey.currentContext; + if (context != null) { + await showDialog( + barrierDismissible: false, + context: context, + useSafeArea: false, + builder: + (context) => const Background( + child: Center(child: DesktopUnlockAppDialog()), + ), + ); + } + } + + late AutoLockInfo _autoLockInfo; + void _prefsTimeoutListener() { + final prefs = ref.read(prefsChangeNotifierProvider); + if (mounted && prefs.autoLockInfo != _autoLockInfo) { + _autoLockInfo = prefs.autoLockInfo; + if (_autoLockInfo.enabled) { + _idleMonitor?.detach(); + _idleMonitor = IdleMonitor( + timeout: Duration(minutes: _autoLockInfo.minutes), + onIdle: _onIdle, + ); + _idleMonitor!.attach(); + } else { + _idleMonitor?.detach(); + _idleMonitor = null; + } + } + } @override void initState() { + _autoLockInfo = ref.read(prefsChangeNotifierProvider).autoLockInfo; + if (_autoLockInfo.enabled) { + _idleMonitor = IdleMonitor( + timeout: Duration(minutes: _autoLockInfo.minutes), + onIdle: _onIdle, + ); + } + myStackViewNav = Navigator( key: myStackViewNavKey, onGenerateRoute: RouteGenerator.generateRoute, initialRoute: MyStackView.routeName, ); + _idleMonitor?.attach(); + + ref.read(prefsChangeNotifierProvider).addListener(_prefsTimeoutListener); // WidgetsBinding.instance.addPostFrameCallback((timeStamp) { // showOneTimeTorHasBeenAddedDialogIfRequired(context); @@ -61,12 +112,19 @@ class _DesktopHomeViewState extends ConsumerState { super.initState(); } + @override + dispose() { + ref.read(prefsChangeNotifierProvider).removeListener(_prefsTimeoutListener); + _idleMonitor?.detach(); + super.dispose(); + } + final Map contentViews = { DesktopMenuItemId.myStack: Container( - // key: Key("desktopStackHomeKey"), - // onGenerateRoute: RouteGenerator.generateRoute, - // initialRoute: MyStackView.routeName, - ), + // key: Key("desktopStackHomeKey"), + // onGenerateRoute: RouteGenerator.generateRoute, + // initialRoute: MyStackView.routeName, + ), DesktopMenuItemId.exchange: const Navigator( key: Key("desktopExchangeHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, @@ -109,11 +167,13 @@ class _DesktopHomeViewState extends ConsumerState { if (ref.read(prevDesktopMenuItemProvider.state).state == DesktopMenuItemId.myStack && ref.read(prevDesktopMenuItemProvider.state).state == newKey) { - Navigator.of(myStackViewNavKey.currentContext!) - .popUntil(ModalRoute.withName(MyStackView.routeName)); + Navigator.of( + myStackViewNavKey.currentContext!, + ).popUntil(ModalRoute.withName(MyStackView.routeName)); if (ref.read(currentWalletIdProvider.state).state != null) { - final wallet = - ref.read(pWallets).getWallet(ref.read(currentWalletIdProvider)!); + final wallet = ref + .read(pWallets) + .getWallet(ref.read(currentWalletIdProvider)!); if (wallet.shouldAutoSync) { wallet.shouldAutoSync = false; @@ -182,17 +242,19 @@ class _DesktopHomeViewState extends ConsumerState { ), Expanded( child: IndexedStack( - index: ref - .watch(currentDesktopMenuItemProvider.state) - .state - .index > - 0 - ? 1 - : 0, + index: + ref + .watch(currentDesktopMenuItemProvider.state) + .state + .index > + 0 + ? 1 + : 0, children: [ myStackViewNav, - contentViews[ - ref.watch(currentDesktopMenuItemProvider.state).state]!, + contentViews[ref + .watch(currentDesktopMenuItemProvider.state) + .state]!, ], ), ), diff --git a/lib/pages_desktop_specific/lelantus_coins/lelantus_coins_view.dart b/lib/pages_desktop_specific/lelantus_coins/lelantus_coins_view.dart deleted file mode 100644 index c08891aef..000000000 --- a/lib/pages_desktop_specific/lelantus_coins/lelantus_coins_view.dart +++ /dev/null @@ -1,134 +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_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; - -import '../../models/isar/models/isar_models.dart'; -import '../../providers/db/main_db_provider.dart'; -import '../../themes/stack_colors.dart'; -import '../../utilities/assets.dart'; -import '../../utilities/text_styles.dart'; -import '../../utilities/util.dart'; -import '../../widgets/background.dart'; -import '../../widgets/conditional_parent.dart'; -import '../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../widgets/desktop/desktop_app_bar.dart'; -import '../../widgets/desktop/desktop_scaffold.dart'; -import '../../widgets/isar_collection_watcher_list.dart'; - -class LelantusCoinsView extends ConsumerWidget { - const LelantusCoinsView({ - super.key, - required this.walletId, - }); - - static const title = "Lelantus coins"; - static const String routeName = "/lelantusCoinsView"; - - final String walletId; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ConditionalParent( - condition: Util.isDesktop, - builder: (child) { - return DesktopScaffold( - appBar: DesktopAppBar( - background: Theme.of(context).extension()!.popupBG, - leading: Expanded( - child: Row( - children: [ - const SizedBox( - width: 32, - ), - 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, - ), - const SizedBox( - width: 12, - ), - Text( - title, - style: STextStyles.desktopH3(context), - ), - const Spacer(), - ], - ), - ), - useSpacers: false, - isCompactHeight: true, - ), - body: Padding( - padding: const EdgeInsets.all(24), - child: child, - ), - ); - }, - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) { - return Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - automaticallyImplyLeading: false, - leading: AppBarBackButton( - onPressed: () => Navigator.of(context).pop(), - ), - title: Text( - title, - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: child, - ), - ), - ); - }, - child: IsarCollectionWatcherList( - itemName: title, - queryBuilder: () => ref - .read(mainDBProvider) - .isar - .lelantusCoins - .where() - .walletIdEqualTo(walletId) - .sortByMintIndexDesc(), - itemBuilder: (LelantusCoin? coin) { - return [ - ("TXID", coin?.txid ?? "", 9), - ("Value (sats)", coin?.value ?? "", 3), - ("Index", coin?.mintIndex.toString() ?? "", 2), - ("Is JMint", coin?.isJMint.toString() ?? "", 2), - ("Used", coin?.isUsed.toString() ?? "", 2), - ]; - }, - ), - ), - ); - } -} diff --git a/lib/pages_desktop_specific/mweb_utxos_view.dart b/lib/pages_desktop_specific/mweb_utxos_view.dart new file mode 100644 index 000000000..0c28cdf66 --- /dev/null +++ b/lib/pages_desktop_specific/mweb_utxos_view.dart @@ -0,0 +1,281 @@ +/* + * 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 2025-06-12 + * + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_app_bar.dart'; +import '../../widgets/desktop/desktop_scaffold.dart'; +import '../db/drift/database.dart'; +import '../providers/providers.dart'; +import '../widgets/detail_item.dart'; +import '../widgets/rounded_white_container.dart'; + +class MwebUtxosView extends ConsumerWidget { + const MwebUtxosView({super.key, required this.walletId}); + + static const title = "MWEB outputs"; + static const String routeName = "/mwebUtxosView"; + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + children: [ + const SizedBox(width: 32), + 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, + ), + const SizedBox(width: 12), + Text(title, style: STextStyles.desktopH3(context)), + const Spacer(), + ], + ), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: Padding(padding: const EdgeInsets.all(24), child: child), + ); + }, + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: AppBarBackButton( + onPressed: () => Navigator.of(context).pop(), + ), + title: Text(title, style: STextStyles.navBarTitle(context)), + ), + body: SafeArea(child: child), + ), + ); + }, + child: StreamViewList( + itemName: title, + stream: + ref + .read(pDrift(walletId)) + .select(ref.read(pDrift(walletId)).mwebUtxos) + .watch(), + itemBuilder: (MwebUtxo? coin) { + return [ + ("Output Id", coin?.outputId ?? "", 9), + ("Address", coin?.address ?? "", 9), + ("Value (sats)", coin?.value.toString() ?? "", 3), + ("Height", coin?.height.toString() ?? "", 2), + ("Block time", coin?.blockTime.toString() ?? "", 2), + ("Blocked", coin?.blocked.toString() ?? "", 2), + ("Used", coin?.used.toString() ?? "", 2), + ]; + }, + ), + ), + ); + } +} + +class StreamViewList extends StatefulWidget { + const StreamViewList({ + super.key, + required this.stream, + required this.itemBuilder, + required this.itemName, + }); + + final Stream> stream; + final String itemName; + final List<(String title, String value, int flex)> Function(T?) itemBuilder; + + @override + State> createState() => _StreamViewListState(); +} + +class _StreamViewListState extends State> { + List _items = []; + + late final StreamSubscription> _streamSubscription; + + void _onMwebUtxossCollectionWatcherEvent(List items) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _items = items; + }); + } + }); + } + + @override + void initState() { + super.initState(); + + _streamSubscription = widget.stream.listen( + (data) => _onMwebUtxossCollectionWatcherEvent(data), + ); + } + + @override + void dispose() { + _streamSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Total ${widget.itemName}: ${_items.length}", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + children: [ + ...widget + .itemBuilder(null) + .map( + (e) => Expanded( + flex: e.$3, + child: Text( + e.$1, + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ), + ), + ], + ), + ), + ), + Expanded( + child: ListView.separated( + shrinkWrap: true, + itemCount: _items.length, + separatorBuilder: + (_, __) => Container( + height: 1, + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, + ), + itemBuilder: + (_, index) => Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ...widget + .itemBuilder(_items[index]) + .map( + (e) => Expanded( + flex: e.$3, + child: SelectableText( + e.$2, + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.left, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ); + } else { + return ListView.builder( + itemCount: _items.length + 1, + itemBuilder: (ctx, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16), + child: RoundedWhiteContainer( + child: + index == 0 + ? Row( + children: [ + Text( + "Total ${widget.itemName}: ${_items.length}", + style: STextStyles.itemSubtitle(context), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...widget + .itemBuilder(_items[index - 1]) + .map( + (e) => DetailItem(title: e.$1, detail: e.$2), + ), + ], + ), + ), + ); + }, + ); + } + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart index 023b9fb79..1ff4405df 100644 --- a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart @@ -10,6 +10,7 @@ import 'dart:io'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -23,7 +24,6 @@ import '../../../themes/coin_icon_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount_formatter.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/text_styles.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; @@ -38,14 +38,13 @@ class DesktopPaynymSendDialog extends ConsumerStatefulWidget { required this.walletId, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), + this.accountLite, }); final String walletId; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; final PaynymAccountLite? accountLite; @override @@ -63,6 +62,15 @@ class _DesktopPaynymSendDialogState final coin = ref.watch(pWalletCoin(widget.walletId)); + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin)?.value, + ), + ); + } + return DesktopDialog( maxHeight: double.infinity, maxWidth: 580, @@ -90,15 +98,11 @@ class _DesktopPaynymSendDialogState child: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 36, height: 36, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -108,15 +112,14 @@ class _DesktopPaynymSendDialogState overflow: TextOverflow.ellipsis, maxLines: 1, ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( "Available balance", style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], @@ -128,7 +131,9 @@ class _DesktopPaynymSendDialogState crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref .watch(pWalletBalance(widget.walletId)) .spendable, @@ -136,28 +141,18 @@ class _DesktopPaynymSendDialogState style: STextStyles.titleBold12(context), textAlign: TextAlign.right, ), - const SizedBox( - height: 2, - ), - Text( - "${(ref.watch(pWalletBalance(widget.walletId)).spendable.decimal * ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin).item1, - ), - )).toAmount(fractionDigits: 2).fiatString( - locale: locale, - )} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, + if (price != null) const SizedBox(height: 2), + if (price != null) + Text( + "${(ref.watch(pWalletBalance(widget.walletId)).spendable.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: locale)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.baseXS(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), - )}", - style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + textAlign: TextAlign.right, ), - textAlign: TextAlign.right, - ), ], ), ), @@ -165,15 +160,9 @@ class _DesktopPaynymSendDialogState ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), child: DesktopSend( walletId: widget.walletId, accountLite: widget.accountLite, 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 066ae9073..45b5d710e 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 @@ -10,6 +10,7 @@ import 'dart:io'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -53,12 +54,11 @@ class _WalletTableState extends ConsumerState { return ConditionalParent( condition: index + 1 == walletsByCoin.length, - builder: (child) => Padding( - padding: const EdgeInsets.only( - bottom: 16, - ), - child: child, - ), + builder: + (child) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: child, + ), child: DesktopWalletSummaryRow( key: Key("DesktopWalletSummaryRow_key_${coin.identifier}"), coin: coin, @@ -66,9 +66,7 @@ class _WalletTableState extends ConsumerState { ), ); }, - separatorBuilder: (_, __) => const SizedBox( - height: 10, - ), + separatorBuilder: (_, __) => const SizedBox(height: 10), itemCount: walletsByCoin.length, ); } @@ -96,11 +94,10 @@ class _DesktopWalletSummaryRowState // ... and if the coin supports Tor. if (!widget.coin.torSupport) { // If not, show a Tor warning dialog. - final shouldContinue = await showDialog( + final shouldContinue = + await showDialog( context: context, - builder: (_) => TorWarningDialog( - coin: widget.coin, - ), + builder: (_) => TorWarningDialog(coin: widget.coin), ) ?? false; if (!shouldContinue) { @@ -121,8 +118,12 @@ class _DesktopWalletSummaryRowState await _checkTor(); if (mounted) { - final wallet = ref.read(pWallets).wallets.firstWhere( - (e) => e.cryptoCurrency.identifier == widget.coin.identifier); + final wallet = ref + .read(pWallets) + .wallets + .firstWhere( + (e) => e.cryptoCurrency.identifier == widget.coin.identifier, + ); final canContinue = await checkShowNodeTorSettingsMismatch( context: context, @@ -139,8 +140,9 @@ class _DesktopWalletSummaryRowState final Future loadFuture; if (wallet is ExternalWallet) { - loadFuture = - wallet.init().then((value) async => await (wallet).open()); + loadFuture = wallet.init().then( + (value) async => await (wallet).open(), + ); } else { loadFuture = wallet.init(); } @@ -152,10 +154,9 @@ class _DesktopWalletSummaryRowState ); if (mounted) { - await Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: wallet.walletId, - ); + await Navigator.of( + context, + ).pushNamed(DesktopWalletView.routeName, arguments: wallet.walletId); } } } finally { @@ -173,40 +174,41 @@ class _DesktopWalletSummaryRowState if (mounted) { await showDialog( context: context, - builder: (_) => DesktopDialog( - maxHeight: 600, - maxWidth: 700, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (_) => DesktopDialog( + maxHeight: 600, + maxWidth: 700, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "${widget.coin.prettyName} (${widget.coin.ticker}) wallets", - style: STextStyles.desktopH3(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "${widget.coin.prettyName} (${widget.coin.ticker}) wallets", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: WalletsOverview( + coin: widget.coin, + navigatorState: Navigator.of(context), + ), ), ), - const DesktopDialogCloseButton(), ], ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: WalletsOverview( - coin: widget.coin, - navigatorState: Navigator.of(context), - ), - ), - ), - ], - ), - ), + ), ); } } finally { @@ -216,6 +218,12 @@ class _DesktopWalletSummaryRowState @override Widget build(BuildContext context) { + final price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(widget.coin), + ), + ); + return Breathing( child: RoundedWhiteContainer( padding: const EdgeInsets.all(20), @@ -229,15 +237,11 @@ class _DesktopWalletSummaryRowState child: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(widget.coin)), - ), + File(ref.watch(coinIconProvider(widget.coin))), width: 28, height: 28, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Text( widget.coin.prettyName, style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -260,12 +264,11 @@ class _DesktopWalletSummaryRowState ), ), ), - Expanded( - flex: 6, - child: TablePriceInfo( - coin: widget.coin, + if (price != null) + Expanded( + flex: 6, + child: TablePriceInfo(coin: widget.coin, price: price), ), - ), ], ), ), @@ -274,43 +277,30 @@ class _DesktopWalletSummaryRowState } class TablePriceInfo extends ConsumerWidget { - const TablePriceInfo({super.key, required this.coin}); + const TablePriceInfo({super.key, required this.coin, required this.price}); final CryptoCurrency coin; + final ({Decimal value, double change24h}) price; @override Widget build(BuildContext context, WidgetRef ref) { - final tuple = ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ); - final currency = ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), + prefsChangeNotifierProvider.select((value) => value.currency), ); final priceString = Amount.fromDecimal( - tuple.item1, + price.value, fractionDigits: 2, ).fiatString( - locale: ref - .watch( - localeServiceChangeNotifierProvider.notifier, - ) - .locale, + locale: ref.watch(localeServiceChangeNotifierProvider.notifier).locale, ); - final double percentChange = tuple.item2; - var percentChangedColor = Theme.of(context).extension()!.textDark; - if (percentChange > 0) { + if (price.change24h > 0) { percentChangedColor = Theme.of(context).extension()!.accentColorGreen; - } else if (percentChange < 0) { + } else if (price.change24h < 0) { percentChangedColor = Theme.of(context).extension()!.accentColorRed; } @@ -325,10 +315,10 @@ class TablePriceInfo extends ConsumerWidget { ), ), Text( - "${percentChange.toStringAsFixed(2)}%", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: percentChangedColor, - ), + "${price.change24h.toStringAsFixed(2)}%", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith(color: percentChangedColor), ), ], ); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart index e30245c4d..577d4e322 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart @@ -12,13 +12,10 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; + import '../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; -import '../../../pages/token_view/sub_widgets/token_summary.dart'; import '../../../pages/token_view/sub_widgets/token_transaction_list_widget.dart'; import '../../../pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart'; -import 'sub_widgets/desktop_wallet_features.dart'; -import 'sub_widgets/desktop_wallet_summary.dart'; -import 'sub_widgets/my_wallet.dart'; import '../../../providers/providers.dart'; import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import '../../../themes/stack_colors.dart'; @@ -26,20 +23,20 @@ import '../../../utilities/assets.dart'; import '../../../utilities/text_styles.dart'; import '../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../widgets/coin_ticker_tag.dart'; import '../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_scaffold.dart'; import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/icon_widgets/eth_token_icon.dart'; import '../../../widgets/rounded_white_container.dart'; +import 'sub_widgets/desktop_wallet_features.dart'; +import 'sub_widgets/desktop_wallet_summary.dart'; +import 'sub_widgets/my_wallet.dart'; /// [eventBus] should only be set during testing class DesktopTokenView extends ConsumerStatefulWidget { - const DesktopTokenView({ - super.key, - required this.walletId, - this.eventBus, - }); + const DesktopTokenView({super.key, required this.walletId, this.eventBus}); static const String routeName = "/desktopTokenView"; @@ -57,9 +54,10 @@ class _DesktopTokenViewState extends ConsumerState { @override void initState() { - initialSyncStatus = ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced; + initialSyncStatus = + ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced; super.initState(); } @@ -79,32 +77,26 @@ class _DesktopTokenViewState extends ConsumerState { flex: 3, child: Row( children: [ - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), SecondaryButton( - padding: const EdgeInsets.only( - left: 12, - right: 18, - ), + padding: const EdgeInsets.only(left: 12, right: 18), buttonHeight: ButtonHeight.s, label: ref.watch(pWalletName(widget.walletId)), icon: SvgPicture.asset( Assets.svg.arrowLeft, width: 18, height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: () { ref.refresh(feeSheetSessionCacheProvider); Navigator.of(context).pop(); }, ), - const SizedBox( - width: 15, - ), + const SizedBox(width: 15), ], ), ), @@ -120,9 +112,7 @@ class _DesktopTokenViewState extends ConsumerState { ), size: 32, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Text( ref.watch( pCurrentTokenWallet.select( @@ -131,11 +121,11 @@ class _DesktopTokenViewState extends ConsumerState { ), style: STextStyles.desktopH3(context), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), CoinTickerTag( - walletId: widget.walletId, + ticker: ref.watch( + pWalletCoin(widget.walletId).select((s) => s.ticker), + ), ), ], ), @@ -159,30 +149,25 @@ class _DesktopTokenViewState extends ConsumerState { ), size: 40, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), DesktopWalletSummary( walletId: widget.walletId, isToken: true, - initialSyncStatus: ref - .watch(pWallets) - .getWallet(widget.walletId) - .refreshMutex - .isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, + initialSyncStatus: + ref + .watch(pWallets) + .getWallet(widget.walletId) + .refreshMutex + .isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, ), const Spacer(), - DesktopWalletFeatures( - walletId: widget.walletId, - ), + DesktopWalletFeatures(walletId: widget.walletId), ], ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Row( children: [ SizedBox( @@ -190,26 +175,27 @@ class _DesktopTokenViewState extends ConsumerState { child: Text( "My wallet", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconLeft, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Recent transactions", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconLeft, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, ), ), CustomTextButton( @@ -233,9 +219,7 @@ class _DesktopTokenViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 14, - ), + const SizedBox(height: 14), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -251,13 +235,9 @@ class _DesktopTokenViewState extends ConsumerState { ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( - child: TokenTransactionsList( - walletId: widget.walletId, - ), + child: TokenTransactionsList(walletId: widget.walletId), ), ], ), 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 2cf41f06a..96363f1e2 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 @@ -28,7 +28,6 @@ import '../../../pages/wallet_view/sub_widgets/transactions_list.dart'; import '../../../pages/wallet_view/transaction_views/all_transactions_view.dart'; import '../../../pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart'; import '../../../pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/active_wallet_provider.dart'; import '../../../providers/global/auto_swb_service_provider.dart'; import '../../../providers/providers.dart'; @@ -46,6 +45,7 @@ import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/wallet/impl/banano_wallet.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_buttons/blue_text_button.dart'; @@ -57,6 +57,7 @@ import '../../coin_control/desktop_coin_control_use_dialog.dart'; import 'sub_widgets/desktop_wallet_features.dart'; import 'sub_widgets/desktop_wallet_summary.dart'; import 'sub_widgets/firo_desktop_wallet_summary.dart'; +import 'sub_widgets/mweb_desktop_wallet_summary.dart'; import 'sub_widgets/my_wallet.dart'; import 'sub_widgets/network_info_button.dart'; import 'sub_widgets/wallet_keys_button.dart'; @@ -423,73 +424,54 @@ class DesktopWalletHeaderRow extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return RoundedWhiteContainer( padding: const EdgeInsets.all(20), - child: - wallet is FiroWallet && MediaQuery.of(context).size.width < 1600 - ? Column( - children: [ - Row( - children: [ - SvgPicture.file( - File(ref.watch(coinIconProvider(wallet.info.coin))), - width: 40, - height: 40, - ), - const SizedBox(width: 10), - FiroDesktopWalletSummary( - walletId: wallet.walletId, - initialSyncStatus: - wallet.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), - - const Spacer(), - ], - ), - const SizedBox(height: 10), - Row( - children: [ - DesktopWalletFeatures(walletId: wallet.walletId), - ], - ), - ], - ) - : Row( - children: [ - if (monke != null) - SvgPicture.memory( - Uint8List.fromList(monke!), - width: 60, - height: 60, - ), - if (monke == null) - SvgPicture.file( - File(ref.watch(coinIconProvider(wallet.info.coin))), - width: 40, - height: 40, - ), - const SizedBox(width: 10), - if (wallet is FiroWallet) - FiroDesktopWalletSummary( - walletId: wallet.walletId, - initialSyncStatus: - wallet.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), + child: Row( + children: [ + if (monke != null) + SvgPicture.memory( + Uint8List.fromList(monke!), + width: 60, + height: 60, + ), + if (monke == null) + SvgPicture.file( + File(ref.watch(coinIconProvider(wallet.info.coin))), + width: 40, + height: 40, + ), + const SizedBox(width: 10), + if (wallet is FiroWallet) + FiroDesktopWalletSummary( + walletId: wallet.walletId, + initialSyncStatus: + wallet.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), - if (wallet is! FiroWallet) - DesktopWalletSummary( - walletId: wallet.walletId, - initialSyncStatus: - wallet.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), - const Spacer(), - DesktopWalletFeatures(walletId: wallet.walletId), - ], - ), + if (wallet is! FiroWallet) + wallet is MwebInterface && + ref.watch( + pWalletInfo( + wallet.walletId, + ).select((s) => s.isMwebEnabled), + ) + ? MwebDesktopWalletSummary( + walletId: wallet.walletId, + initialSyncStatus: + wallet.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ) + : DesktopWalletSummary( + walletId: wallet.walletId, + initialSyncStatus: + wallet.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + Expanded(child: DesktopWalletFeatures(walletId: wallet.walletId)), + ], + ), ); } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart index 16032b36b..032a61bf4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -10,12 +10,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stack_wallet_backup/secure_storage.dart'; import 'package:tuple/tuple.dart'; import '../../../../app_config.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_view_only_wallet_keys_view.dart'; import '../../../../providers/global/wallets_provider.dart'; +import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/logger.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; @@ -24,13 +27,11 @@ import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/rounded_container.dart'; +import '../../../../widgets/rounded_white_container.dart'; import 'delete_wallet_keys_popup.dart'; class DesktopAttentionDeleteWallet extends ConsumerStatefulWidget { - const DesktopAttentionDeleteWallet({ - super.key, - required this.walletId, - }); + const DesktopAttentionDeleteWallet({super.key, required this.walletId}); final String walletId; @@ -55,10 +56,7 @@ class _DesktopAttentionDeleteWallet children: [ DesktopDialogCloseButton( onPressedOverride: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); }, ), ], @@ -67,17 +65,13 @@ class _DesktopAttentionDeleteWallet padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 26), child: Column( children: [ - Text( - "Attention!", - style: STextStyles.desktopH2(context), - ), - const SizedBox( - height: 16, - ), + Text("Attention!", style: STextStyles.desktopH2(context)), + const SizedBox(height: 16), RoundedContainer( - color: Theme.of(context) - .extension()! - .snackBarBackError, + color: + Theme.of( + context, + ).extension()!.snackBarBackError, child: Padding( padding: const EdgeInsets.all(10.0), child: Text( @@ -85,11 +79,13 @@ class _DesktopAttentionDeleteWallet "the only way you can have access to your funds is by using your backup key." "\n\n${AppConfig.appName} does not keep nor is able to restore your backup key or your wallet." "\n\nPLEASE SAVE YOUR BACKUP KEY.", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .snackBarTextError, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.snackBarTextError, ), ), ), @@ -103,10 +99,7 @@ class _DesktopAttentionDeleteWallet buttonHeight: ButtonHeight.xl, label: "Cancel", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); }, ), const SizedBox(width: 16), @@ -115,71 +108,96 @@ class _DesktopAttentionDeleteWallet buttonHeight: ButtonHeight.xl, label: "View Backup Key", onPressed: () async { - final wallet = - ref.read(pWallets).getWallet(widget.walletId); + try { + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId); - if (wallet is ViewOnlyOptionInterface && - wallet.isViewOnly) { - final data = await wallet.getViewOnlyWalletData(); - if (context.mounted) { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (builder) => DesktopDialog( - maxWidth: 614, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, + if (wallet is ViewOnlyOptionInterface && + wallet.isViewOnly) { + final data = await wallet.getViewOnlyWalletData(); + if (context.mounted) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: + (builder) => DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.only( + left: 32, + ), + child: Text( + "Wallet keys", + style: + STextStyles.desktopH3( + context, + ), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ], ), - child: Text( - "Wallet keys", - style: STextStyles.desktopH3( - context, - ), + Padding( + padding: const EdgeInsets.all(32), + child: + DeleteViewOnlyWalletKeysView( + walletId: widget.walletId, + data: data, + ), ), - ), - DesktopDialogCloseButton( - onPressedOverride: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); - }, - ), - ], - ), - Padding( - padding: const EdgeInsets.all(32), - child: DeleteViewOnlyWalletKeysView( - walletId: widget.walletId, - data: data, + ], ), ), - ], - ), ), - ), - ); - } - } else + ); + } + } else + // TODO: [prio=med] handle other types wallet deletion + // All wallets currently are mnemonic based + if (wallet is MnemonicInterface) { + final words = await wallet.getMnemonicAsWords(); - // TODO: [prio=med] handle other types wallet deletion - // All wallets currently are mnemonic based - if (wallet is MnemonicInterface) { - final words = await wallet.getMnemonicAsWords(); + if (context.mounted) { + await Navigator.of(context).pushNamed( + DeleteWalletKeysPopup.routeName, + arguments: Tuple2(widget.walletId, words), + ); + } + } + } on BadDecryption catch (e, s) { + Logging.instance.f( + "Desktop wallet delete error. Showing decryption error continue dialog.", + error: e, + stackTrace: s, + ); if (context.mounted) { - await Navigator.of(context).pushNamed( - DeleteWalletKeysPopup.routeName, - arguments: Tuple2( - widget.walletId, - words, + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return ErrorLoadingKeysDialog( + walletId: widget.walletId, + ); + }, + settings: const RouteSettings( + name: "/desktopErrorLoadingKeysDialog", + ), ), ); } @@ -196,3 +214,69 @@ class _DesktopAttentionDeleteWallet ); } } + +class ErrorLoadingKeysDialog extends StatelessWidget { + const ErrorLoadingKeysDialog({super.key, required this.walletId}); + + final String walletId; + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 614, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Decryption error", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ], + ), + Padding( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "Could not retrieve wallet keys/mnemonic phrase/seed.\n\n" + "Are you certain you would like to continue with wallet deletion?", + style: STextStyles.label(context).copyWith(fontSize: 16), + ), + ), + const SizedBox(height: 40), + PrimaryButton( + label: "Continue", + onPressed: () async { + await Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return ConfirmDelete(walletId: walletId); + }, + settings: const RouteSettings( + name: "/desktopConfirmDelete", + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart index 5f4dcf574..f2c5429db 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart @@ -17,13 +17,9 @@ import '../../../../themes/stack_colors.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/wallet_balance_toggle_state.dart'; import '../../../../utilities/text_styles.dart'; -import '../../../../wallets/isar/providers/wallet_info_provider.dart'; class DesktopBalanceToggleButton extends ConsumerWidget { - const DesktopBalanceToggleButton({ - super.key, - this.onPressed, - }); + const DesktopBalanceToggleButton({super.key, this.onPressed}); final VoidCallback? onPressed; @@ -86,10 +82,6 @@ class DesktopPrivateBalanceToggleButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final currentType = ref.watch(publicPrivateBalanceStateProvider); - final showLelantus = - ref.watch(pWalletBalanceSecondary(walletId)).spendable.raw > - BigInt.zero; - return SizedBox( height: 22, width: 80, @@ -97,32 +89,14 @@ class DesktopPrivateBalanceToggleButton extends ConsumerWidget { color: Theme.of(context).extension()!.buttonBackSecondary, splashColor: Theme.of(context).extension()!.highlight, onPressed: () { - if (showLelantus) { - switch (currentType) { - case FiroType.public: - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.lelantus; - break; - - case FiroType.lelantus: - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; - break; - - case FiroType.spark: - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; - break; - } + if (currentType != BalanceType.private) { + ref.read(publicPrivateBalanceStateProvider.state).state = + BalanceType.private; } else { - if (currentType != FiroType.spark) { - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; - } else { - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; - } + ref.read(publicPrivateBalanceStateProvider.state).state = + BalanceType.public; } + onPressed?.call(); }, elevation: 0, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart deleted file mode 100644 index 6326a6474..000000000 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart +++ /dev/null @@ -1,447 +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:dropdown_button2/dropdown_button2.dart'; -import 'package:flutter/material.dart'; -import 'package:cs_monero/cs_monero.dart' as lib_monero; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; - -import '../../../../models/models.dart'; -import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; -import '../../../../providers/global/wallets_provider.dart'; -import '../../../../providers/ui/fee_rate_type_state_provider.dart'; -import '../../../../providers/wallet/public_private_balance_state_provider.dart'; -import '../../../../themes/stack_colors.dart'; -import '../../../../utilities/amount/amount.dart'; -import '../../../../utilities/amount/amount_formatter.dart'; -import '../../../../utilities/assets.dart'; -import '../../../../utilities/constants.dart'; -import '../../../../utilities/enums/fee_rate_type_enum.dart'; -import '../../../../utilities/text_styles.dart'; -import '../../../../wallets/crypto_currency/crypto_currency.dart'; -import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; -import '../../../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../../../wallets/wallet/impl/firo_wallet.dart'; -import '../../../../widgets/animated_text.dart'; - -final tokenFeeSessionCacheProvider = - ChangeNotifierProvider((ref) { - return FeeSheetSessionCache(); -}); - -class DesktopFeeDropDown extends ConsumerStatefulWidget { - const DesktopFeeDropDown({ - super.key, - required this.walletId, - this.isToken = false, - }); - - final String walletId; - final bool isToken; - - @override - ConsumerState createState() => _DesktopFeeDropDownState(); -} - -class _DesktopFeeDropDownState extends ConsumerState { - late final String walletId; - - FeeObject? feeObject; - FeeRateType feeRateType = FeeRateType.average; - - final stringsToLoopThrough = [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ]; - - Future feeFor({ - required Amount amount, - required FeeRateType feeRateType, - required int feeRate, - required CryptoCurrency coin, - }) async { - switch (feeRateType) { - case FeeRateType.fast: - if (ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .fast[amount] == - null) { - if (widget.isToken == false) { - final wallet = ref.read(pWallets).getWallet(walletId); - - if (coin is Monero || coin is Wownero) { - final fee = await wallet.estimateFeeFor( - amount, - lib_monero.TransactionPriority.high.value, - ); - ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; - } else if (coin is Firo) { - final Amount fee; - switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); - } - ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; - } else { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - await wallet.estimateFeeFor(amount, feeRate); - } - } else { - final tokenWallet = ref.read(pCurrentTokenWallet)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(tokenFeeSessionCacheProvider).fast[amount] = fee; - } - } - return ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .fast[amount]!; - - case FeeRateType.average: - if (ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .average[amount] == - null) { - if (widget.isToken == false) { - final wallet = ref.read(pWallets).getWallet(walletId); - - if (coin is Monero || coin is Wownero) { - final fee = await wallet.estimateFeeFor( - amount, - lib_monero.TransactionPriority.medium.value, - ); - ref.read(feeSheetSessionCacheProvider).average[amount] = fee; - } else if (coin is Firo) { - final Amount fee; - switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); - } - ref.read(feeSheetSessionCacheProvider).average[amount] = fee; - } else { - ref.read(feeSheetSessionCacheProvider).average[amount] = - await wallet.estimateFeeFor(amount, feeRate); - } - } else { - final tokenWallet = ref.read(pCurrentTokenWallet)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(tokenFeeSessionCacheProvider).average[amount] = fee; - } - } - return ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .average[amount]!; - - case FeeRateType.slow: - if (ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .slow[amount] == - null) { - if (widget.isToken == false) { - final wallet = ref.read(pWallets).getWallet(walletId); - - if (coin is Monero || coin is Wownero) { - final fee = await wallet.estimateFeeFor( - amount, - lib_monero.TransactionPriority.normal.value, - ); - ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; - } else if (coin is Firo) { - final Amount fee; - switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); - } - ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; - } else { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - await wallet.estimateFeeFor(amount, feeRate); - } - } else { - final tokenWallet = ref.read(pCurrentTokenWallet)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(tokenFeeSessionCacheProvider).slow[amount] = fee; - } - } - return ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .slow[amount]!; - default: - return Amount.zero; - } - } - - @override - void initState() { - walletId = widget.walletId; - super.initState(); - } - - String? labelSlow; - String? labelAverage; - String? labelFast; - - @override - Widget build(BuildContext context) { - debugPrint("BUILD: $runtimeType"); - - final wallet = - ref.watch(pWallets.select((value) => value.getWallet(walletId))); - - return FutureBuilder( - future: wallet.fees, - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - feeObject = snapshot.data!; - } - return DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - value: ref.watch(feeRateTypeStateProvider.state).state, - items: [ - ...FeeRateType.values.map( - (e) => DropdownMenuItem( - value: e, - child: FeeDropDownChild( - feeObject: feeObject, - feeRateType: e, - walletId: walletId, - feeFor: feeFor, - isSelected: false, - ), - ), - ), - ], - onChanged: (newRateType) { - if (newRateType is FeeRateType) { - ref.read(feeRateTypeStateProvider.state).state = newRateType; - } - }, - iconStyleData: IconStyleData( - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context).extension()!.textDark3, - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, -10), - elevation: 0, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), - ); - }, - ); - } -} - -final sendAmountProvider = - StateProvider.autoDispose((_) => Amount.zero); - -class FeeDropDownChild extends ConsumerWidget { - const FeeDropDownChild({ - super.key, - required this.feeObject, - required this.feeRateType, - required this.walletId, - required this.feeFor, - required this.isSelected, - }); - - final FeeObject? feeObject; - final FeeRateType feeRateType; - final String walletId; - final Future Function({ - required Amount amount, - required FeeRateType feeRateType, - required int feeRate, - required CryptoCurrency coin, - }) feeFor; - final bool isSelected; - - static const stringsToLoopThrough = [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ]; - - String estimatedTimeToBeIncludedInNextBlock( - int targetBlockTime, - int estimatedNumberOfBlocks, - ) { - final int time = targetBlockTime * estimatedNumberOfBlocks; - - final int hours = (time / 3600).floor(); - if (hours > 1) { - return "~$hours hours"; - } else if (hours == 1) { - return "~$hours hour"; - } - - // less than an hour - - final string = (time / 60).toStringAsFixed(1); - - if (string == "1.0") { - return "~1 minute"; - } else { - if (string.endsWith(".0")) { - return "~${(time / 60).floor()} minutes"; - } - return "~$string minutes"; - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - debugPrint("BUILD: $runtimeType : $feeRateType"); - - final coin = ref.watch(pWalletCoin(walletId)); - - if (feeObject == null) { - return AnimatedText( - stringsToLoopThrough: stringsToLoopThrough, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textFieldActiveText, - ), - ); - } else { - return FutureBuilder( - future: feeFor( - coin: coin, - feeRateType: feeRateType, - feeRate: feeRateType == FeeRateType.fast - ? feeObject!.fast - : feeRateType == FeeRateType.slow - ? feeObject!.slow - : feeObject!.medium, - amount: ref.watch(sendAmountProvider.state).state, - ), - builder: (_, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${feeRateType.prettyName} " - "(~${ref.watch(pAmountFormatter(coin)).format( - snapshot.data!, - indicatePrecisionLoss: false, - )})", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - textAlign: TextAlign.left, - ), - if (feeObject != null) - Text( - coin is Ethereum - ? "" - : estimatedTimeToBeIncludedInNextBlock( - coin.targetBlockTimeSeconds, - feeRateType == FeeRateType.fast - ? feeObject!.numberOfBlocksFast - : feeRateType == FeeRateType.slow - ? feeObject!.numberOfBlocksSlow - : feeObject!.numberOfBlocksAverage, - ), - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - ), - ], - ); - } else { - return AnimatedText( - stringsToLoopThrough: stringsToLoopThrough, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - ); - } - }, - ); - } - } -} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index b90cc2f6a..5cea18ff2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -22,7 +22,6 @@ import '../../../../models/isar/models/isar_models.dart'; import '../../../../models/keys/view_only_wallet_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; @@ -31,6 +30,7 @@ import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; @@ -41,6 +41,7 @@ import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../widgets/conditional_parent.dart'; @@ -72,6 +73,7 @@ class _DesktopReceiveState extends ConsumerState { late final String walletId; late final ClipboardInterface clipboard; late final bool supportsSpark; + late bool supportsMweb; late final bool showMultiType; int _currentIndex = 0; @@ -91,10 +93,9 @@ class _DesktopReceiveState extends ConsumerState { return WillPopScope( onWillPop: () async => shouldPop, child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -109,8 +110,9 @@ class _DesktopReceiveState extends ConsumerState { if (wallet is Bip39HDWallet && wallet is! BCashInterface) { DerivePathType? type; if (wallet.isViewOnly && wallet is ExtendedKeysInterface) { - final voData = await wallet.getViewOnlyWalletData() - as ExtendedKeysViewOnlyWalletData; + final voData = + await wallet.getViewOnlyWalletData() + as ExtendedKeysViewOnlyWalletData; for (final t in wallet.cryptoCurrency.supportedDerivationPathTypes) { final testPath = wallet.cryptoCurrency.constructDerivePath( derivePathType: t, @@ -168,10 +170,9 @@ class _DesktopReceiveState extends ConsumerState { return WillPopScope( onWillPop: () async => shouldPop, child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -198,18 +199,50 @@ class _DesktopReceiveState extends ConsumerState { } } + Future
_generateNewMwebAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId) as MwebInterface; + + final address = await wallet.generateNextMwebAddress(); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address); + }); + + return address; + } + + Future generateNewMwebAddress() async { + final address = await showLoading
( + whileFuture: _generateNewMwebAddress(), + context: context, + message: "Generating address", + rootNavigator: Util.isDesktop, + ); + + if (mounted && address != null) { + setState(() { + _addressMap[AddressType.mweb] = address.value; + }); + } + } + @override void initState() { walletId = widget.walletId; coin = ref.read(pWalletInfo(walletId)).coin; clipboard = widget.clipboard; final wallet = ref.read(pWallets).getWallet(walletId); - supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; + supportsSpark = wallet is SparkInterface; + supportsMweb = + wallet is MwebInterface && + !wallet.info.isViewOnly && + wallet.info.isMwebEnabled; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { showMultiType = false; } else { - showMultiType = supportsSpark || + showMultiType = + supportsSpark || + supportsMweb || (wallet is! BCashInterface && wallet is Bip39HDWallet && wallet.supportedAddressTypes.length > 1); @@ -222,10 +255,14 @@ class _DesktopReceiveState extends ConsumerState { _walletAddressTypes.insert(0, AddressType.spark); } else { _walletAddressTypes.addAll( - (wallet as Bip39HDWallet) - .supportedAddressTypes - .where((e) => e != wallet.info.mainAddressType), + (wallet as Bip39HDWallet).supportedAddressTypes.where( + (e) => e != wallet.info.mainAddressType, + ), ); + + if (supportsMweb) { + _walletAddressTypes.insert(0, AddressType.mweb); + } } } @@ -233,8 +270,9 @@ class _DesktopReceiveState extends ConsumerState { _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); } - _addressMap[_walletAddressTypes[_currentIndex]] = - ref.read(pWalletReceivingAddress(walletId)); + _addressMap[_walletAddressTypes[_currentIndex]] = ref.read( + pWalletReceivingAddress(walletId), + ); if (showMultiType) { for (final type in _walletAddressTypes) { @@ -246,19 +284,22 @@ class _DesktopReceiveState extends ConsumerState { .walletIdEqualTo(walletId) .filter() .typeEqualTo(type) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) .sortByDerivationIndexDesc() .findFirst() .asStream() .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _addressMap[type] = - event?.value ?? _addressMap[type] ?? "[No address yet]"; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[type] = + event?.value ?? _addressMap[type] ?? "[No address yet]"; + }); + } }); - } - }); - }); + }); } } @@ -277,6 +318,57 @@ class _DesktopReceiveState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + ref.listen(pWalletInfo(walletId), (prev, next) { + if (prev?.isMwebEnabled != next.isMwebEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + supportsMweb = next.isMwebEnabled; + + if (supportsMweb && + !_walletAddressTypes.contains(AddressType.mweb)) { + _walletAddressTypes.insert(0, AddressType.mweb); + _addressSubMap[AddressType.mweb] = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[AddressType.mweb] = + event?.value ?? + _addressMap[AddressType.mweb] ?? + "[No address yet]"; + }); + } + }); + }); + } else { + _walletAddressTypes.remove(AddressType.mweb); + _addressSubMap[AddressType.mweb]?.cancel(); + _addressSubMap.remove(AddressType.mweb); + } + + if (_currentIndex >= _walletAddressTypes.length) { + _currentIndex = _walletAddressTypes.length - 1; + } + }); + } + }); + } + }); + final String address; if (showMultiType) { address = _addressMap[_walletAddressTypes[_currentIndex]]!; @@ -284,8 +376,9 @@ class _DesktopReceiveState extends ConsumerState { address = ref.watch(pWalletReceivingAddress(walletId)); } - final wallet = - ref.watch(pWallets.select((value) => value.getWallet(walletId))); + final wallet = ref.watch( + pWallets.select((value) => value.getWallet(walletId)), + ); final bool canGen; if (wallet is ViewOnlyOptionInterface && @@ -293,7 +386,8 @@ class _DesktopReceiveState extends ConsumerState { wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { canGen = false; } else { - canGen = (wallet is MultiAddressInterface || supportsSpark); + canGen = + (wallet is MultiAddressInterface || supportsSpark || supportsMweb); } return Column( @@ -301,91 +395,90 @@ class _DesktopReceiveState extends ConsumerState { children: [ ConditionalParent( condition: showMultiType, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - DropdownButtonHideUnderline( - child: DropdownButton2( - value: _currentIndex, - items: [ - for (int i = 0; i < _walletAddressTypes.length; i++) - DropdownMenuItem( - value: i, - child: Text( - supportsSpark && - _walletAddressTypes[i] == AddressType.p2pkh - ? "Transparent address" - : "${_walletAddressTypes[i].readableName} address", - style: STextStyles.w500_14(context), + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _currentIndex, + items: [ + for (int i = 0; i < _walletAddressTypes.length; i++) + DropdownMenuItem( + value: i, + child: Text( + supportsSpark && + _walletAddressTypes[i] == + AddressType.p2pkh + ? "Transparent address" + : "${_walletAddressTypes[i].readableName} address", + style: STextStyles.w500_14(context), + ), + ), + ], + onChanged: (value) { + if (value != null && value != _currentIndex) { + setState(() { + _currentIndex = value; + }); + } + }, + isExpanded: true, + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), ), ), - ], - onChanged: (value) { - if (value != null && value != _currentIndex) { - setState(() { - _currentIndex = value; - }); - } - }, - isExpanded: true, - iconStyleData: IconStyleData( - icon: Padding( - padding: const EdgeInsets.only(right: 10), - child: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), ), - ), - ), - 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, + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), ), - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, -10), - elevation: 0, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), ), ), ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), + const SizedBox(height: 12), + child, + ], ), - const SizedBox( - height: 12, - ), - child, - ], - ), child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { - clipboard.setData( - ClipboardData( - text: address, - ), - ); + clipboard.setData(ClipboardData(text: address)); showFloatingFlushBar( type: FlushBarType.info, message: "Copied to clipboard", @@ -396,9 +489,10 @@ class _DesktopReceiveState extends ConsumerState { child: Container( decoration: BoxDecoration( border: Border.all( - color: Theme.of(context) - .extension()! - .backgroundAppBar, + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, width: 1, ), borderRadius: BorderRadius.circular( @@ -411,11 +505,7 @@ class _DesktopReceiveState extends ConsumerState { Row( children: [ Text( - "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( - pCurrentTokenWallet.select( - (value) => value!.tokenContract.symbol, - ), - )} address", + "Your ${widget.contractAddress == null ? coin.ticker : ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract.symbol))} address", style: STextStyles.itemSubtitle(context), ), const Spacer(), @@ -425,24 +515,18 @@ class _DesktopReceiveState extends ConsumerState { Assets.svg.copy, width: 15, height: 15, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), + const SizedBox(width: 4), + Text("Copy", style: STextStyles.link2(context)), ], ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Row( children: [ Expanded( @@ -451,9 +535,10 @@ class _DesktopReceiveState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ), @@ -467,85 +552,78 @@ class _DesktopReceiveState extends ConsumerState { ), ), - if (canGen) - const SizedBox( - height: 20, - ), + if (canGen) const SizedBox(height: 20), if (canGen) SecondaryButton( buttonHeight: ButtonHeight.l, - onPressed: supportsSpark && - _walletAddressTypes[_currentIndex] == AddressType.spark - ? generateNewSparkAddress - : generateNewAddress, + onPressed: + supportsMweb && + _walletAddressTypes[_currentIndex] == AddressType.mweb + ? generateNewMwebAddress + : supportsSpark && + _walletAddressTypes[_currentIndex] == AddressType.spark + ? generateNewSparkAddress + : generateNewAddress, label: "Generate new address", ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), Center( child: QR( - data: AddressUtils.buildUriString( - coin.uriScheme, - address, - {}, - ), + data: AddressUtils.buildUriString(coin.uriScheme, address, {}), size: 200, ), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), // TODO: create transparent button class to account for hover GestureDetector( onTap: () async { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - children: [ - Row( + builder: + (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( children: [ - const AppBarBackButton( - size: 40, - iconSize: 24, + Row( + children: [ + const AppBarBackButton(size: 40, iconSize: 24), + Text( + "Generate QR code", + style: STextStyles.desktopH3(context), + ), + ], ), - Text( - "Generate QR code", - style: STextStyles.desktopH3(context), + IntrinsicHeight( + child: Navigator( + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: + (_, __) => [ + RouteGenerator.generateRoute( + RouteSettings( + name: GenerateUriQrCodeView.routeName, + arguments: Tuple2(coin, address), + ), + ), + ], + ), ), ], ), - IntrinsicHeight( - child: Navigator( - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) => [ - RouteGenerator.generateRoute( - RouteSettings( - name: GenerateUriQrCodeView.routeName, - arguments: Tuple2(coin, address), - ), - ), - ], - ), - ), - ], - ), - ), + ), ); } else { unawaited( Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: address, - ), + builder: + (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: address, + ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, ), @@ -564,21 +642,21 @@ class _DesktopReceiveState extends ConsumerState { Assets.svg.qrcode, width: 14, height: 16, - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - const SizedBox( - width: 8, + color: + Theme.of( + context, + ).extension()!.accentColorBlue, ), + const SizedBox(width: 8), Padding( padding: const EdgeInsets.only(bottom: 2), child: Text( "Create new QR code", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, + color: + Theme.of( + context, + ).extension()!.accentColorBlue, ), ), ), 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 009893e7e..b4c183c43 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 @@ -10,7 +10,6 @@ import 'dart:async'; -import 'package:cs_monero/cs_monero.dart' as lib_monero; import 'package:decimal/decimal.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; @@ -18,6 +17,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../../../../models/isar/models/blockchain_data/address.dart'; import '../../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../../models/isar/models/contact_entry.dart'; import '../../../../models/paynym/paynym_account_lite.dart'; @@ -28,7 +28,9 @@ import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet. import '../../../../providers/providers.dart'; import '../../../../providers/ui/fee_rate_type_state_provider.dart'; import '../../../../providers/ui/preview_tx_button_state_provider.dart'; +import '../../../../providers/wallet/desktop_fee_providers.dart'; import '../../../../providers/wallet/public_private_balance_state_provider.dart'; +import '../../../../services/spark_names_service.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/address_utils.dart'; import '../../../../utilities/amount/amount.dart'; @@ -36,10 +38,8 @@ import '../../../../utilities/amount/amount_formatter.dart'; import '../../../../utilities/amount/amount_input_formatter.dart'; import '../../../../utilities/amount/amount_unit.dart'; import '../../../../utilities/assets.dart'; -import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; -import '../../../../utilities/enums/fee_rate_type_enum.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/prefs.dart'; import '../../../../utilities/text_styles.dart'; @@ -50,20 +50,17 @@ import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/models/tx_data.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; -import '../../../../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; -import '../../../../widgets/animated_text.dart'; -import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; -import '../../../../widgets/desktop/desktop_fee_dialog.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/qr_code_scanner_dialog.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/dialogs/firo_exchange_address_dialog.dart'; -import '../../../../widgets/fee_slider.dart'; +import '../../../../widgets/eth_fee_form.dart'; import '../../../../widgets/icon_widgets/addressbook_icon.dart'; import '../../../../widgets/icon_widgets/clipboard_icon.dart'; import '../../../../widgets/icon_widgets/qrcode_icon.dart'; @@ -74,7 +71,7 @@ import '../../../../widgets/textfield_icon_button.dart'; import '../../../coin_control/desktop_coin_control_use_dialog.dart'; import '../../../desktop_home_view.dart'; import 'address_book_address_chooser/address_book_address_chooser.dart'; -import 'desktop_fee_dropdown.dart'; +import 'desktop_send_fee_form.dart'; class DesktopSend extends ConsumerStatefulWidget { const DesktopSend({ @@ -82,14 +79,13 @@ class DesktopSend extends ConsumerStatefulWidget { required this.walletId, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), + this.accountLite, }); final String walletId; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; final PaynymAccountLite? accountLite; @override @@ -100,13 +96,12 @@ class _DesktopSendState extends ConsumerState { late final String walletId; late final CryptoCurrency coin; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late TextEditingController sendToController; late TextEditingController cryptoAmountController; late TextEditingController baseAmountController; - // late TextEditingController feeController; late TextEditingController memoController; + late TextEditingController nonceController; late final SendViewAutoFillData? _data; @@ -114,6 +109,7 @@ class _DesktopSendState extends ConsumerState { final _cryptoFocus = FocusNode(); final _baseFocus = FocusNode(); final _memoFocus = FocusNode(); + final _nonceFocusNode = FocusNode(); late final bool isStellar; @@ -134,14 +130,7 @@ class _DesktopSendState extends ConsumerState { bool isCustomFee = false; int customFeeRate = 1; - (FeeRateType, String?, String?)? feeSelectionResult; - - final stringsToLoopThrough = [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ]; + EthEIP1559Fee? ethFee; Future scanWebcam() async { try { @@ -155,13 +144,19 @@ class _DesktopSendState extends ConsumerState { try { _processQrCodeData(qrResult); } catch (e, s) { - Logging.instance - .e("Error processing QR code data", error: e, stackTrace: s); + Logging.instance.e( + "Error processing QR code data", + error: e, + stackTrace: s, + ); } } } catch (e, s) { - Logging.instance - .e("Error opening QR code scanner dialog", error: e, stackTrace: s); + Logging.instance.e( + "Error opening QR code scanner dialog", + error: e, + stackTrace: s, + ); } } @@ -170,16 +165,16 @@ class _DesktopSendState extends ConsumerState { final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; - if ((coin is Firo)) { + if (coin is Firo || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: availableBalance = wallet.info.cachedBalance.spendable; break; - case FiroType.lelantus: - availableBalance = wallet.info.cachedBalanceSecondary.spendable; - break; - case FiroType.spark: - availableBalance = wallet.info.cachedBalanceTertiary.spendable; + case BalanceType.private: + availableBalance = + coin is Firo + ? wallet.info.cachedBalanceTertiary.spendable + : wallet.info.cachedBalanceSecondary.spendable; break; } } else { @@ -190,9 +185,7 @@ class _DesktopSendState extends ConsumerState { ref.read(prefsChangeNotifierProvider).enableCoinControl; if (!(wallet is CoinControlInterface && coinControlEnabled) || - (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isEmpty)) { + (coinControlEnabled && ref.read(desktopUseUTXOs).isEmpty)) { // confirm send all if (amount == availableBalance) { final bool? shouldSendAll = await showDialog( @@ -204,10 +197,7 @@ class _DesktopSendState extends ConsumerState { maxWidth: 450, maxHeight: double.infinity, child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, bottom: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -221,29 +211,20 @@ class _DesktopSendState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: Text( "You are about to send your entire balance. Would you like to continue?", textAlign: TextAlign.left, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - fontSize: 18, - ), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith(fontSize: 18), ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: Row( children: [ Expanded( @@ -255,9 +236,7 @@ class _DesktopSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: ButtonHeight.l, @@ -301,11 +280,12 @@ class _DesktopSendState extends ConsumerState { padding: const EdgeInsets.all(32), child: BuildingTransactionDialog( coin: wallet.info.coin, - isSpark: wallet is FiroWallet && + isSpark: + wallet is FiroWallet && ref .read(publicPrivateBalanceStateProvider.state) .state == - FiroType.spark, + BalanceType.private, onCancel: () { wasCancelled = true; @@ -319,11 +299,7 @@ class _DesktopSendState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); TxData txData; Future txDataFuture; @@ -331,29 +307,31 @@ class _DesktopSendState extends ConsumerState { if (isPaynymSend) { final paynymWallet = wallet as PaynymInterface; - final feeRate = ref.read(feeRateTypeStateProvider); + final feeRate = ref.read(feeRateTypeDesktopStateProvider); txDataFuture = paynymWallet.preparePaymentCodeSend( txData: TxData( paynymAccountLite: widget.accountLite!, recipients: [ - ( + TxRecipient( address: widget.accountLite!.code, amount: amount, isChange: false, + addressType: AddressType.unknown, ), ], satsPerVByte: isCustomFee ? customFeeRate : null, feeRateType: feeRate, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) + : null, ), ); } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (ref.read(pValidSparkSendToAddress)) { txDataFuture = wallet.prepareSparkMintTransaction( txData: TxData( @@ -365,104 +343,126 @@ class _DesktopSendState extends ConsumerState { isChange: false, ), ], - feeRateType: ref.read(feeRateTypeStateProvider), + feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (coinControlEnabled && + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) + : null, ), ); } else { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - ( + TxRecipient( address: _address!, amount: amount, isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(_address!)!, ), ], - feeRateType: ref.read(feeRateTypeStateProvider), + feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (coinControlEnabled && + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) + : null, ), ); } break; - case FiroType.lelantus: - txDataFuture = wallet.prepareSendLelantus( - txData: TxData( - recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], - ), - ); - break; - - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: ref.read(pValidSparkSendToAddress) - ? null - : [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], - sparkRecipients: ref.read(pValidSparkSendToAddress) - ? [ - ( - address: _address!, - amount: amount, - memo: memoController.text, - isChange: false, - ), - ] - : null, + recipients: + ref.read(pValidSparkSendToAddress) + ? null + : [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType( + _address!, + )!, + ), + ], + sparkRecipients: + ref.read(pValidSparkSendToAddress) + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + isChange: false, + ), + ] + : null, ), ); break; } + } else if (wallet is MwebInterface && + ref.read(pWalletInfo(walletId)).isMwebEnabled && + ref.read(publicPrivateBalanceStateProvider) == BalanceType.private) { + txDataFuture = wallet.prepareSendMweb( + txData: TxData( + recipients: [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), + ], + feeRateType: ref.read(feeRateTypeDesktopStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + // these will need to be mweb utxos + // utxos: + // (wallet is CoinControlInterface && + // coinControlEnabled && + // ref.read(pDesktopUseUTXOs).isNotEmpty) + // ? ref.read(pDesktopUseUTXOs) + // : null, + ), + ); } else { final memo = isStellar ? memoController.text : null; txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - ( + TxRecipient( address: _address!, amount: amount, isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, ), ], memo: memo, - feeRateType: ref.read(feeRateTypeStateProvider), + feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + nonce: + wallet.cryptoCurrency is Ethereum + ? int.tryParse(nonceController.text) + : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) + : null, + ethEIP1559Fee: ethFee, ), ); } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; @@ -473,35 +473,29 @@ class _DesktopSendState extends ConsumerState { note: _note ?? "PayNym send", ); } else { - txData = txData.copyWith( - note: _note ?? "", - ); + txData = txData.copyWith(note: _note ?? ""); if (coin is Epiccash) { - txData = txData.copyWith( - noteOnChain: _onChainNote ?? "", - ); + txData = txData.copyWith(noteOnChain: _onChainNote ?? ""); } } // pop building dialog - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmTransactionView( - txData: txData, - walletId: walletId, - onSuccess: clearSendForm, - isPaynymTransaction: isPaynymSend, - routeOnSuccessName: DesktopHomeView.routeName, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + txData: txData, + walletId: walletId, + onSuccess: clearSendForm, + isPaynymTransaction: isPaynymSend, + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), ), ); } @@ -509,10 +503,7 @@ class _DesktopSendState extends ConsumerState { Logging.instance.e("Desktop send: ", error: e, stackTrace: s); if (mounted) { // pop building dialog - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( showDialog( @@ -522,10 +513,7 @@ class _DesktopSendState extends ConsumerState { maxWidth: 450, maxHeight: double.infinity, child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, bottom: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -539,25 +527,18 @@ class _DesktopSendState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: Text( e.toString(), textAlign: TextAlign.left, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - fontSize: 18, - ), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith(fontSize: 18), ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), Row( children: [ Expanded( @@ -572,9 +553,7 @@ class _DesktopSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), ], ), ], @@ -593,6 +572,7 @@ class _DesktopSendState extends ConsumerState { cryptoAmountController.text = ""; baseAmountController.text = ""; memoController.text = ""; + nonceController.text = ""; _address = ""; _addressToggleFlag = false; if (mounted) { @@ -602,9 +582,9 @@ class _DesktopSendState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( - cryptoAmountController.text, - ); + final cryptoAmount = ref + .read(pAmountFormatter(coin)) + .tryParse(cryptoAmountController.text); final Amount? amount; if (cryptoAmount != null) { amount = cryptoAmount; @@ -615,9 +595,9 @@ class _DesktopSendState extends ConsumerState { _cachedAmountToSend = amount; final price = - ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin)?.value; - if (price > Decimal.zero) { + if (price != null && price > Decimal.zero) { final String fiatAmountString = (amount.decimal * price) .toAmount(fractionDigits: 2) .fiatString( @@ -685,16 +665,18 @@ class _DesktopSendState extends ConsumerState { } else { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is SparkInterface) { - ref.read(pValidSparkSendToAddress.notifier).state = - SparkInterface.validateSparkAddress( + ref + .read(pValidSparkSendToAddress.notifier) + .state = SparkInterface.validateSparkAddress( address: address ?? "", isTestNet: wallet.cryptoCurrency.network.isTestNet, ); - ref.read(pIsExchangeAddress.state).state = - (coin as Firo).isExchangeAddress(address ?? ""); + ref.read(pIsExchangeAddress.state).state = (coin as Firo) + .isExchangeAddress(address ?? ""); - if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && + if (ref.read(publicPrivateBalanceStateProvider) == + BalanceType.private && ref.read(pIsExchangeAddress) && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -705,8 +687,8 @@ class _DesktopSendState extends ConsumerState { } } - ref.read(pValidSendToAddress.notifier).state = - wallet.cryptoCurrency.validateAddress(address ?? ""); + ref.read(pValidSendToAddress.notifier).state = wallet.cryptoCurrency + .validateAddress(address ?? ""); } } @@ -725,9 +707,9 @@ class _DesktopSendState extends ConsumerState { // autofill amount field if (paymentData.amount != null) { - final amount = Decimal.parse(paymentData.amount!).toAmount( - fractionDigits: coin.fractionDigits, - ); + final amount = Decimal.parse( + paymentData.amount!, + ).toAmount(fractionDigits: coin.fractionDigits); cryptoAmountController.text = ref .read(pAmountFormatter(coin)) .format(amount, withUnitName: false); @@ -744,12 +726,41 @@ class _DesktopSendState extends ConsumerState { } } + Future _checkSparkNameAndOrSetAddress( + String content, { + bool setController = true, + }) async { + void setContent() { + if (setController) { + sendToController.text = content; + } + _address = content; + } + + // check for spark name + if (coin is Firo) { + final address = await SparkNamesService.getAddressFor( + content, + network: coin.network, + ); + if (address != null) { + // found a spark name + sendToController.text = content; + _address = address; + } else { + setContent(); + } + } else { + setContent(); + } + } + Future pasteAddress() async { final ClipboardData? data = await clipboard.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")); + content = content.substring(0, content.indexOf("\n")).trim(); } try { @@ -761,9 +772,8 @@ class _DesktopSendState extends ConsumerState { paymentData.coin?.uriScheme == coin.uriScheme) { _applyUri(paymentData); } else { - content = content.split("\n").first.trim(); if (coin is Epiccash) { - content = AddressUtils().formatAddress(content); + content = AddressUtils().formatEpicCashAddress(content); } sendToController.text = content; @@ -778,11 +788,10 @@ class _DesktopSendState extends ConsumerState { // If parsing fails, treat it as a plain address. if (coin is Epiccash) { // strip http:// and https:// if content contains @ - content = AddressUtils().formatAddress(content); + content = AddressUtils().formatEpicCashAddress(content); } - sendToController.text = content; - _address = content; + await _checkSparkNameAndOrSetAddress(content); // Trigger validation after pasting. _setValidAddressProviders(_address); @@ -818,26 +827,26 @@ class _DesktopSendState extends ConsumerState { final Amount? amount; if (baseAmount != null) { final _price = - ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin)?.value; - if (_price == Decimal.zero) { + if (_price == null || _price == Decimal.zero) { amount = Decimal.zero.toAmount(fractionDigits: coin.fractionDigits); } else { - amount = baseAmount <= Amount.zero - ? Decimal.zero.toAmount(fractionDigits: coin.fractionDigits) - : (baseAmount.decimal / _price) - .toDecimal(scaleOnInfinitePrecision: coin.fractionDigits) - .toAmount(fractionDigits: coin.fractionDigits); + amount = + baseAmount <= Amount.zero + ? Decimal.zero.toAmount(fractionDigits: coin.fractionDigits) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: coin.fractionDigits) + .toAmount(fractionDigits: coin.fractionDigits); } if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } _cachedAmountToSend = amount; - final amountString = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + final amountString = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; @@ -865,46 +874,45 @@ class _DesktopSendState extends ConsumerState { } Amount _selectedUtxosAmount(Set utxos) => Amount( - rawValue: - utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), - fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, - ); + rawValue: utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), + fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, + ); Future _sendAllTapped(bool showCoinControl) async { final Amount amount; if (showCoinControl && ref.read(desktopUseUTXOs).isNotEmpty) { amount = _selectedUtxosAmount(ref.read(desktopUseUTXOs)); - } else if (coin is Firo) { + } else if (coin is Firo || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.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; + case BalanceType.private: + amount = + coin is Firo + ? ref.read(pWalletBalanceTertiary(walletId)).spendable + : ref.read(pWalletBalanceSecondary(walletId)).spendable; break; } } else { amount = ref.read(pWalletBalance(walletId)).spendable; } - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); } void _showDesktopCoinControl() async { final amount = ref.read(pSendAmount); await showDialog( context: context, - builder: (context) => DesktopCoinControlUseDialog( - walletId: widget.walletId, - amountToSend: amount, - ), + builder: + (context) => DesktopCoinControlUseDialog( + walletId: widget.walletId, + amountToSend: amount, + ), ); } @@ -922,14 +930,14 @@ class _DesktopSendState extends ConsumerState { walletId = widget.walletId; coin = ref.read(pWalletInfo(walletId)).coin; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; + isStellar = coin is Stellar; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); baseAmountController = TextEditingController(); memoController = TextEditingController(); - // feeController = TextEditingController(); + nonceController = TextEditingController(); onCryptoAmountChanged = _cryptoAmountChanged; cryptoAmountController.addListener(onCryptoAmountChanged); @@ -983,12 +991,13 @@ class _DesktopSendState extends ConsumerState { cryptoAmountController.dispose(); baseAmountController.dispose(); memoController.dispose(); - // feeController.dispose(); + nonceController.dispose(); _addressFocusNode.dispose(); _cryptoFocus.dispose(); _baseFocus.dispose(); _memoFocus.dispose(); + _nonceFocusNode.dispose(); super.dispose(); } @@ -1015,12 +1024,16 @@ class _DesktopSendState extends ConsumerState { }); } - final firoType = ref.watch(publicPrivateBalanceStateProvider); + final balType = ref.watch(publicPrivateBalanceStateProvider); + final isMwebEnabled = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final showPrivateBalance = coin is Firo || isMwebEnabled; final isExchangeAddress = ref.watch(pIsExchangeAddress); ref.listen(publicPrivateBalanceStateProvider, (previous, next) { if (previous != next && - next == FiroType.spark && + next == BalanceType.private && isExchangeAddress && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -1033,55 +1046,56 @@ class _DesktopSendState extends ConsumerState { } }); - final showCoinControl = ref.watch( + final showCoinControl = + ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, ), ) && ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && - (coin is Firo ? firoType == FiroType.public : true); + (showPrivateBalance ? balType == BalanceType.public : true); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 4, - ), - if (coin is Firo) + const SizedBox(height: 4), + if (showPrivateBalance) Text( "Send from", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - if (coin is Firo) - const SizedBox( - height: 10, - ), - if (coin is Firo) + if (showPrivateBalance) const SizedBox(height: 10), + if (showPrivateBalance) DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, - value: firoType, + value: balType, items: [ DropdownMenuItem( - value: FiroType.spark, + value: BalanceType.private, child: Row( children: [ Text( - "Spark balance", + "Private balance", style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref - .watch(pWalletBalanceTertiary(walletId)) + .watch( + isMwebEnabled + ? pWalletBalanceSecondary(walletId) + : pWalletBalanceTertiary(walletId), + ) .spendable, ), style: STextStyles.itemSubtitle(context), @@ -1089,43 +1103,19 @@ class _DesktopSendState extends ConsumerState { ], ), ), - if (ref.watch(pWalletBalanceSecondary(walletId)).spendable.raw > - BigInt.zero) - DropdownMenuItem( - value: FiroType.lelantus, - child: Row( - children: [ - Text( - "Lelantus balance", - style: STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, - ), - Text( - ref.watch(pAmountFormatter(coin)).format( - ref - .watch(pWalletBalanceSecondary(walletId)) - .spendable, - ), - style: STextStyles.itemSubtitle(context), - ), - ], - ), - ), DropdownMenuItem( - value: FiroType.public, + value: BalanceType.public, child: Row( children: [ Text( "Public balance", style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref.watch(pWalletBalance(walletId)).spendable, ), style: STextStyles.itemSubtitle(context), @@ -1135,8 +1125,8 @@ class _DesktopSendState extends ConsumerState { ), ], onChanged: (value) { - if (value is FiroType) { - if (value != FiroType.public) { + if (value is BalanceType) { + if (value != BalanceType.public) { ref.read(desktopUseUTXOs.state).state = {}; } setState(() { @@ -1157,45 +1147,37 @@ class _DesktopSendState extends ConsumerState { offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ), - if (coin is Firo) - const SizedBox( - height: 20, - ), + if (showPrivateBalance) const SizedBox(height: 20), if (isPaynymSend) Text( "Send to PayNym address", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (isPaynymSend) - const SizedBox( - height: 10, - ), + if (isPaynymSend) const SizedBox(height: 10), if (isPaynymSend) TextField( key: const Key("sendViewPaynymAddressFieldKey"), controller: sendToController, enabled: false, readOnly: true, - style: STextStyles.desktopTextFieldLabel(context).copyWith( - fontSize: 16, - ), + style: STextStyles.desktopTextFieldLabel( + context, + ).copyWith(fontSize: 16), decoration: const InputDecoration( contentPadding: EdgeInsets.symmetric( vertical: 18, @@ -1203,19 +1185,17 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (isPaynymSend) - const SizedBox( - height: 20, - ), + if (isPaynymSend) const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Amount", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), @@ -1229,9 +1209,7 @@ class _DesktopSendState extends ConsumerState { ), ], ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -1241,12 +1219,13 @@ class _DesktopSendState extends ConsumerState { key: const Key("amountInputFieldCryptoTextFieldKey"), controller: cryptoAmountController, focusNode: _cryptoFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -1270,9 +1249,10 @@ class _DesktopSendState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -1281,19 +1261,17 @@ 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, ), ), ), ), ), ), - if (Prefs.instance.externalCalls) - const SizedBox( - height: 10, - ), + if (Prefs.instance.externalCalls) const SizedBox(height: 10), if (Prefs.instance.externalCalls) TextField( autocorrect: Util.isDesktop ? false : true, @@ -1304,18 +1282,16 @@ class _DesktopSendState extends ConsumerState { key: const Key("amountInputFieldFiatTextFieldKey"), controller: baseAmountController, focusNode: _baseFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ - AmountInputFormatter( - decimals: 2, - locale: locale, - ), + AmountInputFormatter(decimals: 2, locale: locale), // // regex to validate a fiat amount with 2 decimal places // TextInputFormatter.withFunction((oldValue, newValue) => // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') @@ -1332,9 +1308,10 @@ class _DesktopSendState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -1342,23 +1319,22 @@ class _DesktopSendState extends ConsumerState { padding: const EdgeInsets.all(12), child: Text( ref.watch( - prefsChangeNotifierProvider - .select((value) => value.currency), + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), ), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), ), ), ), - if (showCoinControl) - const SizedBox( - height: 10, - ), + if (showCoinControl) const SizedBox(height: 10), if (showCoinControl) RoundedContainer( color: Colors.transparent, @@ -1372,31 +1348,28 @@ class _DesktopSendState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall(context), ), CustomTextButton( - text: ref.watch(desktopUseUTXOs.state).state.isEmpty - ? "Select coins" - : "Selected coins (${ref.watch(desktopUseUTXOs.state).state.length})", + text: + ref.watch(desktopUseUTXOs.state).state.isEmpty + ? "Select coins" + : "Selected coins (${ref.watch(desktopUseUTXOs.state).state.length})", onTap: _showDesktopCoinControl, ), ], ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), if (!isPaynymSend) Text( "Send to", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - if (!isPaynymSend) - const SizedBox( - height: 10, - ), + if (!isPaynymSend) const SizedBox(height: 10), if (!isPaynymSend) ClipRRect( borderRadius: BorderRadius.circular( @@ -1420,19 +1393,24 @@ class _DesktopSendState extends ConsumerState { paste: true, selectAll: false, ), - onChanged: (newValue) { + onChanged: (newValue) async { final trimmed = newValue; if ((trimmed.length - (_address?.length ?? 0)).abs() > 1) { - final parsed = AddressUtils.parsePaymentUri(trimmed, logging: Logging.instance); + final parsed = AddressUtils.parsePaymentUri( + trimmed, + logging: Logging.instance, + ); if (parsed != null) { _applyUri(parsed); } else { - _address = newValue; - sendToController.text = newValue; + await _checkSparkNameAndOrSetAddress(newValue); } } else { - _address = newValue; + await _checkSparkNameAndOrSetAddress( + newValue, + setController: false, + ); } _setValidAddressProviders(_address); @@ -1443,9 +1421,10 @@ class _DesktopSendState extends ConsumerState { }, focusNode: _addressFocusNode, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -1461,76 +1440,80 @@ class _DesktopSendState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _addressToggleFlag ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - sendToController.text = ""; - _address = ""; - _setValidAddressProviders(_address); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + sendToController.text = ""; + _address = ""; + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: pasteAddress, - child: sendToController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + key: const Key( + "sendViewPasteAddressFieldButtonKey", ), + onTap: pasteAddress, + child: + sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key("sendViewAddressBookButtonKey"), onTap: () async { - final entry = - await showDialog( + final entry = await showDialog< + ContactAddressEntry? + >( context: context, - builder: (context) => DesktopDialog( - maxWidth: 696, - maxHeight: 600, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Address book", - style: STextStyles.desktopH3( - context, + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3( + context, + ), + ), ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, ), ), - const DesktopDialogCloseButton(), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: coin, - ), - ), - ], - ), - ), + ), ); if (entry != null) { @@ -1552,9 +1535,7 @@ class _DesktopSendState extends ConsumerState { TextFieldIconButton( semanticsLabel: "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key( - "sendViewScanQrButtonKey", - ), + key: const Key("sendViewScanQrButtonKey"), onTap: scanWebcam, child: const QrCodeIcon(), ), @@ -1573,30 +1554,13 @@ class _DesktopSendState extends ConsumerState { if (_address == null || _address!.isEmpty) { error = null; } else if (coin is Firo) { - if (firoType == FiroType.lelantus) { - if (_data != null && _data.contactLabel == _address) { - error = SparkInterface.validateSparkAddress( - address: _data.address, - isTestNet: coin.network.isTestNet, - ) - ? "Lelantus to Spark not supported" - : null; - } else if (ref.watch(pValidSparkSendToAddress)) { - error = "Lelantus to Spark not supported"; - } else { - error = ref.watch(pValidSendToAddress) - ? null - : "Invalid address"; - } + if (_data != null && _data.contactLabel == _address) { + error = null; + } else if (!ref.watch(pValidSendToAddress) && + !ref.watch(pValidSparkSendToAddress)) { + error = "Invalid address"; } else { - if (_data != null && _data.contactLabel == _address) { - error = null; - } else if (!ref.watch(pValidSendToAddress) && - !ref.watch(pValidSparkSendToAddress)) { - error = "Invalid address"; - } else { - error = null; - } + error = null; } } else { if (_data != null && _data.contactLabel == _address) { @@ -1614,17 +1578,15 @@ class _DesktopSendState extends ConsumerState { return Align( alignment: Alignment.topLeft, child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - top: 4.0, - ), + padding: const EdgeInsets.only(left: 12.0, top: 4.0), child: Text( error, textAlign: TextAlign.left, style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .textError, + color: + Theme.of( + context, + ).extension()!.textError, ), ), ), @@ -1632,15 +1594,9 @@ class _DesktopSendState extends ConsumerState { } }, ), - if (isStellar || - (ref.watch(pValidSparkSendToAddress) && - firoType != FiroType.lelantus)) - const SizedBox( - height: 10, - ), - if (isStellar || - (ref.watch(pValidSparkSendToAddress) && - firoType != FiroType.lelantus)) + if (isStellar || ref.watch(pValidSparkSendToAddress)) + const SizedBox(height: 10), + if (isStellar || ref.watch(pValidSparkSendToAddress)) ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1659,9 +1615,10 @@ class _DesktopSendState extends ConsumerState { setState(() {}); }, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -1678,9 +1635,10 @@ class _DesktopSendState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: memoController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + memoController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -1688,9 +1646,10 @@ class _DesktopSendState extends ConsumerState { TextFieldIconButton( key: const Key("sendViewPasteMemoButtonKey"), onTap: pasteMemo, - child: memoController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + child: + memoController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), ), ], ), @@ -1699,245 +1658,70 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (!isPaynymSend) - const SizedBox( - height: 20, - ), - if (coin is! NanoCurrency && coin is! Epiccash && coin is! Tezos) - ConditionalParent( - condition: ref.watch(pWallets).getWallet(walletId) - is ElectrumXInterface && - !(((coin is Firo) && - (ref.watch(publicPrivateBalanceStateProvider.state).state == - FiroType.lelantus || - ref - .watch(publicPrivateBalanceStateProvider.state) - .state == - FiroType.spark))), - builder: (child) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - child, - CustomTextButton( - text: "Edit", - onTap: () async { - feeSelectionResult = await showDialog< - ( - FeeRateType, - String?, - String?, - )?>( - context: context, - builder: (_) => DesktopFeeDialog( - walletId: walletId, - ), - ); - - if (feeSelectionResult != null) { - if (isCustomFee && - feeSelectionResult!.$1 != FeeRateType.custom) { - isCustomFee = false; - } else if (!isCustomFee && - feeSelectionResult!.$1 == FeeRateType.custom) { - isCustomFee = true; - } - } - - setState(() {}); - }, - ), - ], - ), - child: Text( - "Transaction fee" - "${isCustomFee ? "" : " (${coin is Ethereum ? "max" : "estimated"})"}", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - textAlign: TextAlign.left, - ), - ), + if (!isPaynymSend) const SizedBox(height: 20), if (coin is! NanoCurrency && coin is! Epiccash && coin is! Tezos) - const SizedBox( - height: 10, + DesktopSendFeeForm( + walletId: walletId, + isToken: false, + onCustomFeeSliderChanged: (value) => customFeeRate = value, + onCustomFeeOptionChanged: (value) { + isCustomFee = value; + customFeeRate = 1; + ethFee = null; + }, + onCustomEip1559FeeOptionChanged: (value) => ethFee = value, ), - if (coin is! NanoCurrency && coin is! Epiccash && coin is! Tezos) - if (!isCustomFee) - Padding( - padding: const EdgeInsets.all(10), - child: (feeSelectionResult?.$2 == null) - ? FutureBuilder( - future: ref.watch( - pWallets.select( - (value) => value.getWallet(walletId).fees, - ), - ), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return DesktopFeeItem( - feeObject: snapshot.data, - feeRateType: FeeRateType.average, - walletId: walletId, - isButton: false, - feeFor: ({ - required Amount amount, - required FeeRateType feeRateType, - required int feeRate, - required CryptoCurrency coin, - }) async { - if (ref - .read(feeSheetSessionCacheProvider) - .average[amount] == - null) { - final wallet = - ref.read(pWallets).getWallet(walletId); - - if (coin is Monero || coin is Wownero) { - final fee = await wallet.estimateFeeFor( - amount, - lib_monero.TransactionPriority.medium.value, - ); - ref - .read(feeSheetSessionCacheProvider) - .average[amount] = fee; - } else if ((coin is Firo) && - ref - .read( - publicPrivateBalanceStateProvider - .state, - ) - .state != - FiroType.public) { - final firoWallet = wallet as FiroWallet; - - if (ref - .read( - publicPrivateBalanceStateProvider - .state, - ) - .state == - FiroType.lelantus) { - ref - .read(feeSheetSessionCacheProvider) - .average[amount] = - await firoWallet - .estimateFeeForLelantus(amount); - } else if (ref - .read( - publicPrivateBalanceStateProvider - .state, - ) - .state == - FiroType.spark) { - ref - .read(feeSheetSessionCacheProvider) - .average[amount] = - await firoWallet - .estimateFeeForSpark(amount); - } - } else { - ref - .read(feeSheetSessionCacheProvider) - .average[amount] = - await wallet.estimateFeeFor( - amount, - feeRate, - ); - } - } - return ref - .read(feeSheetSessionCacheProvider) - .average[amount]!; - }, - isSelected: true, - ); - } else { - return Row( - children: [ - AnimatedText( - stringsToLoopThrough: stringsToLoopThrough, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - ), - ], - ); - } - }, - ) - : (coin is Firo) && - ref - .watch( - publicPrivateBalanceStateProvider.state, - ) - .state == - FiroType.lelantus - ? Text( - "~${ref.watch(pAmountFormatter(coin)).format( - Amount( - rawValue: BigInt.parse("3794"), - fractionDigits: coin.fractionDigits, - ), - indicatePrecisionLoss: false, - )}", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - textAlign: TextAlign.left, - ) - : Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - feeSelectionResult?.$2 ?? "", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - textAlign: TextAlign.left, - ), - Text( - feeSelectionResult?.$3 ?? "", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - ), - ], - ), + if (coin is Ethereum) const SizedBox(height: 20), + if (coin is Ethereum) + Text( + "Nonce", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), - if (isCustomFee) - Padding( - padding: const EdgeInsets.only( - bottom: 12, - top: 16, + textAlign: TextAlign.left, + ), + if (coin is Ethereum) const SizedBox(height: 10), + if (coin is Ethereum) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - child: FeeSlider( - coin: coin, - onSatVByteChanged: (rate) { - customFeeRate = rate; - }, + child: TextField( + minLines: 1, + maxLines: 1, + key: const Key("sendViewNonceFieldKey"), + controller: nonceController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + keyboardType: const TextInputType.numberWithOptions(), + focusNode: _nonceFocusNode, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Leave empty to auto select nonce", + _nonceFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + ), ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), PrimaryButton( buttonHeight: ButtonHeight.l, label: "Preview send", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart new file mode 100644 index 000000000..69757ac7c --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart @@ -0,0 +1,306 @@ +import 'package:cs_monero/cs_monero.dart' as lib_monero; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import '../../../../providers/providers.dart'; +import '../../../../providers/wallet/desktop_fee_providers.dart'; +import '../../../../providers/wallet/public_private_balance_state_provider.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/amount/amount.dart'; +import '../../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../../../utilities/eth_commons.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; +import '../../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../../widgets/animated_text.dart'; +import '../../../../widgets/conditional_parent.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../../widgets/desktop/desktop_fee_dialog.dart'; +import '../../../../widgets/eth_fee_form.dart'; +import '../../../../widgets/fee_slider.dart'; + +class DesktopSendFeeForm extends ConsumerStatefulWidget { + const DesktopSendFeeForm({ + super.key, + required this.walletId, + required this.isToken, + required this.onCustomFeeSliderChanged, + required this.onCustomFeeOptionChanged, + this.onCustomEip1559FeeOptionChanged, + }); + + final String walletId; + final bool isToken; + final void Function(int) onCustomFeeSliderChanged; + final void Function(bool) onCustomFeeOptionChanged; + final void Function(EthEIP1559Fee)? onCustomEip1559FeeOptionChanged; + + @override + ConsumerState createState() => _DesktopSendFeeFormState(); +} + +class _DesktopSendFeeFormState extends ConsumerState { + final stringsToLoopThrough = [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ]; + + late final CryptoCurrency cryptoCurrency; + + bool get isEth => cryptoCurrency is Ethereum; + + bool _isCustomFeeValue = false; + bool get _isCustomFee => _isCustomFeeValue; + set _isCustomFee(bool newValue) { + if (_isCustomFeeValue != newValue) { + _isCustomFeeValue = newValue; + widget.onCustomFeeOptionChanged.call(_isCustomFeeValue); + } + } + + (FeeRateType, String?, String?)? feeSelectionResult; + + @override + void initState() { + super.initState(); + cryptoCurrency = ref.read(pWalletCoin(widget.walletId)); + } + + @override + Widget build(BuildContext context) { + final canEditFees = + isEth || + (cryptoCurrency is ElectrumXCurrencyInterface && + !(((cryptoCurrency is Firo) && + (ref.watch(publicPrivateBalanceStateProvider.state).state == + BalanceType.private)))); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: canEditFees, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + CustomTextButton( + text: "Edit", + onTap: () async { + feeSelectionResult = + await showDialog<(FeeRateType, String?, String?)?>( + context: context, + builder: + (_) => DesktopFeeDialog( + walletId: widget.walletId, + isToken: widget.isToken, + ), + ); + + if (feeSelectionResult != null) { + if (_isCustomFee && + feeSelectionResult!.$1 != FeeRateType.custom) { + _isCustomFee = false; + } else if (!_isCustomFee && + feeSelectionResult!.$1 == FeeRateType.custom) { + _isCustomFee = true; + } + } + + setState(() {}); + }, + ), + ], + ), + child: Text( + "Transaction fee" + "${_isCustomFee ? "" : " (${isEth ? "max" : "estimated"})"}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + ), + const SizedBox(height: 10), + if (!_isCustomFee) + Padding( + padding: const EdgeInsets.all(10), + child: + (feeSelectionResult?.$2 == null) + ? FutureBuilder( + future: ref.watch( + pWallets.select( + (value) => value.getWallet(widget.walletId).fees, + ), + ), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + return DesktopFeeItem( + feeObject: snapshot.data, + feeRateType: FeeRateType.average, + walletId: widget.walletId, + isButton: false, + feeFor: ({ + required Amount amount, + required FeeRateType feeRateType, + required BigInt feeRate, + required CryptoCurrency coin, + }) async { + if (ref + .read( + widget.isToken + ? tokenFeeSessionCacheProvider + : feeSheetSessionCacheProvider, + ) + .average[amount] == + null) { + if (widget.isToken == false) { + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId); + + if (coin is Monero || coin is Wownero) { + final fee = await wallet.estimateFeeFor( + amount, + BigInt.from( + lib_monero + .TransactionPriority + .medium + .value, + ), + ); + ref + .read(feeSheetSessionCacheProvider) + .average[amount] = + fee; + } else if ((coin is Firo) && + ref + .read( + publicPrivateBalanceStateProvider + .state, + ) + .state != + BalanceType.public) { + final firoWallet = wallet as FiroWallet; + + if (ref + .read( + publicPrivateBalanceStateProvider + .state, + ) + .state == + BalanceType.private) { + ref + .read(feeSheetSessionCacheProvider) + .average[amount] = await firoWallet + .estimateFeeForSpark(amount); + } + } else { + ref + .read(feeSheetSessionCacheProvider) + .average[amount] = await wallet + .estimateFeeFor(amount, feeRate); + } + } else { + final tokenWallet = + ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor( + amount, + feeRate, + ); + ref + .read(tokenFeeSessionCacheProvider) + .average[amount] = + fee; + } + } + return ref + .read( + widget.isToken + ? tokenFeeSessionCacheProvider + : feeSheetSessionCacheProvider, + ) + .average[amount]!; + }, + isSelected: true, + ); + } else { + return Row( + children: [ + AnimatedText( + stringsToLoopThrough: stringsToLoopThrough, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + ), + ), + ], + ); + } + }, + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + feeSelectionResult?.$2 ?? "", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + ), + textAlign: TextAlign.left, + ), + Text( + feeSelectionResult?.$3 ?? "", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + ), + ], + ), + ), + if (_isCustomFee && isEth) + EthFeeForm( + minGasLimit: + widget.isToken + ? kEthereumTokenMinGasLimit + : kEthereumMinGasLimit, + stateChanged: + (value) => widget.onCustomEip1559FeeOptionChanged?.call(value), + ), + if (_isCustomFee && !isEth) + Padding( + padding: const EdgeInsets.only(bottom: 12, top: 16), + child: FeeSlider( + coin: cryptoCurrency, + onSatVByteChanged: widget.onCustomFeeSliderChanged, + ), + ), + ], + ); + } +} 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 647b1e1cc..bf57331ea 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 @@ -23,17 +23,16 @@ import '../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart import '../../../../providers/providers.dart'; import '../../../../providers/ui/fee_rate_type_state_provider.dart'; import '../../../../providers/ui/preview_tx_button_state_provider.dart'; +import '../../../../providers/wallet/desktop_fee_providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/address_utils.dart'; import '../../../../utilities/amount/amount.dart'; import '../../../../utilities/amount/amount_formatter.dart'; import '../../../../utilities/amount/amount_input_formatter.dart'; import '../../../../utilities/amount/amount_unit.dart'; -import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/logger.dart'; -import '../../../../utilities/prefs.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; @@ -44,7 +43,9 @@ import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/qr_code_scanner_dialog.dart'; import '../../../../widgets/desktop/secondary_button.dart'; +import '../../../../widgets/eth_fee_form.dart'; import '../../../../widgets/icon_widgets/addressbook_icon.dart'; import '../../../../widgets/icon_widgets/clipboard_icon.dart'; import '../../../../widgets/icon_widgets/x_icon.dart'; @@ -52,9 +53,7 @@ import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; import '../../../desktop_home_view.dart'; import 'address_book_address_chooser/address_book_address_chooser.dart'; -import 'desktop_fee_dropdown.dart'; - -// const _kCryptoAmountRegex = r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$'; +import 'desktop_send_fee_form.dart'; class DesktopTokenSend extends ConsumerStatefulWidget { const DesktopTokenSend({ @@ -62,14 +61,13 @@ class DesktopTokenSend extends ConsumerStatefulWidget { required this.walletId, this.autoFillData, this.clipboard = const ClipboardWrapper(), - this.barcodeScanner = const BarcodeScannerWrapper(), + this.accountLite, }); final String walletId; final SendViewAutoFillData? autoFillData; final ClipboardInterface clipboard; - final BarcodeScannerInterface barcodeScanner; final PaynymAccountLite? accountLite; @override @@ -80,7 +78,6 @@ class _DesktopTokenSendState extends ConsumerState { late final String walletId; late final CryptoCurrency coin; late final ClipboardInterface clipboard; - late final BarcodeScannerInterface scanner; late TextEditingController sendToController; late TextEditingController cryptoAmountController; @@ -105,20 +102,21 @@ class _DesktopTokenSendState extends ConsumerState { bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; + EthEIP1559Fee? ethFee; + Future previewSend() async { final tokenWallet = ref.read(pCurrentTokenWallet)!; final Amount amount = _amountToSend!; - final Amount availableBalance = ref - .read( - pTokenBalance( - ( - walletId: walletId, - contractAddress: tokenWallet.tokenContract.address - ), - ), - ) - .spendable; + final Amount availableBalance = + ref + .read( + pTokenBalance(( + walletId: walletId, + contractAddress: tokenWallet.tokenContract.address, + )), + ) + .spendable; // confirm send all if (amount == availableBalance) { @@ -131,10 +129,7 @@ class _DesktopTokenSendState extends ConsumerState { maxWidth: 450, maxHeight: double.infinity, child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, bottom: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -148,29 +143,20 @@ class _DesktopTokenSendState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: Text( "You are about to send your entire balance. Would you like to continue?", textAlign: TextAlign.left, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - fontSize: 18, - ), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith(fontSize: 18), ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: Row( children: [ Expanded( @@ -182,9 +168,7 @@ class _DesktopTokenSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: ButtonHeight.l, @@ -241,11 +225,7 @@ class _DesktopTokenSendState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); TxData txData; Future txDataFuture; @@ -253,59 +233,52 @@ class _DesktopTokenSendState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( txData: TxData( recipients: [ - ( + TxRecipient( address: _address!, amount: amount, isChange: false, + addressType: + tokenWallet.cryptoCurrency.getAddressType(_address!)!, ), ], - feeRateType: ref.read(feeRateTypeStateProvider), + feeRateType: ref.read(feeRateTypeDesktopStateProvider), nonce: int.tryParse(nonceController.text), + ethEIP1559Fee: ethFee, ), ); - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; if (!wasCancelled && mounted) { - txData = txData.copyWith( - note: _note ?? "", - ); + txData = txData.copyWith(note: _note ?? ""); // pop building dialog - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmTransactionView( - txData: txData, - walletId: walletId, - onSuccess: clearSendForm, - isTokenTx: true, - routeOnSuccessName: DesktopHomeView.routeName, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + txData: txData, + walletId: walletId, + onSuccess: clearSendForm, + isTokenTx: true, + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), ), ); } } catch (e) { if (mounted) { // pop building dialog - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( showDialog( @@ -315,10 +288,7 @@ class _DesktopTokenSendState extends ConsumerState { maxWidth: 450, maxHeight: double.infinity, child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, bottom: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -332,25 +302,18 @@ class _DesktopTokenSendState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: SelectableText( e.toString(), textAlign: TextAlign.left, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - fontSize: 18, - ), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith(fontSize: 18), ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), Row( children: [ Expanded( @@ -365,9 +328,7 @@ class _DesktopTokenSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), ], ), ], @@ -395,7 +356,9 @@ class _DesktopTokenSendState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( + final cryptoAmount = ref + .read(pAmountFormatter(coin)) + .tryParse( cryptoAmountController.text, ethContract: ref.read(pCurrentTokenWallet)!.tokenContract, ); @@ -408,14 +371,15 @@ class _DesktopTokenSendState extends ConsumerState { } _cachedAmountToSend = _amountToSend; - final price = ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice( - ref.read(pCurrentTokenWallet)!.tokenContract.address, - ) - .item1; + final price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice( + ref.read(pCurrentTokenWallet)!.tokenContract.address, + ) + ?.value; - if (price > Decimal.zero) { + if (price != null && price > Decimal.zero) { final String fiatAmountString = Amount.fromDecimal( _amountToSend!.decimal * price, fractionDigits: 2, @@ -465,12 +429,20 @@ class _DesktopTokenSendState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await scanner.scan(); + final qrResult = await showDialog( + context: context, + builder: (context) => const QrCodeScannerDialog(), + ); + + if (qrResult == null) { + Logging.instance.w("Qr scanning cancelled"); + return; + } - Logging.instance.d("qrResult content: ${qrResult.rawContent}"); + Logging.instance.d("qrResult content: $qrResult"); final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult, logging: Logging.instance, ); @@ -495,10 +467,9 @@ class _DesktopTokenSendState extends ConsumerState { fractionDigits: ref.read(pCurrentTokenWallet)!.tokenContract.decimals, ); - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); _amountToSend = amount; } @@ -510,7 +481,7 @@ class _DesktopTokenSendState extends ConsumerState { // now check for non standard encoded basic address } else { - _address = qrResult.rawContent.split("\n").first.trim(); + _address = qrResult.split("\n").first.trim(); sendToController.text = _address ?? ""; _updatePreviewButtonState(_address, _amountToSend); @@ -554,33 +525,39 @@ class _DesktopTokenSendState extends ConsumerState { if (baseAmountString.isNotEmpty && baseAmountString != "." && baseAmountString != ",") { - final baseAmount = baseAmountString.contains(",") - ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) - .toAmount(fractionDigits: 2) - : Decimal.parse(baseAmountString).toAmount(fractionDigits: 2); - - final Decimal _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice( - ref.read(pCurrentTokenWallet)!.tokenContract.address, - ) - .item1; - - if (_price == Decimal.zero) { + final baseAmount = + baseAmountString.contains(",") + ? Decimal.parse( + baseAmountString.replaceFirst(",", "."), + ).toAmount(fractionDigits: 2) + : Decimal.parse(baseAmountString).toAmount(fractionDigits: 2); + + final Decimal? _price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice( + ref.read(pCurrentTokenWallet)!.tokenContract.address, + ) + ?.value; + + if (_price == null || _price == Decimal.zero) { _amountToSend = Decimal.zero.toAmount(fractionDigits: tokenDecimals); } else { - _amountToSend = baseAmount <= Amount.zero - ? Decimal.zero.toAmount(fractionDigits: tokenDecimals) - : (baseAmount.decimal / _price) - .toDecimal(scaleOnInfinitePrecision: tokenDecimals) - .toAmount(fractionDigits: tokenDecimals); + _amountToSend = + baseAmount <= Amount.zero + ? Decimal.zero.toAmount(fractionDigits: tokenDecimals) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: tokenDecimals) + .toAmount(fractionDigits: tokenDecimals); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; } _cachedAmountToSend = _amountToSend; - final amountString = ref.read(pAmountFormatter(coin)).format( + final amountString = ref + .read(pAmountFormatter(coin)) + .format( _amountToSend!, withUnitName: false, ethContract: ref.read(pCurrentTokenWallet)!.tokenContract, @@ -602,19 +579,15 @@ class _DesktopTokenSendState extends ConsumerState { Future sendAllTapped() async { cryptoAmountController.text = ref .read( - pTokenBalance( - ( - walletId: walletId, - contractAddress: - ref.read(pCurrentTokenWallet)!.tokenContract.address - ), - ), + pTokenBalance(( + walletId: walletId, + contractAddress: + ref.read(pCurrentTokenWallet)!.tokenContract.address, + )), ) .spendable .decimal - .toStringAsFixed( - ref.read(pCurrentTokenWallet)!.tokenContract.decimals, - ); + .toStringAsFixed(ref.read(pCurrentTokenWallet)!.tokenContract.decimals); } @override @@ -629,7 +602,6 @@ class _DesktopTokenSendState extends ConsumerState { walletId = widget.walletId; coin = ref.read(pWallets).getWallet(walletId).info.coin; clipboard = widget.clipboard; - scanner = widget.barcodeScanner; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); @@ -702,16 +674,15 @@ class _DesktopTokenSendState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), if (coin is Firo) Text( "Send from", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), @@ -721,9 +692,10 @@ class _DesktopTokenSendState extends ConsumerState { Text( "Amount", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), @@ -733,9 +705,7 @@ class _DesktopTokenSendState extends ConsumerState { ), ], ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -745,20 +715,22 @@ class _DesktopTokenSendState extends ConsumerState { key: const Key("amountInputFieldCryptoTextFieldKey"), controller: cryptoAmountController, focusNode: _cryptoFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( decimals: tokenContract.decimals, unit: ref.watch(pAmountUnit(coin)), locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), ), ), // regex to validate a crypto amount with 8 decimal places @@ -780,9 +752,10 @@ class _DesktopTokenSendState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -791,20 +764,23 @@ class _DesktopTokenSendState extends ConsumerState { child: Text( ref.watch(pAmountUnit(coin)).unitForContract(tokenContract), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), ), ), ), - if (Prefs.instance.externalCalls) - const SizedBox( - height: 10, - ), - if (Prefs.instance.externalCalls) + if (ref.watch( + prefsChangeNotifierProvider.select((s) => s.externalCalls), + )) + const SizedBox(height: 10), + if (ref.watch( + prefsChangeNotifierProvider.select((s) => s.externalCalls), + )) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -814,19 +790,21 @@ class _DesktopTokenSendState extends ConsumerState { key: const Key("amountInputFieldFiatTextFieldKey"), controller: baseAmountController, focusNode: _baseFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( decimals: 2, locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), ), ), // // regex to validate a fiat amount with 2 decimal places @@ -845,9 +823,10 @@ class _DesktopTokenSendState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -855,34 +834,33 @@ class _DesktopTokenSendState extends ConsumerState { padding: const EdgeInsets.all(12), child: Text( ref.watch( - prefsChangeNotifierProvider - .select((value) => value.currency), + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), ), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Text( "Send to", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -915,9 +893,10 @@ class _DesktopTokenSendState extends ConsumerState { }, focusNode: _addressFocusNode, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -933,78 +912,83 @@ class _DesktopTokenSendState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _addressToggleFlag ? TextFieldIconButton( - key: const Key( - "sendTokenViewClearAddressFieldButtonKey", - ), - onTap: () { - sendToController.text = ""; - _address = ""; - _updatePreviewButtonState( - _address, - _amountToSend, - ); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) + key: const Key( + "sendTokenViewClearAddressFieldButtonKey", + ), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, + _amountToSend, + ); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "sendTokenViewPasteAddressFieldButtonKey", - ), - onTap: pasteAddress, - child: sendToController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + key: const Key( + "sendTokenViewPasteAddressFieldButtonKey", ), + onTap: pasteAddress, + child: + sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key("sendTokenViewAddressBookButtonKey"), onTap: () async { - final entry = - await showDialog( + final entry = await showDialog< + ContactAddressEntry? + >( context: context, - builder: (context) => DesktopDialog( - maxWidth: 696, - maxHeight: 600, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Address book", - style: - STextStyles.desktopH3(context), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, ), ), - const DesktopDialogCloseButton(), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: coin, - ), - ), - ], - ), - ), + ), ); if (entry != null) { @@ -1034,9 +1018,7 @@ class _DesktopTokenSendState extends ConsumerState { ), Builder( builder: (_) { - final error = _updateInvalidAddressText( - _address ?? "", - ); + final error = _updateInvalidAddressText(_address ?? ""); if (error == null || error.isEmpty) { return Container(); @@ -1044,10 +1026,7 @@ class _DesktopTokenSendState extends ConsumerState { return Align( alignment: Alignment.topLeft, child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - top: 4.0, - ), + padding: const EdgeInsets.only(left: 12.0, top: 4.0), child: Text( error, textAlign: TextAlign.left, @@ -1061,40 +1040,28 @@ class _DesktopTokenSendState extends ConsumerState { } }, ), - const SizedBox( - height: 20, - ), - Text( - "Transaction fee (max)", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 10, - ), - DesktopFeeDropDown( + const SizedBox(height: 20), + DesktopSendFeeForm( walletId: walletId, isToken: true, + onCustomFeeSliderChanged: (value) => {}, + onCustomFeeOptionChanged: (value) { + ethFee = null; + }, + onCustomEip1559FeeOptionChanged: (value) => ethFee = value, ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Text( "Nonce", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1110,9 +1077,10 @@ class _DesktopTokenSendState extends ConsumerState { keyboardType: const TextInputType.numberWithOptions(), focusNode: _nonceFocusNode, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -1130,16 +1098,15 @@ class _DesktopTokenSendState extends ConsumerState { ), ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), PrimaryButton( buttonHeight: ButtonHeight.l, label: "Preview send", enabled: ref.watch(previewTokenTxButtonStateProvider.state).state, - onPressed: ref.watch(previewTokenTxButtonStateProvider.state).state - ? previewSend - : null, + onPressed: + ref.watch(previewTokenTxButtonStateProvider.state).state + ? previewSend + : null, ), ], ); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 78a36d8d7..e5b05270f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -9,23 +9,28 @@ */ 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/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; import '../../../../app_config.dart'; +import '../../../../models/keys/view_only_wallet_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/monkey/monkey_view.dart'; import '../../../../pages/namecoin_names/namecoin_names_home_view.dart'; import '../../../../pages/paynym/paynym_claim_view.dart'; import '../../../../pages/paynym/paynym_home_view.dart'; +import '../../../../pages/spark_names/spark_names_home_view.dart'; import '../../../../providers/desktop/current_desktop_menu_item.dart'; import '../../../../providers/global/paynym_api_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../providers/wallet/my_paynym_account_state_provider.dart'; import '../../../../themes/stack_colors.dart'; +import '../../../../themes/theme_providers.dart'; import '../../../../utilities/amount/amount.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; @@ -34,26 +39,64 @@ import '../../../../utilities/text_styles.dart'; import '../../../../wallets/crypto_currency/coins/banano.dart'; import '../../../../wallets/crypto_currency/coins/firo.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; +import '../../../../wallets/wallet/wallet.dart' show Wallet; import '../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../widgets/custom_loading_overlay.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/loading_indicator.dart'; +import '../../../../widgets/static_overflow_row/static_overflow_row.dart'; import '../../../cashfusion/desktop_cashfusion_view.dart'; import '../../../churning/desktop_churning_view.dart'; import '../../../coin_control/desktop_coin_control_view.dart'; import '../../../desktop_menu.dart'; -import '../../../lelantus_coins/lelantus_coins_view.dart'; +import '../../../mweb_utxos_view.dart'; import '../../../ordinals/desktop_ordinals_view.dart'; import '../../../spark_coins/spark_coins_view.dart'; import '../desktop_wallet_view.dart'; import 'more_features/more_features_dialog.dart'; +enum WalletFeature { + anonymizeFunds("Privatize funds", "Privatize funds"), + swap("Swap", ""), + buy("Buy", "Buy cryptocurrency"), + paynym("PayNym", "Increased address privacy using BIP47"), + coinControl( + "Coin control", + "Control, freeze, and utilize outputs at your discretion", + ), + sparkCoins("Spark coins", "View wallet spark coins"), + mwebUtxos("MWEB outputs", "View wallet MWEB outputs"), + ordinals("Ordinals", "View and control your ordinals in ${AppConfig.prefix}"), + monkey("MonKey", "Generate Banano MonKey"), + fusion("Fusion", "Decentralized mixing protocol"), + churn("Churn", "Churning"), + namecoinName("Domains", "Namecoin DNS"), + sparkNames("Names", "Spark names"), + + // special cases + clearSparkCache("", ""), + rbf("", ""), + reuseAddress("", ""), + enableMweb("", ""); + + final String label; + final String description; + const WalletFeature(this.label, this.description); +} + class DesktopWalletFeatures extends ConsumerStatefulWidget { const DesktopWalletFeatures({super.key, required this.walletId}); @@ -65,8 +108,6 @@ class DesktopWalletFeatures extends ConsumerStatefulWidget { } class _DesktopWalletFeaturesState extends ConsumerState { - static const double buttonWidth = 120; - Future _onSwapPressed() async { ref.read(currentDesktopMenuItemProvider.state).state = DesktopMenuItemId.exchange; @@ -75,60 +116,38 @@ class _DesktopWalletFeaturesState extends ConsumerState { } Future _onBuyPressed() async { - Navigator.of(context, rootNavigator: true).pop(); ref.read(currentDesktopMenuItemProvider.state).state = DesktopMenuItemId.buy; ref.read(prevDesktopMenuItemProvider.state).state = DesktopMenuItemId.buy; } - Future _onMorePressed() async { + Future _onMorePressed( + List<(WalletFeature, String, FutureOr Function())> options, + ) async { await showDialog( context: context, builder: - (_) => MoreFeaturesDialog( - walletId: widget.walletId, - onPaynymPressed: _onPaynymPressed, - onBuyPressed: _onBuyPressed, - onCoinControlPressed: _onCoinControlPressed, - onLelantusCoinsPressed: _onLelantusCoinsPressed, - onSparkCoinsPressedPressed: _onSparkCoinsPressed, - // onAnonymizeAllPressed: _onAnonymizeAllPressed, - onWhirlpoolPressed: _onWhirlpoolPressed, - onOrdinalsPressed: _onOrdinalsPressed, - onMonkeyPressed: _onMonkeyPressed, - onFusionPressed: _onFusionPressed, - onChurnPressed: _onChurnPressed, - onNamesPressed: _onNamesPressed, - ), + (_) => + MoreFeaturesDialog(walletId: widget.walletId, options: options), ); } - void _onWhirlpoolPressed() { - Navigator.of(context, rootNavigator: true).pop(); - } - void _onCoinControlPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(DesktopCoinControlView.routeName, arguments: widget.walletId); } - void _onLelantusCoinsPressed() { - Navigator.of(context, rootNavigator: true).pop(); - + void _onSparkCoinsPressed() { Navigator.of( context, - ).pushNamed(LelantusCoinsView.routeName, arguments: widget.walletId); + ).pushNamed(SparkCoinsView.routeName, arguments: widget.walletId); } - void _onSparkCoinsPressed() { - Navigator.of(context, rootNavigator: true).pop(); - + void _onMwebUtxosPressed() { Navigator.of( context, - ).pushNamed(SparkCoinsView.routeName, arguments: widget.walletId); + ).pushNamed(MwebUtxosView.routeName, arguments: widget.walletId); } Future _onAnonymizeAllPressed() async { @@ -146,7 +165,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { Text("Attention!", style: STextStyles.desktopH2(context)), const SizedBox(height: 16), Text( - "You're about to anonymize all of your public funds.", + "You're about to privatize all of your public funds.", style: STextStyles.desktopTextSmall(context), ), const SizedBox(height: 32), @@ -189,17 +208,16 @@ class _DesktopWalletFeaturesState extends ConsumerState { builder: (context) => WillPopScope( child: const CustomLoadingOverlay( - message: "Anonymizing balance", + message: "Privatizing balance", eventBus: null, ), onWillPop: () async => shouldPop, ), ), ); - final firoWallet = - ref.read(pWallets).getWallet(widget.walletId) as FiroWallet; - final publicBalance = firoWallet.info.cachedBalance.spendable; + final wallet = ref.read(pWallets).getWallet(widget.walletId); + final publicBalance = wallet.info.cachedBalance.spendable; if (publicBalance <= Amount.zero) { shouldPop = true; if (context.mounted) { @@ -210,7 +228,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.info, - message: "No funds available to anonymize!", + message: "No funds available to privatize!", context: context, ), ); @@ -219,8 +237,11 @@ class _DesktopWalletFeaturesState extends ConsumerState { } try { - // await firoWallet.anonymizeAllLelantus(); - await firoWallet.anonymizeAllSpark(); + if (wallet is MwebInterface && wallet.info.isMwebEnabled) { + await wallet.anonymizeAllMweb(); + } else { + await (wallet as FiroWallet).anonymizeAllSpark(); + } shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: true).pop(); @@ -230,7 +251,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.success, - message: "Anonymize transaction submitted", + message: "Privatize transaction submitted", context: context, ), ); @@ -254,7 +275,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Anonymize all failed", + "Privatize all failed", style: STextStyles.desktopH3(context), ), const Spacer(flex: 1), @@ -290,8 +311,6 @@ class _DesktopWalletFeaturesState extends ConsumerState { } Future _onPaynymPressed() async { - Navigator.of(context, rootNavigator: true).pop(); - unawaited( showDialog( context: context, @@ -332,114 +351,173 @@ class _DesktopWalletFeaturesState extends ConsumerState { } Future _onMonkeyPressed() async { - Navigator.of(context, rootNavigator: true).pop(); - await (Navigator.of( context, ).pushNamed(MonkeyView.routeName, arguments: widget.walletId)); } void _onOrdinalsPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(DesktopOrdinalsView.routeName, arguments: widget.walletId); } void _onFusionPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(DesktopCashFusionView.routeName, arguments: widget.walletId); } void _onChurnPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(DesktopChurningView.routeName, arguments: widget.walletId); } void _onNamesPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(NamecoinNamesHomeView.routeName, arguments: widget.walletId); } + void _onSparkNamesPressed() { + Navigator.of( + context, + ).pushNamed(SparkNamesHomeView.routeName, arguments: widget.walletId); + } + + List<(WalletFeature, String, FutureOr Function())> _getOptions( + Wallet wallet, + bool showExchange, + bool showCoinControl, + bool firoAdvanced, + ) { + final coin = wallet.info.coin; + final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; + + return [ + if (!isViewOnly && + (coin is Firo || + (wallet is MwebInterface && wallet.info.isMwebEnabled))) + ( + WalletFeature.anonymizeFunds, + Assets.svg.recycle, + _onAnonymizeAllPressed, + ), + + if (wallet is SparkInterface) + (WalletFeature.sparkNames, Assets.svg.robotHead, _onSparkNamesPressed), + + if (!isViewOnly && + Constants.enableExchange && + AppConfig.hasFeature(AppFeature.swap) && + showExchange) + (WalletFeature.swap, Assets.svg.swap, _onSwapPressed), + + if (showExchange && AppConfig.hasFeature(AppFeature.buy)) + (WalletFeature.buy, Assets.svg.swap, _onBuyPressed), + + if (showCoinControl) + ( + WalletFeature.coinControl, + Assets.svg.coinControl.gamePad, + _onCoinControlPressed, + ), + + if (firoAdvanced && wallet is FiroWallet) + ( + WalletFeature.sparkCoins, + Assets.svg.coinControl.gamePad, + _onSparkCoinsPressed, + ), + + if (kDebugMode && !isViewOnly && wallet is MwebInterface) + ( + WalletFeature.mwebUtxos, + Assets.svg.coinControl.gamePad, + _onMwebUtxosPressed, + ), + + if (!isViewOnly && wallet is PaynymInterface) + (WalletFeature.paynym, Assets.svg.robotHead, _onPaynymPressed), + + if (wallet is OrdinalsInterface) + (WalletFeature.ordinals, Assets.svg.ordinal, _onOrdinalsPressed), + + if (wallet.info.coin is Banano) + (WalletFeature.monkey, Assets.svg.monkey, _onMonkeyPressed), + + if (!isViewOnly && wallet is CashFusionInterface) + (WalletFeature.fusion, Assets.svg.cashFusion, _onFusionPressed), + + if (!isViewOnly && + (wallet is LibMoneroWallet || wallet is LibSalviumWallet)) + (WalletFeature.churn, Assets.svg.churn, _onChurnPressed), + + if (wallet is NamecoinWallet) + (WalletFeature.namecoinName, Assets.svg.robotHead, _onNamesPressed), + ]; + } + @override Widget build(BuildContext context) { final wallet = ref.watch(pWallets).getWallet(widget.walletId); - final coin = wallet.info.coin; - - final prefs = ref.watch(prefsChangeNotifierProvider); - final showExchange = prefs.enableExchange; - final showMore = - wallet is PaynymInterface || - (wallet is CoinControlInterface && - ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.enableCoinControl, - ), - )) || - coin is Firo || - // manager.hasWhirlpoolSupport || - coin is Banano || - wallet is OrdinalsInterface || - wallet is CashFusionInterface; + final options = _getOptions( + wallet, + wallet is! FiroWallet && + ref.watch( + prefsChangeNotifierProvider.select((value) => value.enableExchange), + ), + (wallet is CoinControlInterface && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + )), + ref.watch( + prefsChangeNotifierProvider.select((s) => s.advancedFiroFeatures), + ), + ); final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!isViewOnly && wallet.info.coin is Firo) - SecondaryButton( - label: "Anonymize funds", - width: buttonWidth * 2, - buttonHeight: ButtonHeight.l, - icon: SvgPicture.asset( - Assets.svg.recycle, - height: 20, - width: 20, - color: - Theme.of( - context, - ).extension()!.buttonTextSecondary, - ), - onPressed: () => _onAnonymizeAllPressed(), - ), - if (!isViewOnly && wallet.info.coin is Firo) const SizedBox(width: 16), - if (!isViewOnly && - Constants.enableExchange && - AppConfig.hasFeature(AppFeature.swap) && - showExchange) - SecondaryButton( - label: "Swap", - width: buttonWidth, - buttonHeight: ButtonHeight.l, - icon: SvgPicture.asset( - Assets.svg.arrowRotate, - height: 20, - width: 20, - color: - Theme.of( - context, - ).extension()!.buttonTextSecondary, - ), - onPressed: () => _onSwapPressed(), - ), + final bool canGen; + if (isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { + canGen = false; + } else { + final supportsMweb = + wallet is MwebInterface && + !wallet.info.isViewOnly && + wallet.info.isMwebEnabled; + + canGen = + (wallet is MultiAddressInterface || + wallet is SparkInterface || + supportsMweb); + } + + final showMwebOption = wallet is MwebInterface && !wallet.isViewOnly; + + final extraOptions = [ + if (wallet is SparkInterface && !isViewOnly) + (WalletFeature.clearSparkCache, Assets.svg.key, () => ()), - if (showMore) const SizedBox(width: 16), - if (showMore) - SecondaryButton( + if (wallet is RbfInterface) (WalletFeature.rbf, Assets.svg.key, () => ()), + + if (canGen) (WalletFeature.reuseAddress, Assets.svg.key, () => ()), + + if (showMwebOption) (WalletFeature.enableMweb, Assets.svg.key, () => ()), + ]; + + return StaticOverflowRow( + forcedOverflow: extraOptions.isNotEmpty, + overflowBuilder: (count) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: SecondaryButton( label: "More", - width: buttonWidth, + padding: const EdgeInsets.symmetric(horizontal: 16), buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.bars, @@ -450,9 +528,52 @@ class _DesktopWalletFeaturesState extends ConsumerState { context, ).extension()!.buttonTextSecondary, ), - onPressed: () => _onMorePressed(), + onPressed: + () => _onMorePressed([ + ...options.sublist(options.length - count), + ...extraOptions, + ]), ), - ], + ); + }, + + children: options + .map( + (option) => Padding( + padding: const EdgeInsets.only(left: 16), + child: SecondaryButton( + label: option.$1.label, + padding: const EdgeInsets.symmetric(horizontal: 16), + buttonHeight: ButtonHeight.l, + icon: + option.$1 == WalletFeature.buy + ? SvgPicture.file( + File( + ref.watch( + themeProvider.select((value) => value.assets.buy), + ), + ), + height: 20, + width: 20, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, + ) + : SvgPicture.asset( + option.$2, + height: 20, + width: 20, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, + ), + onPressed: () => option.$3(), + ), + ), + ) + .toList(growable: false), ); } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index fdc0f99b7..c6ad2694d 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -84,7 +84,7 @@ class _WDesktopWalletSummaryState extends ConsumerState { ) : null; - final priceTuple = + final price = widget.isToken ? ref.watch( priceAnd24hChangeNotifierProvider.select( @@ -104,17 +104,12 @@ class _WDesktopWalletSummaryState extends ConsumerState { final Amount balanceToShow; if (isFiro) { switch (ref.watch(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: final balance = ref.watch(pWalletBalanceTertiary(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; - case FiroType.lelantus: - final balance = ref.watch(pWalletBalanceSecondary(walletId)); - balanceToShow = _showAvailable ? balance.spendable : balance.total; - break; - - case FiroType.public: + case BalanceType.public: final balance = ref.watch(pWalletBalance(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; @@ -150,9 +145,9 @@ class _WDesktopWalletSummaryState extends ConsumerState { style: STextStyles.desktopH3(context), ), ), - if (externalCalls) + if (externalCalls && price != null) SelectableText( - "${Amount.fromDecimal(priceTuple.item1 * balanceToShow.decimal, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", + "${Amount.fromDecimal(price.value * balanceToShow.decimal, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart index 73f384f82..650f67f68 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart @@ -66,12 +66,14 @@ class _WFiroDesktopWalletSummaryState if (ref.watch( prefsChangeNotifierProvider.select((value) => value.externalCalls), )) { - final priceTuple = ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ); - price = priceTuple.item1; + price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + ?.value; } final _showAvailable = @@ -83,8 +85,6 @@ class _WFiroDesktopWalletSummaryState _showAvailable ? balance0.spendable : balance0.total; final balance1 = ref.watch(pWalletBalanceSecondary(walletId)); - final balanceToShowLelantus = - _showAvailable ? balance1.spendable : balance1.total; final balance2 = ref.watch(pWalletBalance(walletId)); final balanceToShowPublic = @@ -104,7 +104,7 @@ class _WFiroDesktopWalletSummaryState children: [ TableRow( children: [ - const _Prefix(type: FiroType.spark), + const _Prefix(type: BalanceType.private), _Balance(coin: coin, amount: balanceToShowSpark), if (price != null) _Price( @@ -114,22 +114,10 @@ class _WFiroDesktopWalletSummaryState ), ], ), - if (balanceToShowLelantus.raw > BigInt.zero) - TableRow( - children: [ - const _Prefix(type: FiroType.lelantus), - _Balance(coin: coin, amount: balanceToShowLelantus), - if (price != null) - _Price( - coin: coin, - amount: balanceToShowLelantus, - price: price, - ), - ], - ), + TableRow( children: [ - const _Prefix(type: FiroType.public), + const _Prefix(type: BalanceType.public), _Balance(coin: coin, amount: balanceToShowPublic), if (price != null) _Price( @@ -159,15 +147,13 @@ class _WFiroDesktopWalletSummaryState class _Prefix extends StatelessWidget { const _Prefix({super.key, required this.type}); - final FiroType type; + final BalanceType type; String get asset { switch (type) { - case FiroType.public: - return Assets.png.glasses; - case FiroType.lelantus: + case BalanceType.public: return Assets.png.glasses; - case FiroType.spark: + case BalanceType.private: return Assets.svg.spark; } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 1ee0447b3..20f2524a6 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -8,110 +8,46 @@ * */ +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:wakelock_plus/wakelock_plus.dart'; -import '../../../../../app_config.dart'; import '../../../../../db/sqlite/firo_cache.dart'; -import '../../../../../models/keys/view_only_wallet_data.dart'; -import '../../../../../providers/db/main_db_provider.dart'; -import '../../../../../providers/global/prefs_provider.dart'; -import '../../../../../providers/global/wallets_provider.dart'; +import '../../../../../providers/providers.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../themes/theme_providers.dart'; import '../../../../../utilities/assets.dart'; -import '../../../../../utilities/constants.dart'; -import '../../../../../utilities/logger.dart'; -import '../../../../../utilities/show_loading.dart'; import '../../../../../utilities/text_styles.dart'; -import '../../../../../utilities/util.dart'; import '../../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../../wallets/isar/models/wallet_info.dart'; import '../../../../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../../../../wallets/wallet/impl/firo_wallet.dart'; -import '../../../../../wallets/wallet/impl/namecoin_wallet.dart'; -import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; +import '../../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../../widgets/desktop/primary_button.dart'; import '../../../../../widgets/desktop/secondary_button.dart'; import '../../../../../widgets/rounded_container.dart'; -import '../../../../../widgets/stack_dialog.dart'; +import '../desktop_wallet_features.dart'; class MoreFeaturesDialog extends ConsumerStatefulWidget { const MoreFeaturesDialog({ super.key, required this.walletId, - required this.onPaynymPressed, - required this.onBuyPressed, - required this.onCoinControlPressed, - required this.onLelantusCoinsPressed, - required this.onSparkCoinsPressedPressed, - // required this.onAnonymizeAllPressed, - required this.onWhirlpoolPressed, - required this.onOrdinalsPressed, - required this.onMonkeyPressed, - required this.onFusionPressed, - required this.onChurnPressed, - required this.onNamesPressed, + required this.options, }); final String walletId; - final VoidCallback? onPaynymPressed; - final VoidCallback? onBuyPressed; - final VoidCallback? onCoinControlPressed; - final VoidCallback? onLelantusCoinsPressed; - final VoidCallback? onSparkCoinsPressedPressed; - // final VoidCallback? onAnonymizeAllPressed; - final VoidCallback? onWhirlpoolPressed; - final VoidCallback? onOrdinalsPressed; - final VoidCallback? onMonkeyPressed; - final VoidCallback? onFusionPressed; - final VoidCallback? onChurnPressed; - final VoidCallback? onNamesPressed; + final List<(WalletFeature, String, FutureOr Function())> options; @override ConsumerState createState() => _MoreFeaturesDialogState(); } class _MoreFeaturesDialogState extends ConsumerState { - bool _isUpdatingLelantusScanning = false; // Mutex. - - Future _switchToggled(bool newValue) async { - if (_isUpdatingLelantusScanning) return; - _isUpdatingLelantusScanning = true; // Lock mutex. - - try { - // Toggle enableLelantusScanning in wallet info. - await ref - .read(pWalletInfo(widget.walletId)) - .updateOtherData( - newEntries: {WalletInfoKeys.enableLelantusScanning: newValue}, - isar: ref.read(mainDBProvider).isar, - ); - - if (newValue) { - await _doRescanMaybe(); - } - } finally { - // ensure _isUpdatingLelantusScanning is set to false no matter what - _isUpdatingLelantusScanning = false; - } - } - bool _switchRbfToggledLock = false; // Mutex. Future _switchRbfToggled(bool newValue) async { if (_switchRbfToggledLock) { @@ -133,120 +69,8 @@ class _MoreFeaturesDialogState extends ConsumerState { } } - Future _doRescanMaybe() async { - final shouldRescan = await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxWidth: 700, - child: Column( - children: [ - const DesktopDialogCloseButton(), - const SizedBox(height: 5), - Text( - "Rescan may be required", - style: STextStyles.desktopH2(context), - textAlign: TextAlign.left, - ), - const SizedBox(height: 16), - const Spacer(), - Text( - "A blockchain rescan may be required to fully recover all lelantus history." - "\nThis may take a while.", - style: STextStyles.desktopTextMedium(context).copyWith( - color: Theme.of(context).extension()!.textDark3, - ), - textAlign: TextAlign.center, - ), - const Spacer(), - Padding( - padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Rescan now", - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: PrimaryButton( - label: "Later", - onPressed: () => Navigator.of(context).pop(false), - ), - ), - ], - ), - ), - ], - ), - ); - }, - ); - - if (mounted && shouldRescan == true) { - try { - if (!Platform.isLinux) await WakelockPlus.enable(); - - Exception? e; - if (mounted) { - await showLoading( - whileFuture: ref - .read(pWallets) - .getWallet(widget.walletId) - .recover(isRescan: true), - context: context, - message: "Rescanning blockchain", - subMessage: - "This may take a while.\nPlease do not exit this screen.", - rootNavigator: Util.isDesktop, - onException: (ex) => e = ex, - ); - - if (e != null) { - throw e!; - } - } - } catch (e, s) { - Logging.instance.e("$e\n$s", error: e, stackTrace: s); - if (mounted) { - // show error - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: - (context) => StackDialog( - title: "Rescan failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.itemSubtitle12(context), - ), - onPressed: () { - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop(); - }, - ), - ), - ); - } - } finally { - if (!Platform.isLinux) await WakelockPlus.disable(); - } - } - } - - late final DSBController _switchController; + late final DSBController _switchControllerAddressReuse; + late final DSBController _switchControllerMwebToggle; bool _switchReuseAddressToggledLock = false; // Mutex. Future _switchReuseAddressToggled() async { @@ -256,7 +80,7 @@ class _MoreFeaturesDialogState extends ConsumerState { _switchReuseAddressToggledLock = true; // Lock mutex. try { - if (_switchController.isOn?.call() != true) { + if (_switchControllerAddressReuse.isOn?.call() != true) { final canContinue = await showDialog( context: context, builder: (context) { @@ -345,16 +169,135 @@ class _MoreFeaturesDialogState extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); - if (_switchController.isOn != null) { - if (_switchController.isOn!.call() != shouldReuse) { - _switchController.activate?.call(); + if (_switchControllerAddressReuse.isOn != null) { + if (_switchControllerAddressReuse.isOn!.call() != shouldReuse) { + _switchControllerAddressReuse.activate?.call(); + } + } + } + + bool _switchMwebToggleToggledLock = false; // Mutex. + Future _switchMwebToggleToggled() async { + if (_switchMwebToggleToggledLock) { + return; + } + _switchMwebToggleToggledLock = true; // Lock mutex. + + try { + if (_switchControllerMwebToggle.isOn?.call() != true) { + final canContinue = await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 576, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Notice", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8, + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Activating MWEB requires synchronizing on-chain MWEB related data. " + "This currently requires about 800 MB of storage.", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 43), + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(false); + }, + label: "Cancel", + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(true); + }, + label: "Continue", + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ); + + if (canContinue == true) { + await _updateMwebToggle(true); + + unawaited( + (ref.read(pWallets).getWallet(widget.walletId) as MwebInterface) + .open(), + ); + } + } else { + await _updateMwebToggle(false); + } + } finally { + // ensure _switchMwebToggleToggledLock is set to false no matter what. + _switchMwebToggleToggledLock = false; + } + } + + Future _updateMwebToggle(bool value) async { + if (value) { + unawaited( + ref + .read(pMwebService) + .initService(ref.read(pWalletCoin(widget.walletId)).network), + ); + } + + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: value}, + isar: ref.read(mainDBProvider).isar, + ); + + if (_switchControllerMwebToggle.isOn != null) { + if (_switchControllerMwebToggle.isOn!.call() != value) { + _switchControllerMwebToggle.activate?.call(); } } } @override void initState() { - _switchController = DSBController(); + _switchControllerAddressReuse = DSBController(); + _switchControllerMwebToggle = DSBController(); super.initState(); } @@ -364,16 +307,6 @@ class _MoreFeaturesDialogState extends ConsumerState { pWallets.select((value) => value.getWallet(widget.walletId)), ); - final coinControlPrefEnabled = ref.watch( - prefsChangeNotifierProvider.select((value) => value.enableCoinControl), - ); - - final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; - final isViewOnlyNoAddressGen = - wallet is ViewOnlyOptionInterface && - wallet.isViewOnly && - wallet.viewOnlyType == ViewOnlyWalletType.addressOnly; - return DesktopDialog( maxHeight: double.infinity, child: Column( @@ -392,213 +325,150 @@ class _MoreFeaturesDialogState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - if (Constants.enableExchange && - AppConfig.hasFeature(AppFeature.buy) && - ref.watch(prefsChangeNotifierProvider).enableExchange) - _MoreFeaturesItem( - label: "Buy", - detail: "Buy cryptocurrency", - isSvgFile: true, - iconAsset: ref.watch( - themeProvider.select((value) => value.assets.buy), - ), - onPressed: () async => widget.onBuyPressed?.call(), - ), - // if (!isViewOnly && wallet.info.coin is Firo) - // _MoreFeaturesItem( - // label: "Anonymize funds", - // detail: "Anonymize funds", - // iconAsset: Assets.svg.recycle, - // onPressed: () async => widget.onAnonymizeAllPressed?.call(), - // ), - // TODO: [prio=med] - // if (manager.hasWhirlpoolSupport) - // _MoreFeaturesItem( - // label: "Whirlpool", - // detail: "Powerful Bitcoin privacy enhancer", - // iconAsset: Assets.svg.whirlPool, - // onPressed: () => widget.onWhirlpoolPressed?.call(), - // ), - if (wallet is CoinControlInterface && coinControlPrefEnabled) - _MoreFeaturesItem( - label: "Coin control", - detail: "Control, freeze, and utilize outputs at your discretion", - iconAsset: Assets.svg.coinControl.gamePad, - onPressed: () async => widget.onCoinControlPressed?.call(), - ), - if (wallet is FiroWallet && - ref.watch( - prefsChangeNotifierProvider.select( - (s) => s.advancedFiroFeatures, - ), - )) - _MoreFeaturesItem( - label: "Lelantus Coins", - detail: "View wallet lelantus coins", - iconAsset: Assets.svg.coinControl.gamePad, - onPressed: () async => widget.onLelantusCoinsPressed?.call(), - ), - if (wallet is FiroWallet && - ref.watch( - prefsChangeNotifierProvider.select( - (s) => s.advancedFiroFeatures, - ), - )) - _MoreFeaturesItem( - label: "Spark Coins", - detail: "View wallet spark coins", - iconAsset: Assets.svg.coinControl.gamePad, - onPressed: () async => widget.onSparkCoinsPressedPressed?.call(), - ), - if (!isViewOnly && wallet is PaynymInterface) - _MoreFeaturesItem( - label: "PayNym", - detail: "Increased address privacy using BIP47", - iconAsset: Assets.svg.robotHead, - onPressed: () async => widget.onPaynymPressed?.call(), - ), - if (wallet is OrdinalsInterface) - _MoreFeaturesItem( - label: "Ordinals", - detail: "View and control your ordinals in ${AppConfig.prefix}", - iconAsset: Assets.svg.ordinal, - onPressed: () async => widget.onOrdinalsPressed?.call(), - ), - if (wallet.info.coin is Banano) - _MoreFeaturesItem( - label: "MonKey", - detail: "Generate Banano MonKey", - iconAsset: Assets.svg.monkey, - onPressed: () async => widget.onMonkeyPressed?.call(), - ), - if (!isViewOnly && wallet is CashFusionInterface) - _MoreFeaturesItem( - label: "Fusion", - detail: "Decentralized mixing protocol", - iconAsset: Assets.svg.cashFusion, - onPressed: () async => widget.onFusionPressed?.call(), - ), - if (!isViewOnly && wallet is LibMoneroWallet) - _MoreFeaturesItem( - label: "Churn", - detail: "Churning", - iconAsset: Assets.svg.churn, - onPressed: () async => widget.onChurnPressed?.call(), - ), - if (wallet is NamecoinWallet) - _MoreFeaturesItem( - label: "Domains", - detail: "Namecoin DNS", - iconAsset: Assets.svg.robotHead, - onPressed: () async => widget.onNamesPressed?.call(), - ), - if (wallet is SparkInterface && !isViewOnly) - _MoreFeaturesClearSparkCacheItem( - cryptoCurrency: wallet.cryptoCurrency, - ), - if (wallet is LelantusInterface && !isViewOnly) - _MoreFeaturesItemBase( - child: Row( - children: [ - const SizedBox(width: 3), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: - ref.watch( - pWalletInfo( - widget.walletId, - ).select((value) => value.otherData), - )[WalletInfoKeys.enableLelantusScanning] - as bool? ?? - false, - onValueChanged: _switchToggled, - ), + + ...widget.options.map((option) { + switch (option.$1) { + case WalletFeature.buy: + // Buy has a special icon + return _MoreFeaturesItem( + label: option.$1.label, + detail: option.$1.description, + isSvgFile: true, + iconAsset: ref.watch( + themeProvider.select((value) => value.assets.buy), ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + onPressed: () async { + Navigator.of(context, rootNavigator: true).pop(); + option.$3(); + }, + ); + + case WalletFeature.clearSparkCache: + return _MoreFeaturesClearSparkCacheItem( + cryptoCurrency: wallet.cryptoCurrency, + ); + + case WalletFeature.rbf: + return _MoreFeaturesItemBase( + child: Row( children: [ - Text( - "Scan for Lelantus transactions", - style: STextStyles.w600_20(context), + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.enableOptInRbf] + as bool? ?? + false, + onValueChanged: _switchRbfToggled, + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Flag outgoing transactions with opt-in RBF", + style: STextStyles.w600_20(context), + ), + ], ), ], ), - ], - ), - ), - if (wallet is RbfInterface) - _MoreFeaturesItemBase( - child: Row( - children: [ - const SizedBox(width: 3), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: - ref.watch( - pWalletInfo( - widget.walletId, - ).select((value) => value.otherData), - )[WalletInfoKeys.enableOptInRbf] - as bool? ?? - false, - onValueChanged: _switchRbfToggled, - ), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + + case WalletFeature.reuseAddress: + return _MoreFeaturesItemBase( + onPressed: _switchReuseAddressToggled, + child: Row( children: [ - Text( - "Flag outgoing transactions with opt-in RBF", - style: STextStyles.w600_20(context), + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.reuseAddress] + as bool? ?? + false, + controller: _switchControllerAddressReuse, + ), + ), ), - ], - ), - ], - ), - ), - // reuseAddress preference. - if (!isViewOnlyNoAddressGen) - _MoreFeaturesItemBase( - onPressed: _switchReuseAddressToggled, - child: Row( - children: [ - const SizedBox(width: 3), - SizedBox( - height: 20, - width: 40, - child: IgnorePointer( - child: DraggableSwitchButton( - isOn: - ref.watch( - pWalletInfo( - widget.walletId, - ).select((value) => value.otherData), - )[WalletInfoKeys.reuseAddress] - as bool? ?? - false, - controller: _switchController, + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Reuse receiving address", + style: STextStyles.w600_20(context), + ), + ], ), - ), + ], ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + + case WalletFeature.enableMweb: + return _MoreFeaturesItemBase( + onPressed: _switchMwebToggleToggled, + child: Row( children: [ - Text( - "Reuse receiving address", - style: STextStyles.w600_20(context), + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.mwebEnabled] + as bool? ?? + false, + controller: _switchControllerMwebToggle, + ), + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enable MWEB", + style: STextStyles.w600_20(context), + ), + ], ), ], ), - ], - ), - ), + ); + + default: + return _MoreFeaturesItem( + label: option.$1.label, + detail: option.$1.description, + iconAsset: option.$2, + onPressed: () async { + Navigator.of(context, rootNavigator: true).pop(); + option.$3(); + }, + ); + } + }), + const SizedBox(height: 28), ], ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart new file mode 100644 index 000000000..6ce1d19e4 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart @@ -0,0 +1,206 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-06-13 + * + */ + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import '../../../../providers/providers.dart'; +import '../../../../providers/wallet/wallet_balance_toggle_state_provider.dart'; +import '../../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/amount/amount.dart'; +import '../../../../utilities/amount/amount_formatter.dart'; +import '../../../../utilities/enums/wallet_balance_toggle_state.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../../wallets/isar/providers/wallet_info_provider.dart'; +import 'desktop_balance_toggle_button.dart'; + +class MwebDesktopWalletSummary extends ConsumerStatefulWidget { + const MwebDesktopWalletSummary({ + super.key, + required this.walletId, + required this.initialSyncStatus, + }); + + final String walletId; + final WalletSyncStatus initialSyncStatus; + + @override + ConsumerState createState() => + _WMwebDesktopWalletSummaryState(); +} + +class _WMwebDesktopWalletSummaryState + extends ConsumerState { + late final String walletId; + + late final CryptoCurrency coin; + late final bool isMweb; + + @override + void initState() { + super.initState(); + walletId = widget.walletId; + coin = ref.read(pWalletCoin(widget.walletId)); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + ?.value; + } + + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available; + + final balance0 = ref.watch(pWalletBalanceSecondary(walletId)); + final balanceToShowSpark = + _showAvailable ? balance0.spendable : balance0.total; + + final balance2 = ref.watch(pWalletBalance(walletId)); + final balanceToShowPublic = + _showAvailable ? balance2.spendable : balance2.total; + + return Consumer( + builder: (context, ref, __) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Table( + columnWidths: { + 0: const IntrinsicColumnWidth(), + 1: const IntrinsicColumnWidth(), + if (price != null) 2: const IntrinsicColumnWidth(), + }, + children: [ + TableRow( + children: [ + const _Prefix(isMweb: true), + _Balance(coin: coin, amount: balanceToShowSpark), + if (price != null) + _Price( + coin: coin, + amount: balanceToShowSpark, + price: price, + ), + ], + ), + + TableRow( + children: [ + const _Prefix(isMweb: false), + _Balance(coin: coin, amount: balanceToShowPublic), + if (price != null) + _Price( + coin: coin, + amount: balanceToShowPublic, + price: price, + ), + ], + ), + ], + ), + + const SizedBox(width: 8), + WalletRefreshButton( + walletId: walletId, + initialSyncStatus: widget.initialSyncStatus, + ), + const SizedBox(width: 8), + const DesktopBalanceToggleButton(), + ], + ); + }, + ); + } +} + +class _Prefix extends StatelessWidget { + const _Prefix({super.key, required this.isMweb}); + + final bool isMweb; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SelectableText( + isMweb ? "Private" : "Public", + style: STextStyles.w500_24(context), + ), + ], + ), + ); + } +} + +class _Balance extends ConsumerWidget { + const _Balance({super.key, required this.coin, required this.amount}); + + final CryptoCurrency coin; + final Amount amount; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SelectableText( + ref.watch(pAmountFormatter(coin)).format(amount, ethContract: null), + style: STextStyles.desktopH3(context), + textAlign: TextAlign.end, + ); + } +} + +class _Price extends ConsumerWidget { + const _Price({ + super.key, + required this.coin, + required this.amount, + required this.price, + }); + + final CryptoCurrency coin; + final Amount amount; + final Decimal price; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: SelectableText( + "${Amount.fromDecimal(price * amount.decimal, fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} " + "${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + + textAlign: TextAlign.end, + ), + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index 7fbcef510..7a7bc88ca 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -30,11 +30,7 @@ import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; class NetworkInfoButton extends ConsumerStatefulWidget { - const NetworkInfoButton({ - super.key, - required this.walletId, - this.eventBus, - }); + const NetworkInfoButton({super.key, required this.walletId, this.eventBus}); final String walletId; final EventBus? eventBus; @@ -74,27 +70,25 @@ class _NetworkInfoButtonState extends ConsumerState { } } - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); + _syncStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }); - _nodeStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentNodeStatus = event.newStatus; - }); - } - }, - ); + _nodeStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }); super.initState(); } @@ -151,9 +145,9 @@ class _NetworkInfoButtonState extends ConsumerState { return Text( label, - style: STextStyles.desktopMenuItemSelected(context).copyWith( - color: _getColor(status, context), - ), + style: STextStyles.desktopMenuItemSelected( + context, + ).copyWith(color: _getColor(status, context)), ); } @@ -172,9 +166,7 @@ class _NetworkInfoButtonState extends ConsumerState { Widget build(BuildContext context) { return RawMaterialButton( hoverColor: _getColor(_currentSyncStatus, context).withOpacity(0.1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(1000), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(1000)), onPressed: () { if (Util.isDesktop) { // showDialog( @@ -220,88 +212,80 @@ class _NetworkInfoButtonState extends ConsumerState { showDialog( context: context, - builder: (context) => Navigator( - initialRoute: WalletNetworkSettingsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Network", - style: STextStyles.desktopH3(context), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + builder: + (context) => Navigator( + initialRoute: WalletNetworkSettingsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Network", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], ), - ], - ), - ), - Flexible( - child: Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - right: 32, - bottom: 32, ), - child: SingleChildScrollView( - child: WalletNetworkSettingsView( - walletId: walletId, - initialSyncStatus: _currentSyncStatus, - initialNodeStatus: _currentNodeStatus, + Flexible( + child: Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 32, + ), + child: SingleChildScrollView( + child: WalletNetworkSettingsView( + walletId: walletId, + initialSyncStatus: _currentSyncStatus, + initialNodeStatus: _currentNodeStatus, + ), + ), ), ), - ), + ], ), - ], + ), + const RouteSettings( + name: WalletNetworkSettingsView.routeName, + ), ), - ), - const RouteSettings( - name: WalletNetworkSettingsView.routeName, - ), - ), - ]; - }, - ), + ]; + }, + ), ); } else { Navigator.of(context).pushNamed( WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), + arguments: Tuple3(walletId, _currentSyncStatus, _currentNodeStatus), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32), child: Row( children: [ _buildNetworkIcon(_currentSyncStatus, context), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), _buildText(_currentSyncStatus, context), ], ), 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 938e9568a..4f1db4504 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 @@ -24,6 +24,7 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; @@ -36,10 +37,7 @@ import '../../../../widgets/stack_text_field.dart'; import 'wallet_keys_desktop_popup.dart'; class UnlockWalletKeysDesktop extends ConsumerStatefulWidget { - const UnlockWalletKeysDesktop({ - super.key, - required this.walletId, - }); + const UnlockWalletKeysDesktop({super.key, required this.walletId}); final String walletId; @@ -63,16 +61,12 @@ class _UnlockWalletKeysDesktopState unawaited( showDialog( context: context, - builder: (context) => const Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - LoadingIndicator( - width: 200, - height: 200, + builder: + (context) => const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [LoadingIndicator(width: 200, height: 200)], ), - ], - ), ), ); @@ -88,17 +82,38 @@ class _UnlockWalletKeysDesktopState } final wallet = ref.read(pWallets).getWallet(widget.walletId); - ({String keys, String config})? frostData; + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostWalletData; List? words; // TODO: [prio=low] handle wallets that don't have a mnemonic // All wallets currently are mnemonic based if (wallet is! MnemonicInterface) { if (wallet is BitcoinFrostWallet) { - frostData = ( - keys: (await wallet.getSerializedKeys())!, - config: (await wallet.getMultisigConfig())!, - ); + final futures = [ + wallet.getSerializedKeys(), + wallet.getMultisigConfig(), + wallet.getSerializedKeysPrevGen(), + wallet.getMultisigConfigPrevGen(), + ]; + + final results = await Future.wait(futures); + if (results.length == 4) { + frostWalletData = ( + myName: wallet.frostInfo.myName, + config: results[1]!, + keys: results[0]!, + prevGen: + results[2] == null || results[3] == null + ? null + : (config: results[3]!, keys: results[2]!), + ); + } } else { throw Exception("FIXME ~= see todo in code"); } @@ -118,6 +133,8 @@ class _UnlockWalletKeysDesktopState keyData = await wallet.getXPrivs(); } else if (wallet is LibMoneroWallet) { keyData = await wallet.getKeys(); + } else if (wallet is LibSalviumWallet) { + keyData = await wallet.getKeys(); } if (mounted) { @@ -126,7 +143,7 @@ class _UnlockWalletKeysDesktopState arguments: ( mnemonic: words ?? [], walletId: widget.walletId, - frostData: frostData, + frostData: frostWalletData, keyData: keyData, ), ); @@ -174,44 +191,25 @@ class _UnlockWalletKeysDesktopState mainAxisAlignment: MainAxisAlignment.end, children: [ DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + onPressedOverride: + Navigator.of(context, rootNavigator: true).pop, ), ], ), - const SizedBox( - height: 12, - ), - SvgPicture.asset( - Assets.svg.keys, - width: 100, - height: 58, - ), - const SizedBox( - height: 55, - ), - Text( - "Wallet keys", - style: STextStyles.desktopH2(context), - ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 12), + SvgPicture.asset(Assets.svg.keys, width: 100, height: 58), + const SizedBox(height: 55), + Text("Wallet keys", style: STextStyles.desktopH2(context)), + const SizedBox(height: 16), Text( "Enter your password", style: STextStyles.desktopTextMedium(context).copyWith( color: Theme.of(context).extension()!.textDark3, ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -220,9 +218,9 @@ class _UnlockWalletKeysDesktopState key: const Key("enterPasswordUnlockWalletKeysDesktopFieldKey"), focusNode: passwordFocusNode, controller: passwordController, - style: STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), + style: STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2), obscureText: hidePassword, enableSuggestions: false, autocorrect: false, @@ -263,18 +261,17 @@ class _UnlockWalletKeysDesktopState hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 24, height: 19, ), ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), ], ), ), @@ -288,27 +285,18 @@ class _UnlockWalletKeysDesktopState ), ), ), - const SizedBox( - height: 55, - ), + const SizedBox(height: 55), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( children: [ Expanded( child: SecondaryButton( label: "Cancel", - onPressed: Navigator.of( - context, - rootNavigator: true, - ).pop, + onPressed: Navigator.of(context, rootNavigator: true).pop, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Continue", @@ -319,9 +307,7 @@ class _UnlockWalletKeysDesktopState ], ), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), ], ), ); 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 bb8ba8089..2c31098a2 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 @@ -49,7 +49,13 @@ class WalletKeysDesktopPopup extends ConsumerWidget { final List words; final String walletId; - final ({String keys, String config})? frostData; + final ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostData; final ClipboardInterface clipboardInterface; final KeyDataInterface? keyData; @@ -66,9 +72,7 @@ class WalletKeysDesktopPopup extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( - padding: const EdgeInsets.only( - left: 32, - ), + padding: const EdgeInsets.only(left: 32), child: Text( "Wallet keys", style: STextStyles.desktopH3(context), @@ -81,28 +85,93 @@ class WalletKeysDesktopPopup extends ConsumerWidget { ), ], ), - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), frostData != null ? Column( - children: [ + children: [ + Text("Keys", style: STextStyles.desktopTextMedium(context)), + const SizedBox(height: 8), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: RoundedWhiteContainer( + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 9, + ), + child: Row( + children: [ + Flexible( + child: SelectableText( + frostData!.keys, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(width: 10), + IconCopyButton(data: frostData!.keys), + // TODO [prio=low: Add QR code button and dialog. + ], + ), + ), + ), + ), + const SizedBox(height: 24), + Text("Config", style: STextStyles.desktopTextMedium(context)), + const SizedBox(height: 8), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: RoundedWhiteContainer( + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 9, + ), + child: Row( + children: [ + Flexible( + child: SelectableText( + frostData!.config, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(width: 10), + IconCopyButton(data: frostData!.config), + // TODO [prio=low: Add QR code button and dialog. + ], + ), + ), + ), + ), + if (frostData?.prevGen != null) const SizedBox(height: 24), + if (frostData?.prevGen != null) Text( - "Keys", + "Previous generation Keys", style: STextStyles.desktopTextMedium(context), ), - const SizedBox( - height: 8, - ), + if (frostData?.prevGen != null) const SizedBox(height: 8), + if (frostData?.prevGen != null) Center( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: RoundedWhiteContainer( - borderColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 9, @@ -118,37 +187,30 @@ class WalletKeysDesktopPopup extends ConsumerWidget { textAlign: TextAlign.center, ), ), - const SizedBox( - width: 10, - ), - IconCopyButton( - data: frostData!.keys, - ), + const SizedBox(width: 10), + IconCopyButton(data: frostData!.keys), // TODO [prio=low: Add QR code button and dialog. ], ), ), ), ), - const SizedBox( - height: 24, - ), + if (frostData?.prevGen != null) const SizedBox(height: 24), + if (frostData?.prevGen != null) Text( - "Config", + "Previous generation Config", style: STextStyles.desktopTextMedium(context), ), - const SizedBox( - height: 8, - ), + if (frostData?.prevGen != null) const SizedBox(height: 8), + if (frostData?.prevGen != null) Center( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: RoundedWhiteContainer( - borderColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 9, @@ -157,70 +219,58 @@ class WalletKeysDesktopPopup extends ConsumerWidget { children: [ Flexible( child: SelectableText( - frostData!.config, + frostData!.prevGen!.config, style: STextStyles.desktopTextExtraExtraSmall( context, ), textAlign: TextAlign.center, ), ), - const SizedBox( - width: 10, - ), - IconCopyButton( - data: frostData!.config, - ), + const SizedBox(width: 10), + IconCopyButton(data: frostData!.prevGen!.config), // TODO [prio=low: Add QR code button and dialog. ], ), ), ), ), - const SizedBox( - height: 24, - ), - ], - ) + const SizedBox(height: 24), + ], + ) : keyData != null - ? keyData is ViewOnlyWalletData - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ViewOnlyWalletDataWidget( - data: keyData as ViewOnlyWalletData, - ), - ) - : CustomTabView( - titles: [ - if (words.isNotEmpty) "Mnemonic", - if (keyData is XPrivData) "XPriv(s)", - if (keyData is CWKeyData) "Keys", - ], - children: [ - if (words.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 16), - child: _Mnemonic( - words: words, - ), - ), - if (keyData is XPrivData) - WalletXPrivs( - xprivData: keyData as XPrivData, - walletId: walletId, - ), - if (keyData is CWKeyData) - CNWalletKeys( - cwKeyData: keyData as CWKeyData, - walletId: walletId, - ), - ], - ) - : _Mnemonic( - words: words, + ? keyData is ViewOnlyWalletData + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ViewOnlyWalletDataWidget( + data: keyData as ViewOnlyWalletData, ), - const SizedBox( - height: 32, - ), + ) + : CustomTabView( + titles: [ + if (words.isNotEmpty) "Mnemonic", + if (keyData is XPrivData) "XPriv(s)", + if (keyData is CWKeyData) "Keys", + ], + children: [ + if (words.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 16), + child: _Mnemonic(words: words), + ), + if (keyData is XPrivData) + WalletXPrivs( + xprivData: keyData as XPrivData, + walletId: walletId, + ), + if (keyData is CWKeyData) + CNWalletKeys( + cwKeyData: keyData as CWKeyData, + walletId: walletId, + ), + ], + ) + : _Mnemonic(words: words), + const SizedBox(height: 32), ], ), ); @@ -241,18 +291,11 @@ class _Mnemonic extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - Text( - "Recovery phrase", - style: STextStyles.desktopTextMedium(context), - ), - const SizedBox( - height: 8, - ), + Text("Recovery phrase", style: STextStyles.desktopTextMedium(context)), + const SizedBox(height: 8), Center( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + 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" @@ -262,13 +305,9 @@ class _Mnemonic extends StatelessWidget { ), ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: MnemonicTable( words: words, isDesktop: true, @@ -276,13 +315,9 @@ class _Mnemonic extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( children: [ Expanded( @@ -305,9 +340,7 @@ class _Mnemonic extends StatelessWidget { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Show QR code", 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 47b590a61..8136133ca 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 @@ -30,6 +30,7 @@ import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart'; import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../addresses/desktop_wallet_addresses_view.dart'; @@ -296,7 +297,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { final bool canChangeRep = coin is NanoCurrency; final bool isFrost = coin is FrostCurrency; - final bool isMoneroWow = wallet is LibMoneroWallet; + final bool isMoneroWow = wallet is LibMoneroWallet || wallet is LibSalviumWallet; return Stack( children: [ 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 4aff4afb0..aea4a8f02 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; import '../../models/isar/ordinal.dart'; @@ -21,6 +21,7 @@ import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/prefs.dart'; import '../../utilities/show_loading.dart'; +import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_app_bar.dart'; @@ -56,9 +57,10 @@ class _DesktopOrdinalDetailsViewState final response = await client.get( url: Uri.parse(widget.ordinal.content), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code != 200) { @@ -69,16 +71,15 @@ class _DesktopOrdinalDetailsViewState final bytes = response.bodyBytes; - if (Platform.isAndroid) { - await Permission.storage.request(); - } - - final dir = Platform.isAndroid - ? Directory("/storage/emulated/0/Documents") - : await getApplicationDocumentsDirectory(); + final dir = + Platform.isAndroid + ? await StackFileSystem.wtfAndroidDocumentsPath() + : await getApplicationDocumentsDirectory(); - final docPath = dir.path; - final filePath = "$docPath/ordinal_${widget.ordinal.inscriptionNumber}.png"; + final filePath = path.join( + dir.path, + "ordinal_${widget.ordinal.inscriptionNumber}.png", + ); final File imgFile = File(filePath); @@ -105,32 +106,27 @@ class _DesktopOrdinalDetailsViewState child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), AppBarIconButton( size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, width: 18, height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: Navigator.of(context).pop, ), - const SizedBox( - width: 18, - ), - Text( - "Ordinal details", - style: STextStyles.desktopH3(context), - ), + const SizedBox(width: 18), + Text("Ordinal details", style: STextStyles.desktopH3(context)), ], ), ), @@ -138,11 +134,7 @@ class _DesktopOrdinalDetailsViewState isCompactHeight: true, ), body: Padding( - padding: const EdgeInsets.only( - left: 24, - top: 24, - right: 24, - ), + padding: const EdgeInsets.only(left: 24, top: 24, right: 24), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -154,7 +146,8 @@ class _DesktopOrdinalDetailsViewState Constants.size.circularBorderRadius, ), child: Image.network( - widget.ordinal + widget + .ordinal .content, // Use the preview URL as the image source fit: BoxFit.cover, filterQuality: @@ -162,9 +155,7 @@ class _DesktopOrdinalDetailsViewState ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: SingleChildScrollView( child: Padding( @@ -187,9 +178,7 @@ class _DesktopOrdinalDetailsViewState ], ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), // PrimaryButton( // width: 150, // label: "Send", @@ -224,9 +213,10 @@ class _DesktopOrdinalDetailsViewState Assets.svg.arrowDown, width: 13, height: 18, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), buttonHeight: ButtonHeight.l, iconSpacing: 8, @@ -264,9 +254,7 @@ class _DesktopOrdinalDetailsViewState ], ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), RoundedWhiteContainer( padding: const EdgeInsets.all(16), child: Column( @@ -288,25 +276,28 @@ class _DesktopOrdinalDetailsViewState const _Divider(), Consumer( builder: (context, ref, _) { - final coin = ref - .watch(pWallets) - .getWallet(widget.walletId) - .info - .coin; + final coin = + ref + .watch(pWallets) + .getWallet(widget.walletId) + .info + .coin; return _DetailsItemWCopy( title: "Amount", - data: utxo == null - ? "ERROR" - : ref - .watch(pAmountFormatter(coin)) - .format( - Amount( - rawValue: - BigInt.from(utxo!.value), - fractionDigits: - coin.fractionDigits, - ), - ), + data: + utxo == null + ? "ERROR" + : ref + .watch(pAmountFormatter(coin)) + .format( + Amount( + rawValue: BigInt.from( + utxo!.value, + ), + fractionDigits: + coin.fractionDigits, + ), + ), ); }, ), @@ -341,9 +332,7 @@ class _Divider extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, - ), + padding: const EdgeInsets.symmetric(vertical: 16), child: Container( height: 1, color: Theme.of(context).extension()!.backgroundAppBar, @@ -353,11 +342,7 @@ class _Divider extends StatelessWidget { } class _DetailsItemWCopy extends StatelessWidget { - const _DetailsItemWCopy({ - super.key, - required this.title, - required this.data, - }); + const _DetailsItemWCopy({super.key, required this.title, required this.data}); final String title; final String data; @@ -370,22 +355,12 @@ class _DetailsItemWCopy extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - title, - style: STextStyles.itemSubtitle(context), - ), - IconCopyButton( - data: data, - ), + Text(title, style: STextStyles.itemSubtitle(context)), + IconCopyButton(data: data), ], ), - const SizedBox( - height: 4, - ), - SelectableText( - data, - style: STextStyles.itemSubtitle12(context), - ), + const SizedBox(height: 4), + SelectableText(data, style: STextStyles.itemSubtitle12(context)), ], ); } diff --git a/lib/pages_desktop_specific/password/desktop_unlock_app_dialog.dart b/lib/pages_desktop_specific/password/desktop_unlock_app_dialog.dart new file mode 100644 index 000000000..46f3218f5 --- /dev/null +++ b/lib/pages_desktop_specific/password/desktop_unlock_app_dialog.dart @@ -0,0 +1,206 @@ +/* + * 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 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../../../providers/desktop/storage_crypto_handler_provider.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/stack_text_field.dart'; +import '../../app_config.dart'; +import '../../notifications/show_flush_bar.dart'; +import '../../utilities/show_loading.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; + +class DesktopUnlockAppDialog extends ConsumerStatefulWidget { + const DesktopUnlockAppDialog({super.key}); + + @override + ConsumerState createState() => + _DesktopUnlockAppDialogState(); +} + +class _DesktopUnlockAppDialogState + extends ConsumerState { + late final TextEditingController passwordController; + late final FocusNode passwordFocusNode; + + bool hidePassword = true; + + bool _confirmEnabled = false; + bool _lock = false; + + Future _confirmPressed() async { + if (_lock) { + return; + } + _lock = true; + + try { + final passwordIsValid = await showLoading( + whileFuture: ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text), + context: context, + message: "Verifying password...", + delay: const Duration(seconds: 1), + ); + + if (mounted) { + if (passwordIsValid == true) { + Navigator.of(context, rootNavigator: true).pop(); + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Invalid password!", + context: context, + ), + ); + } + } + } finally { + _lock = false; + } + } + + @override + void initState() { + passwordController = TextEditingController(); + passwordFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordController.dispose(); + passwordFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxWidth: 579, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.svg.keys, width: 100), + const SizedBox(height: 56), + Text( + "Unlock ${AppConfig.appName}", + style: STextStyles.desktopH3(context), + ), + const SizedBox(height: 16), + Text( + "Enter your wallet password to unlock ${AppConfig.appName}", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + const SizedBox(height: 24), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("desktopUnlockAppPasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.desktopTextMedium( + context, + ).copyWith(height: 2), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_confirmEnabled) { + _confirmPressed(); + } + }, + decoration: standardInputDecoration( + "Enter password", + passwordFocusNode, + context, + ).copyWith( + suffixIcon: UnconstrainedBox( + child: SizedBox( + height: 70, + child: Row( + children: [ + const SizedBox(width: 24), + GestureDetector( + key: const Key( + "desktopUnlockAppPasswordFieldShowPasswordButtonKey", + ), + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of( + context, + ).extension()!.textDark3, + width: 24, + height: 24, + ), + ), + const SizedBox(width: 12), + ], + ), + ), + ), + ), + onChanged: (newValue) { + setState(() { + _confirmEnabled = passwordController.text.isNotEmpty; + }); + }, + ), + ), + const SizedBox(height: 48), + Row( + children: [ + const Spacer(), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + enabled: _confirmEnabled, + label: "Unlock", + buttonHeight: ButtonHeight.l, + onPressed: _confirmPressed, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_autolock_timeout_settings_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_autolock_timeout_settings_dialog.dart new file mode 100644 index 000000000..2ebd3c85b --- /dev/null +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_autolock_timeout_settings_dialog.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../../../../pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../widgets/desktop/desktop_dialog.dart'; +import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; + +class DesktopAutolockTimeoutSettingsDialog extends StatelessWidget { + const DesktopAutolockTimeoutSettingsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return DesktopDialog( + maxHeight: double.infinity, + maxWidth: 480, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: Text( + "Auto lock timeout", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + ), + const DesktopDialogCloseButton(), + ], + ), + + const Padding( + padding: EdgeInsets.only(left: 32, right: 32, bottom: 32, top: 20), + child: AutoLockTimeoutSettingsView(), + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart b/lib/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart index 98a7dec34..179ce7650 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/desktop_about_view.dart @@ -38,14 +38,8 @@ class DesktopAboutView extends ConsumerWidget { isCompactHeight: true, leading: Row( children: [ - const SizedBox( - width: 24, - height: 24, - ), - Text( - "About", - style: STextStyles.desktopH3(context), - ), + const SizedBox(width: 24, height: 24), + Text("About", style: STextStyles.desktopH3(context)), ], ), ), @@ -85,55 +79,63 @@ class DesktopAboutView extends ConsumerWidget { TextSpan( text: "By using ${AppConfig.appName}, you agree to the ", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark3, + ), ), TextSpan( text: "Terms of service", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/terms-of-service.html", - ), - mode: - LaunchMode.externalApplication, - ); - }, + style: STextStyles.richLink( + context, + ).copyWith(fontSize: 14), + recognizer: + TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/terms-of-service.html", + ), + mode: + LaunchMode + .externalApplication, + ); + }, ), TextSpan( text: " and ", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark3, + ), ), TextSpan( text: "Privacy policy", - style: STextStyles.richLink(context) - .copyWith(fontSize: 14), - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl( - Uri.parse( - "https://stackwallet.com/privacy-policy.html", - ), - mode: - LaunchMode.externalApplication, - ); - }, + style: STextStyles.richLink( + context, + ).copyWith(fontSize: 14), + recognizer: + TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse( + "https://stackwallet.com/privacy-policy.html", + ), + mode: + LaunchMode + .externalApplication, + ); + }, ), ], ), @@ -142,8 +144,10 @@ class DesktopAboutView extends ConsumerWidget { ), const SizedBox(height: 32), Padding( - padding: - const EdgeInsets.only(right: 10, bottom: 10), + padding: const EdgeInsets.only( + right: 10, + bottom: 10, + ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -182,33 +186,29 @@ class DesktopAboutView extends ConsumerWidget { children: [ Text( "Version", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, - ), - ), - const SizedBox( - height: 2, + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ), ), + const SizedBox(height: 2), SelectableText( version, style: STextStyles.itemSubtitle( - context, - ), + context, + ), ), ], ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: @@ -216,33 +216,29 @@ class DesktopAboutView extends ConsumerWidget { children: [ Text( "Build number", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, - ), - ), - const SizedBox( - height: 2, + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ), ), + const SizedBox(height: 2), SelectableText( build, style: STextStyles.itemSubtitle( - context, - ), + context, + ), ), ], ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: @@ -250,27 +246,25 @@ class DesktopAboutView extends ConsumerWidget { children: [ Text( "Build commit", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, - ), - ), - const SizedBox( - height: 2, + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ), ), + const SizedBox(height: 2), SelectableText( GitStatus.appCommitHash, style: STextStyles.itemSubtitle( - context, - ), + context, + ), ), ], ), @@ -286,27 +280,25 @@ class DesktopAboutView extends ConsumerWidget { children: [ Text( "Build signature", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, - ), - ), - const SizedBox( - height: 2, + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ), ), + const SizedBox(height: 2), SelectableText( signature, style: STextStyles.itemSubtitle( - context, - ), + context, + ), ), ], ), @@ -317,74 +309,16 @@ class DesktopAboutView extends ConsumerWidget { spacing: 64, runSpacing: 32, children: [ - if (AppConfig.coins - .whereType() - .isNotEmpty) - FutureBuilder( - future: GitStatus - .getFiroCommitStatus(), - builder: ( - context, - AsyncSnapshot - snapshot, - ) { - CommitStatus stateOfCommit = - CommitStatus.notLoaded; - - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - stateOfCommit = - snapshot.data!; - } - - return Column( - mainAxisSize: - MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - Text( - "Firo Build Commit", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, - ), - ), - const SizedBox( - height: 2, - ), - SelectableText( - GitStatus.firoCommit, - style: GitStatus - .styleForStatus( - stateOfCommit, - context, - ), - ), - ], - ); - }, - ), if (AppConfig.coins .whereType() .isNotEmpty) FutureBuilder( - future: GitStatus - .getEpicCommitStatus(), + future: + GitStatus.getEpicCommitStatus(), builder: ( context, AsyncSnapshot - snapshot, + snapshot, ) { CommitStatus stateOfCommit = CommitStatus.notLoaded; @@ -406,29 +340,26 @@ class DesktopAboutView extends ConsumerWidget { children: [ Text( "Epic Cash Build Commit", - style: STextStyles - .desktopTextExtraExtraSmall( + style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( GitStatus .epicCashCommit, - style: GitStatus - .styleForStatus( - stateOfCommit, - context, - ), + style: + GitStatus.styleForStatus( + stateOfCommit, + context, + ), ), ], ); @@ -498,14 +429,17 @@ class DesktopAboutView extends ConsumerWidget { children: [ Text( "Website:", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ), ), CustomTextButton( text: "https://stackwallet.com", @@ -514,8 +448,9 @@ class DesktopAboutView extends ConsumerWidget { Uri.parse( "https://stackwallet.com", ), - mode: LaunchMode - .externalApplication, + mode: + LaunchMode + .externalApplication, ); }, ), @@ -532,14 +467,17 @@ class DesktopAboutView extends ConsumerWidget { children: [ Text( "Tezos functionality:", - style: STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ), ), CustomTextButton( text: "Powered by TzKT API", @@ -548,8 +486,9 @@ class DesktopAboutView extends ConsumerWidget { Uri.parse( "https://tzkt.io", ), - mode: LaunchMode - .externalApplication, + mode: + LaunchMode + .externalApplication, ); }, ), diff --git a/lib/pages_desktop_specific/settings/settings_menu/security_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/security_settings.dart index 3b227f1d3..bb0ed0fda 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/security_settings.dart @@ -18,6 +18,7 @@ import 'package:zxcvbn/zxcvbn.dart'; import '../../../app_config.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../providers/desktop/storage_crypto_handler_provider.dart'; +import '../../../providers/global/prefs_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/constants.dart'; @@ -27,6 +28,7 @@ import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/progress_bar.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/stack_text_field.dart'; +import 'advanced_settings/desktop_autolock_timeout_settings_dialog.dart'; class SecuritySettings extends ConsumerStatefulWidget { const SecuritySettings({super.key}); @@ -69,8 +71,9 @@ class _SecuritySettings extends ConsumerState { final String pwNew = passwordController.text; final String pwNewRepeat = passwordRepeatController.text; - final verified = - await ref.read(storageCryptoHandlerProvider).verifyPassphrase(pw); + final verified = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(pw); if (verified) { if (pwNew != pwNewRepeat) { @@ -78,11 +81,9 @@ class _SecuritySettings extends ConsumerState { return (false, FlushBarType.warning, "New passphrase does not match!"); } else { - final success = - await ref.read(storageCryptoHandlerProvider).changePassphrase( - pw, - pwNew, - ); + final success = await ref + .read(storageCryptoHandlerProvider) + .changePassphrase(pw, pwNew); if (success) { await Future.delayed(const Duration(seconds: 1)); @@ -90,7 +91,7 @@ class _SecuritySettings extends ConsumerState { return ( true, FlushBarType.success, - "Passphrase successfully changed" + "Passphrase successfully changed", ); } else { await Future.delayed(const Duration(seconds: 1)); @@ -137,9 +138,7 @@ class _SecuritySettings extends ConsumerState { return Column( children: [ Padding( - padding: const EdgeInsets.only( - right: 30, - ), + padding: const EdgeInsets.only(right: 30), child: RoundedWhiteContainer( radiusMultiplier: 2, child: Column( @@ -162,392 +161,450 @@ class _SecuritySettings extends ConsumerState { "Change Password", style: STextStyles.desktopTextSmall(context), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Text( "Protect your ${AppConfig.appName} with a strong password. ${AppConfig.appName} does not store " "your password, and is therefore NOT able to restore it. Keep your password safe and secure.", style: STextStyles.desktopTextExtraExtraSmall(context), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), changePassword ? SizedBox( - width: 512, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Current password", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - textAlign: TextAlign.left, + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current password", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldKey", ), - child: TextField( - key: const Key( - "desktopSecurityRestoreFromFilePasswordFieldKey", - ), - focusNode: passwordCurrentFocusNode, - controller: passwordCurrentController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter current password", - passwordCurrentFocusNode, + focusNode: passwordCurrentFocusNode, + controller: passwordCurrentController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter current password", + passwordCurrentFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel( context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey", - ), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), + ), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey", ), - const SizedBox( - width: 12, + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark3, + width: 16, + height: 16, ), - ], - ), + ), + const SizedBox(width: 12), + ], ), ), - onChanged: (newValue) { - setState(() {}); - }, ), + onChanged: (newValue) { + setState(() {}); + }, ), - const SizedBox(height: 16), - Text( - "New password", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - textAlign: TextAlign.left, + ), + const SizedBox(height: 16), + Text( + "New password", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey1", ), - child: TextField( - key: const Key( - "desktopSecurityCreateNewPasswordFieldKey1", - ), - focusNode: passwordFocusNode, - controller: passwordController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Enter new password", - passwordFocusNode, + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter new password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel( context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityCreateNewPasswordButtonKey1", - ), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), + ), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey1", ), - const SizedBox( - width: 12, + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark3, + width: 16, + height: 16, ), - ], - ), + ), + const SizedBox(width: 12), + ], ), ), - onChanged: (newValue) { - if (newValue.isEmpty) { - setState(() { - passwordFeedback = ""; - }); - return; - } - final result = - zxcvbn.evaluate(newValue); - String suggestionsAndTips = ""; - for (final sug in result - .feedback.suggestions! - .toSet()) { - suggestionsAndTips += "$sug\n"; - } - suggestionsAndTips += - result.feedback.warning!; - String feedback = - // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" - suggestionsAndTips; - - passwordStrength = result.score! / 4; - - // hack fix to format back string returned from zxcvbn - if (feedback - .contains("phrasesNo need")) { - feedback = feedback.replaceFirst( - "phrasesNo need", - "phrases\nNo need", - ); - } - - if (feedback.endsWith("\n")) { - feedback = feedback.substring( - 0, - feedback.length - 2, - ); - } - + ), + onChanged: (newValue) { + if (newValue.isEmpty) { setState(() { - passwordFeedback = feedback; + passwordFeedback = ""; }); - }, - ), + return; + } + final result = zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (final sug + in result.feedback.suggestions! + .toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += + result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback.contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", + "phrases\nNo need", + ); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring( + 0, + feedback.length - 2, + ); + } + + setState(() { + passwordFeedback = feedback; + }); + }, ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: EdgeInsets.only( - left: 12, - right: 12, - top: - passwordFeedback.isNotEmpty ? 4 : 0, - ), - child: passwordFeedback.isNotEmpty - ? Text( + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: EdgeInsets.only( + left: 12, + right: 12, + top: passwordFeedback.isNotEmpty ? 4 : 0, + ), + child: + passwordFeedback.isNotEmpty + ? Text( passwordFeedback, style: STextStyles.infoSmall( context, ), ) - : null, + : null, + ), + if (passwordFocusNode.hasFocus || + passwordRepeatFocusNode.hasFocus || + passwordController.text.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 10, ), - if (passwordFocusNode.hasFocus || - passwordRepeatFocusNode.hasFocus || - passwordController.text.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 10, - ), - child: ProgressBar( - key: const Key( - "desktopSecurityCreateStackBackUpProgressBar", - ), - width: 450, - height: 5, - fillColor: passwordStrength < 0.51 - ? Theme.of(context) - .extension()! - .accentColorRed - : passwordStrength < 1 - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - backgroundColor: Theme.of(context) - .extension()! - .buttonBackSecondary, - percent: passwordStrength < 0.25 - ? 0.03 - : passwordStrength, + child: ProgressBar( + key: const Key( + "desktopSecurityCreateStackBackUpProgressBar", ), + width: 450, + height: 5, + fillColor: + passwordStrength < 0.51 + ? Theme.of(context) + .extension()! + .accentColorRed + : passwordStrength < 1 + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + backgroundColor: + Theme.of(context) + .extension()! + .buttonBackSecondary, + percent: + passwordStrength < 0.25 + ? 0.03 + : passwordStrength, ), - const SizedBox(height: 16), - Text( - "Confirm new password", - style: - STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - textAlign: TextAlign.left, ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + const SizedBox(height: 16), + Text( + "Confirm new password", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey2", ), - child: TextField( - key: const Key( - "desktopSecurityCreateNewPasswordFieldKey2", - ), - focusNode: passwordRepeatFocusNode, - controller: passwordRepeatController, - style: STextStyles.field(context), - obscureText: hidePassword, - enableSuggestions: false, - autocorrect: false, - decoration: standardInputDecoration( - "Confirm new password", - passwordRepeatFocusNode, + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm new password", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: STextStyles.fieldLabel( context, - ).copyWith( - labelStyle: - STextStyles.fieldLabel(context), - suffixIcon: UnconstrainedBox( - child: Row( - children: [ - const SizedBox( - width: 16, - ), - GestureDetector( - key: const Key( - "desktopSecurityCreateNewPasswordButtonKey2", - ), - onTap: () async { - setState(() { - hidePassword = - !hidePassword; - }); - }, - child: SvgPicture.asset( - hidePassword - ? Assets.svg.eye - : Assets.svg.eyeSlash, - color: Theme.of(context) - .extension()! - .textDark3, - width: 16, - height: 16, - ), + ), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox(width: 16), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey2", ), - const SizedBox( - width: 12, + onTap: () async { + setState(() { + hidePassword = !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark3, + width: 16, + height: 16, ), - ], - ), + ), + const SizedBox(width: 12), + ], ), ), - onChanged: (newValue) { - setState(() {}); - }, ), + onChanged: (newValue) { + setState(() {}); + }, ), - const SizedBox(height: 20), - PrimaryButton( - width: 160, - buttonHeight: ButtonHeight.l, - enabled: shouldEnableSave, - label: "Save changes", - onPressed: () async { - if (_changePWLock) { - return; + ), + const SizedBox(height: 20), + PrimaryButton( + width: 160, + buttonHeight: ButtonHeight.l, + enabled: shouldEnableSave, + label: "Save changes", + onPressed: () async { + if (_changePWLock) { + return; + } + _changePWLock = true; + + try { + final (didChangePW, type, message) = + (await showLoading( + whileFuture: _attemptChangePW(), + context: context, + message: "Updating...", + rootNavigator: true, + ))!; + + if (mounted) { + unawaited( + showFloatingFlushBar( + type: type, + message: message, + context: context, + ), + ); } - _changePWLock = true; - - try { - final (didChangePW, type, message) = - (await showLoading( - whileFuture: _attemptChangePW(), - context: context, - message: "Updating...", - rootNavigator: true, - ))!; - - if (mounted) { - unawaited( - showFloatingFlushBar( - type: type, - message: message, - context: context, - ), - ); - } - - if (didChangePW == true) { - setState(() { - changePassword = false; - }); - } - } finally { - _changePWLock = false; + + if (didChangePW == true) { + setState(() { + changePassword = false; + }); } - }, - ), - ], - ), - ) + } finally { + _changePWLock = false; + } + }, + ), + ], + ), + ) : PrimaryButton( - width: 210, - buttonHeight: ButtonHeight.m, - enabled: true, - label: "Set up new password", - onPressed: () { - setState(() { - changePassword = true; - }); - }, + width: 210, + buttonHeight: ButtonHeight.m, + enabled: true, + label: "Set up new password", + onPressed: () { + setState(() { + changePassword = true; + }); + }, + ), + + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider(thickness: 0.5), + ), + + Consumer( + builder: (_, ref, __) { + final autoLockInfo = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.autoLockInfo, ), + ); + return Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Auto lock timeout", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), + textAlign: TextAlign.left, + ), + Text( + autoLockInfo.enabled + ? "${autoLockInfo.minutes} minutes" + : "Disabled", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + ], + ), + PrimaryButton( + buttonHeight: ButtonHeight.xs, + label: "Edit", + width: 101, + onPressed: () async { + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const DesktopAutolockTimeoutSettingsDialog(); + }, + ); + }, + ), + ], + ), + ); + }, + ), ], ), ), diff --git a/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart index 8a937fab3..c2f2519c6 100644 --- a/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart +++ b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart @@ -26,10 +26,7 @@ import '../../widgets/desktop/desktop_scaffold.dart'; import '../../widgets/isar_collection_watcher_list.dart'; class SparkCoinsView extends ConsumerWidget { - const SparkCoinsView({ - super.key, - required this.walletId, - }); + const SparkCoinsView({super.key, required this.walletId}); static const title = "Spark coins"; static const String routeName = "/sparkCoinsView"; @@ -47,32 +44,27 @@ class SparkCoinsView extends ConsumerWidget { leading: Expanded( child: Row( children: [ - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), AppBarIconButton( size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, width: 18, height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: Navigator.of(context).pop, ), - const SizedBox( - width: 12, - ), - Text( - title, - style: STextStyles.desktopH3(context), - ), + const SizedBox(width: 12), + Text(title, style: STextStyles.desktopH3(context)), const Spacer(), ], ), @@ -80,10 +72,7 @@ class SparkCoinsView extends ConsumerWidget { useSpacers: false, isCompactHeight: true, ), - body: Padding( - padding: const EdgeInsets.all(24), - child: child, - ), + body: Padding(padding: const EdgeInsets.all(24), child: child), ); }, child: ConditionalParent( @@ -98,26 +87,23 @@ class SparkCoinsView extends ConsumerWidget { leading: AppBarBackButton( onPressed: () => Navigator.of(context).pop(), ), - title: Text( - title, - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: child, + title: Text(title, style: STextStyles.navBarTitle(context)), ), + body: SafeArea(child: child), ), ); }, child: IsarCollectionWatcherList( itemName: title, - queryBuilder: () => ref - .read(mainDBProvider) - .isar - .sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .sortByHeightDesc(), + queryBuilder: + () => + ref + .read(mainDBProvider) + .isar + .sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .sortByHeightDesc(), itemBuilder: (SparkCoin? coin) { return [ ("TXID", coin?.txHash ?? "", 9), @@ -129,6 +115,7 @@ class SparkCoinsView extends ConsumerWidget { ("Group ID", coin?.groupId.toString() ?? "", 2), ("Type", coin?.type.name ?? "", 2), ("Used", coin?.isUsed.toString() ?? "", 2), + ("Locked", coin?.isLocked.toString() ?? "", 2), ]; }, ), diff --git a/lib/providers/db/drift_provider.dart b/lib/providers/db/drift_provider.dart new file mode 100644 index 000000000..658dd5bc7 --- /dev/null +++ b/lib/providers/db/drift_provider.dart @@ -0,0 +1,17 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-05-06 + * + */ + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../db/drift/database.dart'; + +final pDrift = Provider.family( + (ref, walletId) => Drift.get(walletId), +); diff --git a/lib/providers/global/barcode_scanner_provider.dart b/lib/providers/global/barcode_scanner_provider.dart new file mode 100644 index 000000000..3623b20a1 --- /dev/null +++ b/lib/providers/global/barcode_scanner_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../utilities/barcode_scanner_interface.dart'; + +final pBarcodeScanner = Provider( + (ref) => const BarcodeScannerWrapper(), +); diff --git a/lib/providers/global/clipboard_provider.dart b/lib/providers/global/clipboard_provider.dart new file mode 100644 index 000000000..956d03c02 --- /dev/null +++ b/lib/providers/global/clipboard_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../utilities/clipboard_interface.dart'; + +final pClipboard = Provider( + (ref) => const ClipboardWrapper(), +); diff --git a/lib/providers/global/duress_provider.dart b/lib/providers/global/duress_provider.dart new file mode 100644 index 000000000..22ef4072d --- /dev/null +++ b/lib/providers/global/duress_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final pDuress = StateProvider((ref) => false); + diff --git a/lib/providers/global/mweb_service_provider.dart b/lib/providers/global/mweb_service_provider.dart new file mode 100644 index 000000000..1cddcb407 --- /dev/null +++ b/lib/providers/global/mweb_service_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../services/mwebd_service.dart'; + +final pMwebService = StateProvider( + (ref) => MwebdService.instance, +); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index e6e9ef20b..d86e4de3b 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -11,13 +11,19 @@ export './buy/buy_form_state_provider.dart'; export './buy/simplex_initial_load_status.dart'; export './buy/simplex_provider.dart'; +export './db/drift_provider.dart'; +export './db/main_db_provider.dart'; export './exchange/changenow_initial_load_status.dart'; export './exchange/exchange_flow_is_active_state_provider.dart'; export './exchange/exchange_form_state_provider.dart'; export './exchange/exchange_send_from_wallet_id_provider.dart'; export './exchange/trade_note_service_provider.dart'; export './exchange/trade_sent_from_stack_lookup_provider.dart'; +export './global/barcode_scanner_provider.dart'; +export './global/clipboard_provider.dart'; +export './global/duress_provider.dart'; export './global/locale_provider.dart'; +export './global/mweb_service_provider.dart'; export './global/node_service_provider.dart'; export './global/notifications_provider.dart'; export './global/prefs_provider.dart'; @@ -25,6 +31,7 @@ export './global/price_provider.dart'; export './global/should_show_lockscreen_on_resume_state_provider.dart'; export './global/wallets_provider.dart'; export './global/wallets_service_provider.dart'; +export './progress_report/xelis_table_progress_provider.dart'; export './ui/add_wallet_selected_coin_provider.dart'; export './ui/check_box_state_provider.dart'; export './ui/home_view_index_provider.dart'; @@ -32,4 +39,3 @@ export './ui/verify_recovery_phrase/correct_word_provider.dart'; export './ui/verify_recovery_phrase/random_index_provider.dart'; export './ui/verify_recovery_phrase/selected_word_provider.dart'; export './wallet/transaction_note_provider.dart'; -export './progress_report/xelis_table_progress_provider.dart'; diff --git a/lib/providers/ui/fee_rate_type_state_provider.dart b/lib/providers/ui/fee_rate_type_state_provider.dart index 3e1421c14..8b309a2e5 100644 --- a/lib/providers/ui/fee_rate_type_state_provider.dart +++ b/lib/providers/ui/fee_rate_type_state_provider.dart @@ -9,7 +9,13 @@ */ import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../utilities/enums/fee_rate_type_enum.dart'; -final feeRateTypeStateProvider = - StateProvider.autoDispose((_) => FeeRateType.average); +final feeRateTypeMobileStateProvider = StateProvider.autoDispose( + (_) => FeeRateType.average, +); + +final feeRateTypeDesktopStateProvider = StateProvider( + (_) => FeeRateType.average, +); diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart index 196fe298d..9ff71aca7 100644 --- a/lib/providers/ui/preview_tx_button_state_provider.dart +++ b/lib/providers/ui/preview_tx_button_state_provider.dart @@ -20,31 +20,26 @@ final pValidSparkSendToAddress = StateProvider.autoDispose((_) => false); final pIsExchangeAddress = StateProvider((_) => false); -final pPreviewTxButtonEnabled = - Provider.autoDispose.family((ref, coin) { - final amount = ref.watch(pSendAmount) ?? Amount.zero; - - if (coin is Firo) { - final firoType = ref.watch(publicPrivateBalanceStateProvider); - switch (firoType) { - case FiroType.lelantus: - return ref.watch(pValidSendToAddress) && - !ref.watch(pValidSparkSendToAddress) && - amount > Amount.zero; - - case FiroType.spark: - return (ref.watch(pValidSendToAddress) || - ref.watch(pValidSparkSendToAddress)) && - !ref.watch(pIsExchangeAddress) && - amount > Amount.zero; - - case FiroType.public: +final pPreviewTxButtonEnabled = Provider.autoDispose + .family((ref, coin) { + final amount = ref.watch(pSendAmount) ?? Amount.zero; + + if (coin is Firo) { + final firoType = ref.watch(publicPrivateBalanceStateProvider); + switch (firoType) { + case BalanceType.private: + return (ref.watch(pValidSendToAddress) || + ref.watch(pValidSparkSendToAddress)) && + !ref.watch(pIsExchangeAddress) && + amount > Amount.zero; + + case BalanceType.public: + return ref.watch(pValidSendToAddress) && amount > Amount.zero; + } + } else { return ref.watch(pValidSendToAddress) && amount > Amount.zero; - } - } else { - return ref.watch(pValidSendToAddress) && amount > Amount.zero; - } -}); + } + }); final previewTokenTxButtonStateProvider = StateProvider.autoDispose((_) { return false; diff --git a/lib/providers/wallet/desktop_fee_providers.dart b/lib/providers/wallet/desktop_fee_providers.dart new file mode 100644 index 000000000..d1723c78c --- /dev/null +++ b/lib/providers/wallet/desktop_fee_providers.dart @@ -0,0 +1,23 @@ +/* + * 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_riverpod/flutter_riverpod.dart'; + +import '../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import '../../utilities/amount/amount.dart'; + +final tokenFeeSessionCacheProvider = + ChangeNotifierProvider((ref) { + return FeeSheetSessionCache(); + }); + +final sendAmountProvider = StateProvider.autoDispose( + (_) => Amount.zero, +); diff --git a/lib/providers/wallet/public_private_balance_state_provider.dart b/lib/providers/wallet/public_private_balance_state_provider.dart index 8fa012edb..39ce0ae88 100644 --- a/lib/providers/wallet/public_private_balance_state_provider.dart +++ b/lib/providers/wallet/public_private_balance_state_provider.dart @@ -10,11 +10,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -enum FiroType { - public, - lelantus, - spark; -} +enum BalanceType { public, private } -final publicPrivateBalanceStateProvider = - StateProvider((_) => FiroType.spark); +final publicPrivateBalanceStateProvider = StateProvider( + (_) => BalanceType.private, +); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 6695a53b8..ee09bdba0 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -13,6 +13,7 @@ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; +import 'db/drift/database.dart'; import 'models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'models/buy/response_objects/quote.dart'; @@ -112,7 +113,9 @@ import 'pages/settings_views/global_settings_view/manage_nodes_views/add_edit_no import 'pages/settings_views/global_settings_view/manage_nodes_views/coin_nodes_view.dart'; import 'pages/settings_views/global_settings_view/manage_nodes_views/manage_nodes_view.dart'; import 'pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; +import 'pages/settings_views/global_settings_view/security_views/auto_lock_timeout_settings_view.dart'; import 'pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart'; +import 'pages/settings_views/global_settings_view/security_views/create_duress_pin_view.dart'; import 'pages/settings_views/global_settings_view/security_views/security_view.dart'; import 'pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart'; import 'pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart'; @@ -141,12 +144,15 @@ import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_setting import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/edit_refresh_height_view.dart'; -import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/lelantus_settings_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rbf_settings_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; +import 'pages/spark_names/buy_spark_name_view.dart'; +import 'pages/spark_names/confirm_spark_name_transaction_view.dart'; +import 'pages/spark_names/spark_names_home_view.dart'; +import 'pages/spark_names/sub_widgets/spark_name_details.dart'; import 'pages/special/firo_rescan_recovery_error_dialog.dart'; import 'pages/stack_privacy_calls.dart'; import 'pages/token_view/my_tokens_view.dart'; @@ -173,7 +179,7 @@ import 'pages_desktop_specific/desktop_buy/desktop_buy_view.dart'; import 'pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart'; import 'pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'pages_desktop_specific/desktop_home_view.dart'; -import 'pages_desktop_specific/lelantus_coins/lelantus_coins_view.dart'; +import 'pages_desktop_specific/mweb_utxos_view.dart'; import 'pages_desktop_specific/my_stack_view/my_stack_view.dart'; import 'pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; import 'pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; @@ -253,12 +259,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreatePinView( - popOnSuccess: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreatePinView(popOnSuccess: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -285,14 +287,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChooseCoinView( - title: args.item1, - coinAdditional: args.item2, - nextRouteName: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ChooseCoinView( + title: args.item1, + coinAdditional: args.item2, + nextRouteName: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -301,12 +302,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ManageExplorerView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ManageExplorerView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -315,12 +312,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FiroRescanRecoveryErrorView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FiroRescanRecoveryErrorView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -343,23 +336,18 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditWalletTokensView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditWalletTokensView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditWalletTokensView( - walletId: args.item1, - contractsToMarkSelected: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditWalletTokensView( + walletId: args.item1, + contractsToMarkSelected: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -368,12 +356,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopTokenView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopTokenView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -382,12 +366,8 @@ class RouteGenerator { if (args is EthTokenEntity) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SelectWalletForTokenView( - entity: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SelectWalletForTokenView(entity: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -396,21 +376,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const AddCustomTokenView(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case WalletsOverview.routeName: if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletsOverview( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletsOverview(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -419,13 +393,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenContractDetailsView( - contractAddress: args.item1, - walletId: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenContractDetailsView( + contractAddress: args.item1, + walletId: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -434,13 +407,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SingleFieldEditView( - initialValue: args.item1, - label: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SingleFieldEditView( + initialValue: args.item1, + label: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -449,66 +421,50 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MonkeyView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => MonkeyView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case CreateNewFrostMsWalletView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreateNewFrostMsWalletView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CreateNewFrostMsWalletView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case RestoreFrostMsWalletView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreFrostMsWalletView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreFrostMsWalletView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case SelectNewFrostImportTypeView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SelectNewFrostImportTypeView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SelectNewFrostImportTypeView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -517,21 +473,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const FrostStepScaffold(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case FrostMSWalletOptionsView.routeName: if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostMSWalletOptionsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FrostMSWalletOptionsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -540,12 +490,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostParticipantsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FrostParticipantsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -554,12 +500,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => InitiateResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => InitiateResharingView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -568,31 +510,23 @@ class RouteGenerator { if (args is ({String walletId, Map resharers})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CompleteReshareConfigView( - walletId: args.walletId, - resharers: args.resharers, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CompleteReshareConfigView( + walletId: args.walletId, + resharers: args.resharers, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case FrostSendView.routeName: - if (args is ({ - String walletId, - CryptoCurrency coin, - })) { + if (args is ({String walletId, CryptoCurrency coin})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostSendView( - walletId: args.walletId, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => FrostSendView(walletId: args.walletId, coin: args.coin), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -616,27 +550,22 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinControlView( - walletId: args.item1, - type: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CoinControlView(walletId: args.item1, type: args.item2), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple4?>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinControlView( - walletId: args.item1, - type: args.item2, - requestedTotal: args.item3, - selectedUTXOs: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CoinControlView( + walletId: args.item1, + type: args.item2, + requestedTotal: args.item3, + selectedUTXOs: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -645,12 +574,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => OrdinalsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => OrdinalsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -659,12 +584,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopOrdinalsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopOrdinalsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -673,13 +594,12 @@ class RouteGenerator { if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => OrdinalDetailsView( - walletId: args.walletId, - ordinal: args.ordinal, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => OrdinalDetailsView( + walletId: args.walletId, + ordinal: args.ordinal, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -688,13 +608,12 @@ class RouteGenerator { if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopOrdinalDetailsView( - walletId: args.walletId, - ordinal: args.ordinal, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DesktopOrdinalDetailsView( + walletId: args.walletId, + ordinal: args.ordinal, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -710,13 +629,10 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => UtxoDetailsView( - walletId: args.item2, - utxoId: args.item1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => + UtxoDetailsView(walletId: args.item2, utxoId: args.item1), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -725,13 +641,8 @@ class RouteGenerator { if (args is (Id, String)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NameDetailsView( - walletId: args.$2, - utxoId: args.$1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NameDetailsView(walletId: args.$2, utxoId: args.$1), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -740,12 +651,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => PaynymClaimView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => PaynymClaimView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -754,12 +661,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => PaynymHomeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => PaynymHomeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -768,12 +671,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddNewPaynymFollowView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddNewPaynymFollowView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -782,12 +681,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CashFusionView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CashFusionView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -796,12 +691,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NamecoinNamesHomeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NamecoinNamesHomeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -810,13 +701,172 @@ class RouteGenerator { if (args is ({String walletId, UTXO utxo})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ManageDomainView( - walletId: args.walletId, - utxo: args.utxo, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => + ManageDomainView(walletId: args.walletId, utxo: args.utxo), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + case SparkNamesHomeView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SparkNamesHomeView(walletId: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case BuySparkNameView.routeName: + if (args is ({String walletId, String name})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => + BuySparkNameView(walletId: args.walletId, name: args.name), + settings: RouteSettings(name: settings.name), + ); + } else if (args + is ({String walletId, String name, SparkName? nameToRenew})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => BuySparkNameView( + walletId: args.walletId, + name: args.name, + nameToRenew: args.nameToRenew, + ), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case ConfirmSparkNameTransactionView.routeName: + if (args is ({String walletId, TxData txData})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => ConfirmSparkNameTransactionView( + walletId: args.walletId, + txData: args.txData, + ), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case SparkNameDetailsView.routeName: + if (args is ({String walletId, SparkName name})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => SparkNameDetailsView( + walletId: args.walletId, + name: args.name, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -825,12 +875,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FusionProgressView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FusionProgressView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -839,12 +885,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChurningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChurningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -853,12 +895,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChurningProgressView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChurningProgressView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -867,12 +905,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopCashFusionView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopCashFusionView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -881,12 +915,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopChurningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopChurningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -902,12 +932,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddressBookView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddressBookView(coin: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -965,6 +991,20 @@ class RouteGenerator { settings: RouteSettings(name: settings.name), ); + case CreateDuressPinView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const CreateDuressPinView(), + settings: RouteSettings(name: settings.name), + ); + + case AutoLockTimeoutSettingsView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AutoLockTimeoutSettingsView(), + settings: RouteSettings(name: settings.name), + ); + case BaseCurrencySettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, @@ -1011,13 +1051,8 @@ class RouteGenerator { if (args is (String, ({List xpubs, String fingerprint}))) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => XPubView( - walletId: args.$1, - xpubData: args.$2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => XPubView(walletId: args.$1, xpubData: args.$2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1026,12 +1061,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChangeRepresentativeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChangeRepresentativeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1117,12 +1148,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreFromEncryptedStringView( - encrypted: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => RestoreFromEncryptedStringView(encrypted: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1138,12 +1165,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditCoinUnitsView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditCoinUnitsView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1173,12 +1196,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinNodesView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CoinNodesView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1187,14 +1206,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NodeDetailsView( - coin: args.item1, - nodeId: args.item2, - popRouteName: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NodeDetailsView( + coin: args.item1, + nodeId: args.item2, + popRouteName: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1203,13 +1221,9 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditNoteView( - txid: args.item1, - walletId: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditNoteView(txid: args.item1, walletId: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1218,12 +1232,8 @@ class RouteGenerator { if (args is int) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditAddressLabelView( - addressLabelId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditAddressLabelView(addressLabelId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1232,13 +1242,9 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditTradeNoteView( - tradeId: args.item1, - note: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditTradeNoteView(tradeId: args.item1, note: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1248,15 +1254,14 @@ class RouteGenerator { is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddEditNodeView( - viewType: args.item1, - coin: args.item2, - nodeId: args.item3, - routeOnSuccessOrDelete: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AddEditNodeView( + viewType: args.item1, + coin: args.item2, + nodeId: args.item3, + routeOnSuccessOrDelete: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1265,12 +1270,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ContactDetailsView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ContactDetailsView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1279,12 +1280,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddNewContactAddressView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddNewContactAddressView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1293,12 +1290,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditContactNameEmojiView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditContactNameEmojiView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1307,13 +1300,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditContactAddressView( - contactId: args.item1, - addressEntry: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditContactAddressView( + contactId: args.item1, + addressEntry: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1322,23 +1314,20 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const SystemBrightnessThemeSelectionView(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case WalletNetworkSettingsView.routeName: if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletNetworkSettingsView( - walletId: args.item1, - initialSyncStatus: args.item2, - initialNodeStatus: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletNetworkSettingsView( + walletId: args.item1, + initialSyncStatus: args.item2, + initialNodeStatus: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1347,91 +1336,88 @@ class RouteGenerator { if (args is ({String walletId, List mnemonic})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletBackupView( - walletId: args.walletId, - mnemonic: args.mnemonic, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletBackupView( + 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, - ), + } 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), ); - } else if (args is ({ - String walletId, - List mnemonic, - KeyDataInterface? keyData, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletBackupView( - walletId: args.walletId, - mnemonic: args.mnemonic, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonic, + KeyDataInterface? keyData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List mnemonic, - KeyDataInterface? keyData, - ({ - 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, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonic, + KeyDataInterface? keyData, + ({ + 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, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case MobileKeyDataView.routeName: - if (args is ({ - String walletId, - KeyDataInterface keyData, - })) { + if (args is ({String walletId, KeyDataInterface keyData})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MobileKeyDataView( - walletId: args.walletId, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => MobileKeyDataView( + walletId: args.walletId, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1440,12 +1426,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletSettingsWalletSettingsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletSettingsWalletSettingsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1454,12 +1436,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RenameWalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => RenameWalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1468,12 +1446,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletWarningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DeleteWalletWarningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1482,12 +1456,8 @@ class RouteGenerator { if (args is AddWalletListEntity) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreateOrRestoreWalletView( - entity: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreateOrRestoreWalletView(entity: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1496,13 +1466,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NameYourWalletView( - addWalletType: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NameYourWalletView( + addWalletType: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1511,13 +1480,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletRecoveryPhraseWarningView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NewWalletRecoveryPhraseWarningView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1526,13 +1494,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreOptionsView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreOptionsView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1540,56 +1507,50 @@ class RouteGenerator { case NewWalletOptionsView.routeName: if (args is Tuple2) { return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletOptionsView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => NewWalletOptionsView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case RestoreWalletView.routeName: - if (args - is Tuple6) { + if (args is Tuple5) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreWalletView( - walletName: args.item1, - coin: args.item2, - seedWordsLength: args.item3, - restoreFromDate: args.item4, - mnemonicPassphrase: args.item5, - enableLelantusScanning: args.item6 ?? false, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreWalletView( + walletName: args.item1, + coin: args.item2, + seedWordsLength: args.item3, + restoreBlockHeight: args.item4, + mnemonicPassphrase: args.item5, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case RestoreViewOnlyWalletView.routeName: - if (args is ({ - String walletName, - CryptoCurrency coin, - DateTime? restoreFromDate, - bool enableLelantusScanning, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreViewOnlyWalletView( - walletName: args.walletName, - coin: args.coin, - restoreFromDate: args.restoreFromDate, - enableLelantusScanning: args.enableLelantusScanning, - ), - settings: RouteSettings( - name: settings.name, - ), + if (args + is ({ + String walletName, + CryptoCurrency coin, + int restoreBlockHeight, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => RestoreViewOnlyWalletView( + walletName: args.walletName, + coin: args.coin, + restoreBlockHeight: args.restoreBlockHeight, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1598,13 +1559,12 @@ class RouteGenerator { if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletRecoveryPhraseView( - wallet: args.item1, - mnemonic: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NewWalletRecoveryPhraseView( + wallet: args.item1, + mnemonic: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1613,13 +1573,12 @@ class RouteGenerator { if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => VerifyRecoveryPhraseView( - wallet: args.item1, - mnemonic: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => VerifyRecoveryPhraseView( + wallet: args.item1, + mnemonic: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1634,12 +1593,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1648,54 +1603,49 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionDetailsView( - transaction: args.item1, - coin: args.item2, - walletId: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TransactionDetailsView( + transaction: args.item1, + coin: args.item2, + walletId: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case TransactionV2DetailsView.routeName: - if (args is ({ - TransactionV2 tx, - CryptoCurrency coin, - String walletId - })) { + if (args + is ({TransactionV2 tx, CryptoCurrency coin, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionV2DetailsView( - transaction: args.tx, - coin: args.coin, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TransactionV2DetailsView( + transaction: args.tx, + coin: args.coin, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case FusionGroupDetailsView.routeName: - if (args is ({ - List transactions, - CryptoCurrency coin, - String walletId - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FusionGroupDetailsView( - transactions: args.transactions, - coin: args.coin, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + if (args + is ({ + List transactions, + CryptoCurrency coin, + String walletId, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => FusionGroupDetailsView( + transactions: args.transactions, + coin: args.coin, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1704,12 +1654,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AllTransactionsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1718,24 +1664,19 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsV2View( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AllTransactionsV2View(walletId: args), + settings: RouteSettings(name: settings.name), ); } if (args is ({String walletId, String contractAddress})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsV2View( - walletId: args.walletId, - contractAddress: args.contractAddress, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AllTransactionsV2View( + walletId: args.walletId, + contractAddress: args.contractAddress, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1744,12 +1685,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionSearchFilterView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => TransactionSearchFilterView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1758,23 +1695,18 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ReceiveView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ReceiveView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ReceiveView( - walletId: args.item1, - tokenContract: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ReceiveView( + walletId: args.item1, + tokenContract: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1783,12 +1715,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletAddressesView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletAddressesView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1797,13 +1725,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddressDetailsView( - walletId: args.item2, - addressId: args.item1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AddressDetailsView( + walletId: args.item2, + addressId: args.item1, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1812,49 +1739,37 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SendView(walletId: args.item1, coin: args.item2), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - autoFillData: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendView( + walletId: args.item1, + coin: args.item2, + autoFillData: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - accountLite: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendView( + walletId: args.item1, + coin: args.item2, + accountLite: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } else if (args is ({CryptoCurrency coin, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.walletId, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SendView(walletId: args.walletId, coin: args.coin), + settings: RouteSettings(name: settings.name), ); } @@ -1864,14 +1779,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenSendView( - walletId: args.item1, - coin: args.item2, - tokenContract: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenSendView( + walletId: args.item1, + coin: args.item2, + tokenContract: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1880,14 +1794,13 @@ class RouteGenerator { if (args is (TxData, String, VoidCallback)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: args.$1, - walletId: args.$2, - onSuccess: args.$3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ConfirmTransactionView( + txData: args.$1, + walletId: args.$2, + onSuccess: args.$3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1896,13 +1809,12 @@ class RouteGenerator { if (args is (TxData, String)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ConfirmNameTransactionView( - txData: args.$1, - walletId: args.$2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ConfirmNameTransactionView( + txData: args.$1, + walletId: args.$2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1911,40 +1823,38 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Stack( - children: [ - WalletInitiatedExchangeView( - walletId: args.item1, - coin: args.item2, + builder: + (_) => Stack( + children: [ + WalletInitiatedExchangeView( + walletId: args.item1, + coin: args.item2, + ), + // ExchangeLoadingOverlayView( + // unawaitedLoad: args.item3, + // ), + ], ), - // ExchangeLoadingOverlayView( - // unawaitedLoad: args.item3, - // ), - ], - ), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Stack( - children: [ - WalletInitiatedExchangeView( - walletId: args.item1, - coin: args.item2, - contract: args.item3, + builder: + (_) => Stack( + children: [ + WalletInitiatedExchangeView( + walletId: args.item1, + coin: args.item2, + contract: args.item3, + ), + // ExchangeLoadingOverlayView( + // unawaitedLoad: args.item3, + // ), + ], ), - // ExchangeLoadingOverlayView( - // unawaitedLoad: args.item3, - // ), - ], - ), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1953,30 +1863,30 @@ class RouteGenerator { if (args is String?) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NotificationsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NotificationsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case WalletSettingsView.routeName: - if (args is Tuple4) { + if (args + is Tuple4< + String, + CryptoCurrency, + WalletSyncStatus, + NodeConnectionStatus + >) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletSettingsView( - walletId: args.item1, - coin: args.item2, - initialSyncStatus: args.item3, - initialNodeStatus: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletSettingsView( + walletId: args.item1, + coin: args.item2, + initialSyncStatus: args.item3, + initialNodeStatus: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1985,34 +1895,34 @@ class RouteGenerator { if (args is ({String walletId, List mnemonicWords})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletRecoveryPhraseView( - mnemonic: args.mnemonicWords, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DeleteWalletRecoveryPhraseView( + mnemonic: args.mnemonicWords, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List mnemonicWords, - ({ - String myName, - String config, - String keys, - ({String config, String keys})? prevGen, - })? frostWalletData, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletRecoveryPhraseView( - mnemonic: args.mnemonicWords, - walletId: args.walletId, - frostWalletData: args.frostWalletData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonicWords, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostWalletData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => DeleteWalletRecoveryPhraseView( + mnemonic: args.mnemonicWords, + walletId: args.walletId, + frostWalletData: args.frostWalletData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2021,13 +1931,12 @@ class RouteGenerator { if (args is ({String walletId, ViewOnlyWalletData data})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteViewOnlyWalletKeysView( - data: args.data, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DeleteViewOnlyWalletKeysView( + data: args.data, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2038,12 +1947,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step1View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step1View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2052,12 +1957,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step2View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step2View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2066,12 +1967,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step3View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step3View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2080,12 +1977,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step4View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step4View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2094,15 +1987,14 @@ class RouteGenerator { if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TradeDetailsView( - tradeId: args.item1, - transactionIfSentFromStack: args.item2, - walletId: args.item3, - walletName: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TradeDetailsView( + tradeId: args.item1, + transactionIfSentFromStack: args.item2, + walletId: args.item3, + walletName: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2111,12 +2003,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChooseFromStackView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChooseFromStackView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2125,15 +2013,14 @@ class RouteGenerator { if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendFromView( - coin: args.item1, - amount: args.item2, - trade: args.item4, - address: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendFromView( + coin: args.item1, + amount: args.item2, + trade: args.item4, + address: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2142,13 +2029,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: args.item1, - receivingAddress: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => GenerateUriQrCodeView( + coin: args.item1, + receivingAddress: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2157,24 +2043,8 @@ class RouteGenerator { if (args is SimplexQuote) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyQuotePreviewView( - quote: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); - - case LelantusSettingsView.routeName: - if (args is String) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => LelantusSettingsView(walletId: args), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => BuyQuotePreviewView(quote: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2184,9 +2054,7 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => RbfSettingsView(walletId: args), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2195,12 +2063,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SparkInfoView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SparkInfoView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2209,12 +2073,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditRefreshHeightView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditRefreshHeightView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2223,13 +2083,12 @@ class RouteGenerator { if (args is ({String walletId, String domainName})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyDomainView( - walletId: args.walletId, - domainName: args.domainName, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => BuyDomainView( + walletId: args.walletId, + domainName: args.domainName, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2239,12 +2098,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreatePasswordView( - restoreFromSWB: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreatePasswordView(restoreFromSWB: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -2271,12 +2126,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeletePasswordWarningView( - shouldCreateNew: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DeletePasswordWarningView(shouldCreateNew: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2314,21 +2165,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => BuyInWalletView(coin: args), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyInWalletView( - coin: args.item1, - contract: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => BuyInWalletView(coin: args.item1, contract: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2365,12 +2210,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopWalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopWalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2379,40 +2220,28 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopWalletAddressesView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopWalletAddressesView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case LelantusCoinsView.routeName: + case SparkCoinsView.routeName: if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => LelantusCoinsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SparkCoinsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case SparkCoinsView.routeName: + case MwebUtxosView.routeName: if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SparkCoinsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => MwebUtxosView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2421,12 +2250,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopCoinControlView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopCoinControlView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2435,12 +2260,8 @@ class RouteGenerator { if (args is TransactionV2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BoostTransactionView( - transaction: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => BoostTransactionView(transaction: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2530,27 +2351,39 @@ class RouteGenerator { ); case WalletKeysDesktopPopup.routeName: - if (args is ({ - List mnemonic, - String walletId, - ({String keys, String config})? frostData - })) { + if (args + is ({ + List mnemonic, + String walletId, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, walletId: args.walletId, frostData: args.frostData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); - } else if (args is ({ - List mnemonic, - String walletId, - ({String keys, String config})? frostData, - KeyDataInterface? keyData, - })) { + } else if (args + is ({ + List mnemonic, + String walletId, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostData, + KeyDataInterface? keyData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, @@ -2558,24 +2391,21 @@ class RouteGenerator { frostData: args.frostData, keyData: args.keyData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); - } else if (args is ({ - List mnemonic, - String walletId, - KeyDataInterface? keyData, - })) { + } else if (args + is ({ + List mnemonic, + String walletId, + KeyDataInterface? keyData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, walletId: args.walletId, keyData: args.keyData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2583,12 +2413,8 @@ class RouteGenerator { case UnlockWalletKeysDesktop.routeName: if (args is String) { return FadePageRoute( - UnlockWalletKeysDesktop( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + UnlockWalletKeysDesktop(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2605,12 +2431,8 @@ class RouteGenerator { case DesktopDeleteWalletDialog.routeName: if (args is String) { return FadePageRoute( - DesktopDeleteWalletDialog( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + DesktopDeleteWalletDialog(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2627,12 +2449,8 @@ class RouteGenerator { case DesktopAttentionDeleteWallet.routeName: if (args is String) { return FadePageRoute( - DesktopAttentionDeleteWallet( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + DesktopAttentionDeleteWallet(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2649,13 +2467,8 @@ class RouteGenerator { case DeleteWalletKeysPopup.routeName: if (args is Tuple2>) { return FadePageRoute( - DeleteWalletKeysPopup( - walletId: args.item1, - words: args.item2, - ), - RouteSettings( - name: settings.name, - ), + DeleteWalletKeysPopup(walletId: args.item1, words: args.item2), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2672,12 +2485,8 @@ class RouteGenerator { case QRCodeDesktopPopupContent.routeName: if (args is String) { return FadePageRoute( - QRCodeDesktopPopupContent( - value: args, - ), - RouteSettings( - name: settings.name, - ), + QRCodeDesktopPopupContent(value: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2695,12 +2504,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MyTokensView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => MyTokensView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2723,23 +2528,18 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => TokenView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is ({String walletId, bool popPrevious})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenView( - walletId: args.walletId, - popPrevious: args.popPrevious, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenView( + walletId: args.walletId, + popPrevious: args.popPrevious, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2785,13 +2585,12 @@ class RouteGenerator { final end = Offset.zero; final curve = Curves.easeInOut; - final tween = - Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + final tween = Tween( + begin: begin, + end: end, + ).chain(CurveTween(curve: curve)); - return SlideTransition( - position: animation.drive(tween), - child: child, - ); + return SlideTransition(position: animation.drive(tween), child: child); }, ); } @@ -2835,10 +2634,7 @@ class FadePageRoute extends PageRoute { Animation animation, Animation secondaryAnimation, ) { - return FadeTransition( - opacity: animation, - child: child, - ); + return FadeTransition(opacity: animation, child: child); } @override diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index fccce0ceb..fd644944a 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -10,18 +10,13 @@ import 'dart:convert'; -import 'package:tuple/tuple.dart'; - import '../../dto/ethereum/eth_token_tx_dto.dart'; -import '../../dto/ethereum/eth_token_tx_extra_dto.dart'; import '../../dto/ethereum/eth_tx_dto.dart'; -import '../../dto/ethereum/pending_eth_tx_dto.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../models/paymint/fee_object_model.dart'; import '../../networking/http.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/eth_commons.dart'; -import '../../utilities/extensions/extensions.dart'; import '../../utilities/logger.dart'; import '../../utilities/prefs.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; @@ -48,7 +43,7 @@ class EthereumResponse { abstract class EthereumAPI { static String get stackBaseServer => - Ethereum(CryptoCurrencyNetwork.main).defaultNode.host; + Ethereum(CryptoCurrencyNetwork.main).defaultNode(isPrimary: true).host; static HTTP client = HTTP(); @@ -60,11 +55,12 @@ abstract class EthereumAPI { try { final response = await client.get( url: Uri.parse( - "$stackBaseServer/export?addrs=$address&firstBlock=$firstBlock", + "$stackBaseServer/export?addrs=$address&firstBlock=$firstBlock&unripe=true", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -80,17 +76,11 @@ abstract class EthereumAPI { txns.add(txn); } } - return EthereumResponse( - txns, - null, - ); + return EthereumResponse(txns, null); } else { // nice that the api returns an empty body instead of being // consistent and returning a json object with no transactions - return EthereumResponse( - [], - null, - ); + return EthereumResponse([], null); } } else { throw EthApiException( @@ -99,10 +89,7 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { Logging.instance.e("getEthTransactions()", error: e); Logging.instance.d( @@ -110,212 +97,7 @@ abstract class EthereumAPI { error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } - - static Future> getEthTransactionByHash( - String txid, - ) async { - try { - final response = await client.post( - url: Uri.parse( - "$stackBaseServer/v1/mainnet", - ), - headers: {'Content-Type': 'application/json'}, - body: json.encode({ - "jsonrpc": "2.0", - "method": "eth_getTransactionByHash", - "params": [ - txid, - ], - "id": DateTime.now().millisecondsSinceEpoch, - }), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - if (response.code == 200) { - if (response.body.isNotEmpty) { - try { - final json = jsonDecode(response.body) as Map; - final result = json["result"] as Map; - return EthereumResponse( - PendingEthTxDto.fromMap(Map.from(result)), - null, - ); - } catch (_) { - throw EthApiException( - "getEthTransactionByHash($txid) failed with response: " - "${response.body}", - ); - } - } else { - throw EthApiException( - "getEthTransactionByHash($txid) response is empty but status code is " - "${response.code}", - ); - } - } else { - throw EthApiException( - "getEthTransactionByHash($txid) failed with status code: " - "${response.code}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.e( - "getEthTransactionByHash()", - error: e, - ); - Logging.instance.d( - "getEthTransactionByHash($txid)", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } - - static Future>>> - getEthTransactionNonces( - List txns, - ) async { - try { - final response = await client.get( - url: Uri.parse( - "$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}&raw=true", - ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - if (response.code == 200) { - if (response.body.isNotEmpty) { - final json = jsonDecode(response.body) as Map; - final list = List>.from(json["data"] as List); - - final List> result = []; - - for (final dto in txns) { - final data = - list.firstWhere((e) => e["hash"] == dto.hash, orElse: () => {}); - - final nonce = (data["nonce"] as String?)?.toBigIntFromHex.toInt(); - result.add(Tuple2(dto, nonce)); - } - return EthereumResponse( - result, - null, - ); - } else { - // nice that the api returns an empty body instead of being - // consistent and returning a json object with no transactions - return EthereumResponse( - [], - null, - ); - } - } else { - throw EthApiException( - "getEthTransactionNonces($txns) failed with status code: " - "${response.code}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.e( - "getEthTransactionNonces()", - error: e, - ); - Logging.instance.d( - "getEthTransactionNonces($txns)", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } - - static Future>> - getEthTokenTransactionsByTxids(List txids) async { - try { - final response = await client.get( - url: Uri.parse( - "$stackBaseServer/transactions?transactions=${txids.join(" ")}", - ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - if (response.code == 200) { - if (response.body.isNotEmpty) { - final json = jsonDecode(response.body) as Map; - final list = json["data"] as List?; - - final List txns = []; - for (final map in list!) { - final txn = EthTokenTxExtraDTO.fromMap( - Map.from(map as Map), - ); - - txns.add(txn); - } - return EthereumResponse( - txns, - null, - ); - } else { - throw EthApiException( - "getEthTokenTransactionsByTxids($txids) response is empty but status code is " - "${response.code}", - ); - } - } else { - throw EthApiException( - "getEthTokenTransactionsByTxids($txids) failed with status code: " - "${response.code}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.e( - "getEthTokenTransactionsByTxids()", - error: e, - ); - Logging.instance.d( - "getEthTokenTransactionsByTxids($txids)", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } @@ -328,9 +110,10 @@ abstract class EthereumAPI { url: Uri.parse( "$stackBaseServer/export?addrs=$address&emitter=$tokenContractAddress&logs=true", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -340,22 +123,17 @@ abstract class EthereumAPI { final List txns = []; for (final map in list!) { - final txn = - EthTokenTxDto.fromMap(Map.from(map as Map)); + final txn = EthTokenTxDto.fromMap( + Map.from(map as Map), + ); txns.add(txn); } - return EthereumResponse( - txns, - null, - ); + return EthereumResponse(txns, null); } else { // nice that the api returns an empty body instead of being // consistent and returning a json object with no transactions - return EthereumResponse( - [], - null, - ); + return EthereumResponse([], null); } } else { throw EthApiException( @@ -364,100 +142,18 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { - Logging.instance.e( - "getTokenTransactions()", - error: e, - ); + Logging.instance.e("getTokenTransactions()", error: e); Logging.instance.d( "getTokenTransactions($address, $tokenContractAddress)", error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } -// ONLY FETCHES WALLET TOKENS WITH A NON ZERO BALANCE - // static Future>> getWalletTokens({ - // required String address, - // }) async { - // try { - // final uri = Uri.parse( - // "$blockExplorer?module=account&action=tokenlist&address=$address", - // ); - // final response = await get(uri); - // - // if (response.statusCode == 200) { - // final json = jsonDecode(response.body); - // if (json["message"] == "OK") { - // final result = - // List>.from(json["result"] as List); - // final List tokens = []; - // for (final map in result) { - // if (map["type"] == "ERC-20") { - // tokens.add( - // Erc20Token( - // balance: int.parse(map["balance"] as String), - // contractAddress: map["contractAddress"] as String, - // decimals: int.parse(map["decimals"] as String), - // name: map["name"] as String, - // symbol: map["symbol"] as String, - // ), - // ); - // } else if (map["type"] == "ERC-721") { - // tokens.add( - // Erc721Token( - // balance: int.parse(map["balance"] as String), - // contractAddress: map["contractAddress"] as String, - // decimals: int.parse(map["decimals"] as String), - // name: map["name"] as String, - // symbol: map["symbol"] as String, - // ), - // ); - // } else { - // throw EthApiException( - // "Unsupported token type found: ${map["type"]}"); - // } - // } - // - // return EthereumResponse( - // tokens, - // null, - // ); - // } else { - // throw EthApiException(json["message"] as String); - // } - // } else { - // throw EthApiException( - // "getWalletTokens($address) failed with status code: " - // "${response.statusCode}", - // ); - // } - // } on EthApiException catch (e) { - // return EthereumResponse( - // null, - // e, - // ); - // } catch (e, s) { - // Logging.instance.log( - // "getWalletTokens(): $e\n$s", - // level: LogLevel.Error, - // ); - // return EthereumResponse( - // null, - // EthApiException(e.toString()), - // ); - // } - // } - static Future> getWalletTokenBalance({ required String address, required String contractAddress, @@ -468,9 +164,10 @@ abstract class EthereumAPI { ); final response = await client.get( url: uri, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -479,7 +176,7 @@ abstract class EthereumAPI { final map = json["data"].first as Map; final balance = - BigInt.tryParse(map["units"].toString()) ?? BigInt.zero; + BigInt.tryParse(map["balance"].toString()) ?? BigInt.zero; return EthereumResponse( Amount(rawValue: balance, fractionDigits: map["decimals"] as int), @@ -495,84 +192,21 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.e( - "getWalletTokenBalance()", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } - - static Future> getAddressNonce({ - required String address, - }) async { - try { - final uri = Uri.parse( - "$stackBaseServer/state?addrs=$address&parts=all", - ); - final response = await client.get( - url: uri, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - if (response.code == 200) { - final json = jsonDecode(response.body); - if (json["data"] is List) { - final map = json["data"].first as Map; - - final nonce = map["nonce"] as int; - - return EthereumResponse( - nonce, - null, - ); - } else { - throw EthApiException(json["message"] as String); - } - } else { - throw EthApiException( - "getAddressNonce($address) failed with status code: " - "${response.code}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { - Logging.instance.e( - "getAddressNonce()", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + Logging.instance.e("getWalletTokenBalance()", error: e, stackTrace: s); + return EthereumResponse(null, EthApiException(e.toString())); } } static Future> getGasOracle() async { try { final response = await client.get( - url: Uri.parse( - "$stackBaseServer/gas-prices", - ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + url: Uri.parse("$stackBaseServer/gas-prices"), + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -604,36 +238,27 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { - Logging.instance.e( - "getGasOracle()", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + Logging.instance.e("getGasOracle()", error: e, stackTrace: s); + return EthereumResponse(null, EthApiException(e.toString())); } } - static Future getFees() async { - final fees = (await getGasOracle()).value!; - final feesFast = fees.fast.shift(9).toBigInt(); - final feesStandard = fees.average.shift(9).toBigInt(); - final feesSlow = fees.slow.shift(9).toBigInt(); - - return FeeObject( - numberOfBlocksFast: fees.numberOfBlocksFast, - numberOfBlocksAverage: fees.numberOfBlocksAverage, - numberOfBlocksSlow: fees.numberOfBlocksSlow, - fast: feesFast.toInt(), - medium: feesStandard.toInt(), - slow: feesSlow.toInt(), + static Future getFees() async { + final response = await getGasOracle(); + if (response.exception != null) { + throw response.exception!; + } + + return EthFeeObject( + suggestBaseFee: response.value!.suggestBaseFee.shift(9).toBigInt(), + numberOfBlocksFast: response.value!.numberOfBlocksFast, + numberOfBlocksAverage: response.value!.numberOfBlocksAverage, + numberOfBlocksSlow: response.value!.numberOfBlocksSlow, + fast: response.value!.high.shift(9).toBigInt(), + medium: response.value!.average.shift(9).toBigInt(), + slow: response.value!.low.shift(9).toBigInt(), ); } @@ -642,9 +267,10 @@ abstract class EthereumAPI { url: Uri.parse( "$stackBaseServer/names?terms=$contractAddress&autoname=$contractAddress&all", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); } @@ -658,9 +284,10 @@ abstract class EthereumAPI { // "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", "$stackBaseServer/names?terms=$contractAddress&all", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -713,10 +340,7 @@ abstract class EthereumAPI { ); } - return EthereumResponse( - token, - null, - ); + return EthereumResponse(token, null); } else { throw EthApiException(response.body); } @@ -727,20 +351,14 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { Logging.instance.e( "getTokenByContractAddress()", error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } @@ -753,18 +371,16 @@ abstract class EthereumAPI { url: Uri.parse( "$stackBaseServer/abis?addrs=$contractAddress&verbose=true", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { final json = jsonDecode(response.body)["data"] as List; - return EthereumResponse( - jsonEncode(json), - null, - ); + return EthereumResponse(jsonEncode(json), null); } else { throw EthApiException( "getTokenAbi($name, $contractAddress) failed with status code: " @@ -772,20 +388,14 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { Logging.instance.e( "getTokenAbi($name, $contractAddress)", error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } @@ -798,19 +408,17 @@ abstract class EthereumAPI { url: Uri.parse( "$stackBaseServer/state?addrs=$contractAddress&parts=proxy", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { final json = jsonDecode(response.body); final list = json["data"] as List; final map = Map.from(list.first as Map); - return EthereumResponse( - map["proxy"] as String, - null, - ); + return EthereumResponse(map["proxy"] as String, null); } else { throw EthApiException( "getProxyTokenImplementationAddress($contractAddress) failed with" @@ -818,20 +426,14 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { Logging.instance.e( "getProxyTokenImplementationAddress($contractAddress)", error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } } diff --git a/lib/services/exchange/change_now/change_now_api.dart b/lib/services/exchange/change_now/change_now_api.dart index d7cf255d5..7b5cd0737 100644 --- a/lib/services/exchange/change_now/change_now_api.dart +++ b/lib/services/exchange/change_now/change_now_api.dart @@ -12,18 +12,14 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; -import 'package:tuple/tuple.dart'; import '../../../exceptions/exchange/exchange_exception.dart'; import '../../../exceptions/exchange/pair_unavailable_exception.dart'; -import '../../../exceptions/exchange/unsupported_currency_exception.dart'; import '../../../external_api_keys.dart'; -import '../../../models/exchange/change_now/cn_exchange_estimate.dart'; +import '../../../models/exchange/change_now/cn_exchange_transaction.dart'; +import '../../../models/exchange/change_now/cn_exchange_transaction_status.dart'; import '../../../models/exchange/change_now/estimated_exchange_amount.dart'; -import '../../../models/exchange/change_now/exchange_transaction.dart'; -import '../../../models/exchange/change_now/exchange_transaction_status.dart'; import '../../../models/exchange/response_objects/estimate.dart'; -import '../../../models/exchange/response_objects/fixed_rate_market.dart'; import '../../../models/exchange/response_objects/range.dart'; import '../../../models/isar/exchange_cache/currency.dart'; import '../../../models/isar/exchange_cache/pair.dart'; @@ -34,10 +30,25 @@ import '../../tor_service.dart'; import '../exchange_response.dart'; import 'change_now_exchange.dart'; +enum CNFlow { + standard("standard"), + fixedRate("fixed-rate"); + + const CNFlow(this.value); + final String value; +} + +enum CNExchangeType { + direct("direct"), + reverse("reverse"); + + const CNExchangeType(this.value); + final String value; +} + class ChangeNowAPI { static const String scheme = "https"; static const String authority = "api.changenow.io"; - static const String apiVersion = "/v1"; static const String apiVersionV2 = "/v2"; final HTTP client; @@ -48,56 +59,24 @@ class ChangeNowAPI { static final ChangeNowAPI _instance = ChangeNowAPI(); static ChangeNowAPI get instance => _instance; - Uri _buildUri(String path, Map? params) { - return Uri.https(authority, apiVersion + path, params); - } - Uri _buildUriV2(String path, Map? params) { return Uri.https(authority, apiVersionV2 + path, params); } - Future _makeGetRequest(Uri uri) async { - try { - final response = await client.get( - url: uri, - headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - String? data; - try { - data = response.body; - final parsed = jsonDecode(data); - - return parsed; - } on FormatException catch (e) { - return { - "error": "Dart format exception", - "message": data, - }; - } - } catch (e, s) { - Logging.instance.e( - "_makeRequest($uri) threw", - error: e, - stackTrace: s, - ); - rethrow; - } - } - Future _makeGetRequestV2(Uri uri, String apiKey) async { + Logging.instance.t("ChangeNOW _makeGetRequestV2 to $uri"); + try { final response = await client.get( url: uri, headers: { - // 'Content-Type': 'application/json', - 'x-changenow-api-key': apiKey, + "Content-Type": "application/json", + "x-changenow-api-key": apiKey, }, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final data = response.body; @@ -105,27 +84,29 @@ class ChangeNowAPI { return parsed; } catch (e, s) { - Logging.instance.e( - "_makeRequestV2($uri) threw", - error: e, - stackTrace: s, - ); + Logging.instance.e("_makeRequestV2($uri) threw", error: e, stackTrace: s); rethrow; } } - Future _makePostRequest( + Future _makePostRequestV2( Uri uri, Map body, + String apiKey, ) async { + Logging.instance.t("ChangeNOW _makePostRequestV2 to $uri"); try { final response = await client.post( url: uri, - headers: {'Content-Type': 'application/json'}, + headers: { + "Content-Type": "application/json", + "x-changenow-api-key": apiKey, + }, body: jsonEncode(body), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); String? data; @@ -144,7 +125,7 @@ class ChangeNowAPI { } } catch (e, s) { Logging.instance.e( - "_makePostRequest($uri) threw", + "_makePostRequestV2($uri) threw", error: e, stackTrace: s, ); @@ -152,129 +133,41 @@ class ChangeNowAPI { } } - /// This API endpoint returns the list of available currencies. + /// Retrieves the list of available currencies from the API. /// - /// Set [active] to true to return only active currencies. - /// Set [fixedRate] to true to return only currencies available on a fixed-rate flow. + /// - Set [active] to `true` to return only active currencies. + /// - Set [buy] to `true` to return only currencies available for buying. + /// - Set [sell] to `true` to return only currencies available for selling. + /// - Set [flow] to specify the type of exchange flow. + /// Options are [CNFlow.standard] (default) or [CNFlow.fixedRate]. Future>> getAvailableCurrencies({ - bool? fixedRate, bool? active, + bool? buy, + bool? sell, + CNFlow flow = CNFlow.standard, + String? apiKey, }) async { - Map? params; - - if (active != null || fixedRate != null) { - params = {}; - if (fixedRate != null) { - params.addAll({"fixedRate": fixedRate.toString()}); - } - if (active != null) { - params.addAll({"active": active.toString()}); - } - } - - final uri = _buildUri("/currencies", params); - - try { - // json array is expected here - final jsonArray = await _makeGetRequest(uri); - - try { - final result = await compute( - _parseAvailableCurrenciesJson, - Tuple2(jsonArray as List, fixedRate == true), - ); - return result; - } catch (e, s) { - Logging.instance.e( - "getAvailableCurrencies exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - "Error: $jsonArray", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "getAvailableCurrencies exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - ExchangeResponse> _parseAvailableCurrenciesJson( - Tuple2, bool> args, - ) { - try { - final List currencies = []; - - for (final json in args.item1) { - try { - final map = Map.from(json as Map); - currencies.add( - Currency.fromJson( - map, - rateType: (map["supportsFixedRate"] as bool) - ? SupportedRateType.both - : SupportedRateType.estimated, - exchangeName: ChangeNowExchange.exchangeName, - ), - ); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } - - return ExchangeResponse(value: currencies); - } catch (_) { - rethrow; - } - } - - Future>> getCurrenciesV2( - // { - // bool? fixedRate, - // bool? active, - // } - ) async { - Map? params; - - // if (active != null || fixedRate != null) { - // params = {}; - // if (fixedRate != null) { - // params.addAll({"fixedRate": fixedRate.toString()}); - // } - // if (active != null) { - // params.addAll({"active": active.toString()}); - // } - // } + final params = { + "flow": flow.value, + if (active != null) "active": active.toString(), + if (buy != null) "buy": buy.toString(), + if (sell != null) "sell": sell.toString(), + }; final uri = _buildUriV2("/exchange/currencies", params); try { // json array is expected here - final jsonArray = await _makeGetRequest(uri); + final jsonArray = await _makeGetRequestV2( + uri, + apiKey ?? kChangeNowApiKey, + ); try { - final result = await compute( - _parseV2CurrenciesJson, - jsonArray as List, - ); + final result = await compute(_parseAvailableCurrenciesJson, ( + jsonList: jsonArray as List, + fixedRateFlow: flow == CNFlow.fixedRate, + )); return result; } catch (e, s) { Logging.instance.e( @@ -304,21 +197,22 @@ class ChangeNowAPI { } } - ExchangeResponse> _parseV2CurrenciesJson( - List args, + ExchangeResponse> _parseAvailableCurrenciesJson( + ({List jsonList, bool fixedRateFlow}) args, ) { try { final List currencies = []; - for (final json in args) { + for (final json in args.jsonList) { try { final map = Map.from(json as Map); currencies.add( Currency.fromJson( map, - rateType: (map["supportsFixedRate"] as bool) - ? SupportedRateType.both - : SupportedRateType.estimated, + rateType: + (map["supportsFixedRate"] as bool) + ? SupportedRateType.both + : SupportedRateType.estimated, exchangeName: ChangeNowExchange.exchangeName, ), ); @@ -338,110 +232,37 @@ class ChangeNowAPI { } } - /// This API endpoint returns the array of markets available for the specified currency be default. - /// The availability of a particular pair is determined by the 'isAvailable' field. + /// Retrieves the minimum amount required to exchange [fromCurrency] to [toCurrency]. /// - /// Required [ticker] to fetch paired currencies for. - /// Set [fixedRate] to true to return only currencies available on a fixed-rate flow. - Future>> getPairedCurrencies({ - required String ticker, - bool? fixedRate, - }) async { - Map? params; - - if (fixedRate != null) { - params = {}; - params.addAll({"fixedRate": fixedRate.toString()}); - } - - final uri = _buildUri("/currencies-to/$ticker", params); - - try { - // json array is expected here - - final response = await _makeGetRequest(uri); - - if (response is Map && response["error"] != null) { - return ExchangeResponse( - exception: UnsupportedCurrencyException( - response["message"] as String? ?? response["error"].toString(), - ExchangeExceptionType.generic, - ticker, - ), - ); - } - - final jsonArray = response as List; - - final List currencies = []; - try { - for (final json in jsonArray) { - try { - final map = Map.from(json as Map); - currencies.add( - Currency.fromJson( - map, - rateType: (map["supportsFixedRate"] as bool) - ? SupportedRateType.both - : SupportedRateType.estimated, - exchangeName: ChangeNowExchange.exchangeName, - ), - ); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } - } catch (e, s) { - Logging.instance.e( - "getPairedCurrencies exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - "Error: $jsonArray", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - return ExchangeResponse(value: currencies); - } catch (e, s) { - Logging.instance.e( - "getPairedCurrencies exception", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - /// The API endpoint returns minimal payment amount required to make - /// an exchange of [fromTicker] to [toTicker]. - /// If you try to exchange less, the transaction will most likely fail. + /// If you attempt to exchange less than this amount, the transaction may fail. + /// + /// - [fromCurrency]: Ticker of the currency you want to exchange (e.g., "btc"). + /// - [toCurrency]: Ticker of the currency you want to receive (e.g., "usdt"). + /// - [fromNetwork]: (Optional) Network of the currency you want to exchange (e.g., "btc"). + /// - [toNetwork]: (Optional) Network of the currency you want to receive (e.g., "eth"). + /// - [flow]: (Optional) Exchange flow type. Defaults to [CNFlow.standard]. + /// - [apiKey]: (Optional) API key if required. Future> getMinimalExchangeAmount({ - required String fromTicker, - required String toTicker, + required String fromCurrency, + required String toCurrency, + String? fromNetwork, + String? toNetwork, + CNFlow flow = CNFlow.standard, String? apiKey, }) async { - final Map params = { - "api_key": apiKey ?? kChangeNowApiKey, + final params = { + "fromCurrency": fromCurrency, + "toCurrency": toCurrency, + "flow": flow.value, + if (fromNetwork != null) "fromNetwork": fromNetwork, + if (toNetwork != null) "toNetwork": toNetwork, }; - final uri = _buildUri("/min-amount/${fromTicker}_$toTicker", params); + final uri = _buildUriV2("/exchange/min-amount", params); try { // simple json object is expected here - final json = await _makeGetRequest(uri); + final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey); try { final value = Decimal.parse(json["minAmount"].toString()); @@ -469,27 +290,40 @@ class ChangeNowAPI { } } - /// The API endpoint returns minimal payment amount and maximum payment amount - /// required to make an exchange. If you try to exchange less than minimum or - /// more than maximum, the transaction will most likely fail. Any pair of - /// assets has minimum amount and some of pairs have maximum amount. + /// Retrieves the minimum and maximum exchangeable amounts for a given currency pair. + /// + /// Attempting to exchange less than the minimum or more than the maximum may result in a failed transaction. + /// Every asset pair has a minimum exchange amount, and some also have a maximum. + /// + /// - [fromCurrency]: Ticker of the currency you want to exchange (e.g., "btc"). + /// - [toCurrency]: Ticker of the currency you want to receive (e.g., "eth"). + /// - [fromNetwork]: (Optional) Network of the currency you want to exchange (e.g., "btc"). + /// - [toNetwork]: (Optional) Network of the currency you want to receive (e.g., "eth"). + /// - [flow]: (Optional) Type of exchange flow. Defaults to [CNFlow.standard]. + /// - [apiKey]: (Optional) API key if required. Future> getRange({ - required String fromTicker, - required String toTicker, - required bool isFixedRate, + required String fromCurrency, + required String toCurrency, + String? fromNetwork, + String? toNetwork, + CNFlow flow = CNFlow.standard, String? apiKey, }) async { - final Map params = { - "api_key": apiKey ?? kChangeNowApiKey, + final params = { + "fromCurrency": fromCurrency, + "toCurrency": toCurrency, + "flow": flow.value, + if (fromNetwork != null) "fromNetwork": fromNetwork, + if (toNetwork != null) "toNetwork": toNetwork, }; - final uri = _buildUri( - "/exchange-range${isFixedRate ? "/fixed-rate" : ""}/${fromTicker}_$toTicker", - params, - ); + final uri = _buildUriV2("/exchange/range", params); try { - final jsonObject = await _makeGetRequest(uri); + final jsonObject = await _makeGetRequestV2( + uri, + apiKey ?? kChangeNowApiKey, + ); final json = Map.from(jsonObject as Map); return ExchangeResponse( @@ -499,11 +333,7 @@ class ChangeNowAPI { ), ); } catch (e, s) { - Logging.instance.e( - "getRange exception: ", - error: e, - stackTrace: s, - ); + Logging.instance.f("getRange exception: ", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -513,24 +343,50 @@ class ChangeNowAPI { } } - /// Get estimated amount of [toTicker] cryptocurrency to receive - /// for [fromAmount] of [fromTicker] + /// Retrieves an estimated amount of [toCurrency] you would receive for a given input amount of [fromCurrency]. + /// + /// - [fromCurrency]: Ticker of the currency you want to exchange (e.g., "btc"). + /// - [toCurrency]: Ticker of the currency you want to receive (e.g., "eth"). + /// - [fromAmount]: (Required if [type] is [CNExchangeType.direct]) Amount to exchange. Must be greater than 0. + /// - [toAmount]: (Required if [type] is [CNExchangeType.reverse]) Desired amount to receive. Must be greater than 0. + /// - [fromNetwork]: (Optional) Network of the currency you want to exchange (e.g., "btc"). + /// - [toNetwork]: (Optional) Network of the currency you want to receive (e.g., "eth"). + /// - [flow]: (Optional) Type of exchange flow. Defaults to [CNFlow.standard]. + /// - [type]: (Optional) Exchange direction. Either [CNExchangeType.direct] or [CNExchangeType.reverse]. Defaults to [CNExchangeType.direct]. + /// - [useRateId]: (Optional) For fixed-rate flow. When true, the response includes a [rateId] for locking the rate in the next request. + /// - [isTopUp]: (Optional) If true, gets an estimate for a balance top-up (no withdrawal fee). + /// - [apiKey]: (Optional) API key if required. Future> getEstimatedExchangeAmount({ - required String fromTicker, - required String toTicker, - required Decimal fromAmount, + required String fromCurrency, + required String toCurrency, + Decimal? fromAmount, + Decimal? toAmount, + String? fromNetwork, + String? toNetwork, + CNFlow flow = CNFlow.standard, + CNExchangeType type = CNExchangeType.direct, + bool? useRateId, + bool? isTopUp, String? apiKey, }) async { - final Map params = {"api_key": apiKey ?? kChangeNowApiKey}; + final params = { + "fromCurrency": fromCurrency, + "toCurrency": toCurrency, + if (fromAmount != null) "fromAmount": fromAmount.toString(), + if (toAmount != null) "toAmount": toAmount.toString(), + if (fromNetwork != null) "fromNetwork": fromNetwork, + if (toNetwork != null) "toNetwork": toNetwork, + "flow": flow.value, + "type": type.value, + if (useRateId != null) "useRateId": useRateId.toString(), + if (isTopUp != null) "isTopUp": isTopUp.toString(), + }; - final uri = _buildUri( - "/exchange-amount/${fromAmount.toString()}/${fromTicker}_$toTicker", - params, - ); + final uri = _buildUriV2("/exchange/estimated-amount", params); try { // simple json object is expected here - final json = await _makeGetRequest(uri); + final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey); try { final map = Map.from(json as Map); @@ -554,104 +410,19 @@ class ChangeNowAPI { } final value = EstimatedExchangeAmount.fromJson(map); + final reversed = value.type == CNExchangeType.reverse; return ExchangeResponse( value: Estimate( - estimatedAmount: value.estimatedAmount, - fixedRate: false, - reversed: false, - rateId: value.rateId, - warningMessage: value.warningMessage, - exchangeProvider: ChangeNowExchange.exchangeName, - ), - ); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "getEstimatedExchangeAmount exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - /// Get estimated amount of [toTicker] cryptocurrency to receive - /// for [fromAmount] of [fromTicker] - Future> getEstimatedExchangeAmountFixedRate({ - required String fromTicker, - required String toTicker, - required Decimal fromAmount, - required bool reversed, - bool useRateId = true, - String? apiKey, - }) async { - final Map params = { - "api_key": apiKey ?? kChangeNowApiKey, - "useRateId": useRateId.toString(), - }; - - late final Uri uri; - if (reversed) { - uri = _buildUri( - "/exchange-deposit/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker", - params, - ); - } else { - uri = _buildUri( - "/exchange-amount/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker", - params, - ); - } - - try { - // simple json object is expected here - final json = await _makeGetRequest(uri); - - try { - final map = Map.from(json as Map); - - if (map["error"] != null) { - if (map["error"] == "not_valid_fixed_rate_pair") { - return ExchangeResponse( - exception: PairUnavailableException( - map["message"] as String? ?? "Unsupported fixed rate pair", - ExchangeExceptionType.generic, - ), - ); - } else { - return ExchangeResponse( - exception: ExchangeException( - map["message"] as String? ?? map["error"].toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - final value = EstimatedExchangeAmount.fromJson(map); - return ExchangeResponse( - value: Estimate( - estimatedAmount: value.estimatedAmount, - fixedRate: true, + estimatedAmount: reversed ? value.fromAmount : value.toAmount, + fixedRate: value.flow == CNFlow.fixedRate, reversed: reversed, rateId: value.rateId, warningMessage: value.warningMessage, exchangeProvider: ChangeNowExchange.exchangeName, ), ); - } catch (_) { + } catch (e, s) { + Logging.instance.f(json, error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( "Failed to serialize $json", @@ -674,325 +445,100 @@ class ChangeNowAPI { } } - // old v1 version - /// This API endpoint returns fixed-rate estimated exchange amount of - /// [toTicker] cryptocurrency to receive for [fromAmount] of [fromTicker] - // Future> - // getEstimatedFixedRateExchangeAmount({ - // required String fromTicker, - // required String toTicker, - // required Decimal fromAmount, - // // (Optional) Use rateId for fixed-rate flow. If this field is true, you - // // could use returned field "rateId" in next method for creating transaction - // // to freeze estimated amount that you got in this method. Current estimated - // // amount would be valid until time in field "validUntil" - // bool useRateId = true, - // String? apiKey, - // }) async { - // Map params = { - // "api_key": apiKey ?? kChangeNowApiKey, - // "useRateId": useRateId.toString(), - // }; - // - // final uri = _buildUri( - // "/exchange-amount/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker", - // params, - // ); - // - // try { - // // simple json object is expected here - // final json = await _makeGetRequest(uri); - // - // try { - // final value = EstimatedExchangeAmount.fromJson( - // Map.from(json as Map)); - // return ExchangeResponse(value: value); - // } catch (_) { - // return ExchangeResponse( - // exception: ExchangeException( - // "Failed to serialize $json", - // ExchangeExceptionType.serializeResponseError, - // ), - // ); - // } - // } catch (e, s) { - // Logging.instance.log( - // "getEstimatedFixedRateExchangeAmount exception: $e\n$s", - // level: LogLevel.Error); - // return ExchangeResponse( - // exception: ExchangeException( - // e.toString(), - // ExchangeExceptionType.generic, - // ), - // ); - // } - // } - - /// Get estimated amount of [toTicker] cryptocurrency to receive - /// for [fromAmount] of [fromTicker] - Future> getEstimatedExchangeAmountV2({ - required String fromTicker, - required String toTicker, - required CNEstimateType fromOrTo, - required Decimal amount, - String? fromNetwork, - String? toNetwork, - CNFlowType flow = CNFlowType.standard, + /// Creates a new exchange transaction. + /// + /// This method initializes a currency exchange by specifying the source and destination + /// currencies, the exchange direction, recipient details, and optional metadata. + /// + /// If using a fixed-rate flow, you **must** provide a [rateId] obtained from a prior estimate + /// to lock in the rate. If using a standard flow, [rateId] can be left null. + /// + /// Parameters: + /// - [fromCurrency]: Ticker of the currency you want to exchange (e.g., "btc"). + /// - [fromNetwork]: Network of the currency you want to exchange (e.g., "btc"). + /// - [toCurrency]: Ticker of the currency you want to receive (e.g., "usdt"). + /// - [toNetwork]: Network of the currency you want to receive (e.g., "eth"). + /// - [fromAmount]: Amount of currency you want to exchange (used in "direct" flow). + /// - [toAmount]: Amount of currency you want to receive (used in "reverse" flow). + /// - [flow]: Type of exchange flow. Either [CNFlow.standard] or [CNFlow.fixedRate]. + /// - [type]: Direction of the exchange. Use [CNExchangeType.direct] to define the amount to send, + /// or [CNExchangeType.reverse] to define the amount to receive. + /// - [address]: Wallet address that will receive the exchanged funds. + /// - [extraId]: (Optional) Extra ID required by some currencies (e.g., memo, tag). + /// - [refundAddress]: (Optional) Address used to refund funds in case of timeout or failure. + /// - [refundExtraId]: (Optional) Extra ID for the refund address if required. + /// - [userId]: (Optional) Internal user identifier for partners with special access. + /// - [payload]: (Optional) Arbitrary string to store additional context for the transaction. + /// - [contactEmail]: (Optional) Email address to contact the user in case of issues. + /// - [rateId]: (Required for fixed-rate) The rate ID returned from the estimate step to freeze the exchange rate. + /// - [apiKey]: (Optional) Your API key, if authentication is required. + /// + /// Returns a [Future] resolving to [ExchangeResponse] containing the created [ExchangeTransaction]. + Future> createExchangeTransaction({ + required String fromCurrency, + required String fromNetwork, + required String toCurrency, + required String toNetwork, + Decimal? fromAmount, + Decimal? toAmount, + CNFlow flow = CNFlow.standard, + CNExchangeType type = CNExchangeType.direct, + required String address, + String? extraId, + String? refundAddress, + String? refundExtraId, + String? userId, + String? payload, + String? contactEmail, + required String? rateId, String? apiKey, }) async { - final Map params = { - "fromCurrency": fromTicker, - "toCurrency": toTicker, + final Map body = { + "fromCurrency": fromCurrency, + "fromNetwork": fromNetwork ?? "", + "toCurrency": toCurrency, + "toNetwork": toNetwork ?? "", + "fromAmount": fromAmount?.toString() ?? "", + "toAmount": toAmount?.toString() ?? "", "flow": flow.value, - "type": fromOrTo.name, + "type": type.value, + "address": address, + "extraId": extraId ?? "", + "refundAddress": refundAddress ?? "", + "refundExtraId": refundExtraId ?? "", + "userId": userId ?? "", + "payload": payload ?? "", + "contactEmail": contactEmail ?? "", + "rateId": rateId ?? "", }; - switch (fromOrTo) { - case CNEstimateType.direct: - params["fromAmount"] = amount.toString(); - break; - case CNEstimateType.reverse: - params["toAmount"] = amount.toString(); - break; - } - - if (fromNetwork != null) { - params["fromNetwork"] = fromNetwork; - } - - if (toNetwork != null) { - params["toNetwork"] = toNetwork; - } - - if (flow == CNFlowType.fixedRate) { - params["useRateId"] = "true"; - } - - final uri = _buildUriV2("/exchange/estimated-amount", params); + final uri = _buildUriV2("/exchange", null); try { // simple json object is expected here - final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey); - - try { - final value = - CNExchangeEstimate.fromJson(Map.from(json as Map)); - return ExchangeResponse(value: value); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "getEstimatedExchangeAmountV2 exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), + final json = await _makePostRequestV2( + uri, + body, + apiKey ?? kChangeNowApiKey, ); - } - } - - /// This API endpoint returns the list of all the pairs available on a - /// fixed-rate flow. Some currencies get enabled or disabled from time to - /// time and the market info gets updates, so make sure to refresh the list - /// occasionally. One time per minute is sufficient. - Future>> getAvailableFixedRateMarkets({ - String? apiKey, - }) async { - final uri = _buildUri( - "/market-info/fixed-rate/${apiKey ?? kChangeNowApiKey}", - null, - ); - try { - // json array is expected here - final jsonArray = await _makeGetRequest(uri); + json["date"] = DateTime.now().toIso8601String(); try { - final result = - await compute(_parseFixedRateMarketsJson, jsonArray as List); - return result; - } catch (e, s) { - Logging.instance.e( - "getAvailableFixedRateMarkets exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - "Error: $jsonArray", - ExchangeExceptionType.serializeResponseError, - ), + final value = CNExchangeTransaction.fromJson( + Map.from(json as Map), ); - } - } catch (e, s) { - Logging.instance.e( - "getAvailableFixedRateMarkets exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - ExchangeResponse> _parseFixedRateMarketsJson( - List jsonArray, - ) { - try { - final List markets = []; - for (final json in jsonArray) { - try { - markets.add( - FixedRateMarket.fromMap(Map.from(json as Map)), - ); - } catch (_) { + return ExchangeResponse(value: value); + } catch (e, s) { + if (json["error"] == "rate_id_not_found_or_expired") { return ExchangeResponse( exception: ExchangeException( - "Failed to serialize $json", + "Rate ID not found or expired", ExchangeExceptionType.serializeResponseError, ), ); } - } - return ExchangeResponse(value: markets); - } catch (_) { - rethrow; - } - } - - /// The API endpoint creates a transaction, generates an address for - /// sending funds and returns transaction attributes. - Future> - createStandardExchangeTransaction({ - required String fromTicker, - required String toTicker, - required String receivingAddress, - required Decimal amount, - String extraId = "", - String userId = "", - String contactEmail = "", - String refundAddress = "", - String refundExtraId = "", - String? apiKey, - }) async { - final Map map = { - "from": fromTicker, - "to": toTicker, - "address": receivingAddress, - "amount": amount.toString(), - "flow": "standard", - "extraId": extraId, - "userId": userId, - "contactEmail": contactEmail, - "refundAddress": refundAddress, - "refundExtraId": refundExtraId, - }; - - final uri = _buildUri("/transactions/${apiKey ?? kChangeNowApiKey}", null); - - try { - // simple json object is expected here - final json = await _makePostRequest(uri, map); - - // pass in date to prevent using default 1970 date - json["date"] = DateTime.now().toString(); - - try { - final value = ExchangeTransaction.fromJson( - Map.from(json as Map), - ); - return ExchangeResponse(value: value); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "createStandardExchangeTransaction exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - /// The API endpoint creates a transaction, generates an address for - /// sending funds and returns transaction attributes. - Future> - createFixedRateExchangeTransaction({ - required String fromTicker, - required String toTicker, - required String receivingAddress, - required Decimal amount, - required String rateId, - required bool reversed, - String extraId = "", - String userId = "", - String contactEmail = "", - String refundAddress = "", - String refundExtraId = "", - String? apiKey, - }) async { - final Map map = { - "from": fromTicker, - "to": toTicker, - "address": receivingAddress, - "flow": "fixed-rate", - "extraId": extraId, - "userId": userId, - "contactEmail": contactEmail, - "refundAddress": refundAddress, - "refundExtraId": refundExtraId, - "rateId": rateId, - }; - - if (reversed) { - map["result"] = amount.toString(); - } else { - map["amount"] = amount.toString(); - } - - final uri = _buildUri( - "/transactions/fixed-rate${reversed ? "/from-result" : ""}/${apiKey ?? kChangeNowApiKey}", - null, - ); - - try { - // simple json object is expected here - final json = await _makePostRequest(uri, map); - - // pass in date to prevent using default 1970 date - json["date"] = DateTime.now().toString(); - - try { - final value = ExchangeTransaction.fromJson( - Map.from(json as Map), - ); - return ExchangeResponse(value: value); - } catch (_) { + Logging.instance.f(json, error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( "Failed to serialize $json", @@ -1015,23 +561,23 @@ class ChangeNowAPI { } } - Future> getTransactionStatus({ + Future> getTransactionStatus({ required String id, String? apiKey, }) async { - final uri = - _buildUri("/transactions/$id/${apiKey ?? kChangeNowApiKey}", null); + final uri = _buildUriV2("/exchange/by-id", {"id": id}); try { // simple json object is expected here - final json = await _makeGetRequest(uri); + final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey); try { - final value = ExchangeTransactionStatus.fromJson( + final value = CNExchangeTransactionStatus.fromMap( Map.from(json as Map), ); return ExchangeResponse(value: value); - } catch (_) { + } catch (e, s) { + Logging.instance.f(json, error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( "Failed to serialize $json", @@ -1053,81 +599,4 @@ class ChangeNowAPI { ); } } - - Future>> getAvailableFloatingRatePairs({ - bool includePartners = false, - }) async { - final uri = _buildUri( - "/market-info/available-pairs", - {"includePartners": includePartners.toString()}, - ); - - try { - // json array is expected here - final jsonArray = await _makeGetRequest(uri); - - try { - final result = await compute( - _parseAvailableFloatingRatePairsJson, - jsonArray as List, - ); - return result; - } catch (e, s) { - Logging.instance.e( - "getAvailableFloatingRatePairs exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - "Error: $jsonArray", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "getAvailableFloatingRatePairs exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - ExchangeResponse> _parseAvailableFloatingRatePairsJson( - List jsonArray, - ) { - try { - final List pairs = []; - for (final json in jsonArray) { - try { - final List stringPair = (json as String).split("_"); - pairs.add( - Pair( - exchangeName: ChangeNowExchange.exchangeName, - from: stringPair[0], - to: stringPair[1], - rateType: SupportedRateType.estimated, - ), - ); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } - return ExchangeResponse(value: pairs); - } catch (_) { - rethrow; - } - } } diff --git a/lib/services/exchange/change_now/change_now_exchange.dart b/lib/services/exchange/change_now/change_now_exchange.dart index 35e8e30b2..48389afeb 100644 --- a/lib/services/exchange/change_now/change_now_exchange.dart +++ b/lib/services/exchange/change_now/change_now_exchange.dart @@ -9,16 +9,16 @@ */ import 'package:decimal/decimal.dart'; -import '../../../models/exchange/change_now/exchange_transaction.dart'; +import 'package:uuid/uuid.dart'; + import '../../../models/exchange/response_objects/estimate.dart'; import '../../../models/exchange/response_objects/range.dart'; import '../../../models/exchange/response_objects/trade.dart'; import '../../../models/isar/exchange_cache/currency.dart'; import '../../../models/isar/exchange_cache/pair.dart'; -import 'change_now_api.dart'; import '../exchange.dart'; import '../exchange_response.dart'; -import 'package:uuid/uuid.dart'; +import 'change_now_api.dart'; class ChangeNowExchange extends Exchange { ChangeNowExchange._(); @@ -35,6 +35,8 @@ class ChangeNowExchange extends Exchange { Future> createTrade({ required String from, required String to, + String? fromNetwork, + String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -44,45 +46,35 @@ class ChangeNowExchange extends Exchange { Estimate? estimate, required bool reversed, }) async { - late final ExchangeResponse response; - if (fixedRate) { - response = await ChangeNowAPI.instance.createFixedRateExchangeTransaction( - fromTicker: from, - toTicker: to, - receivingAddress: addressTo, - amount: amount, - rateId: estimate!.rateId!, - extraId: extraId ?? "", - refundAddress: addressRefund, - refundExtraId: refundExtraId, - reversed: reversed, - ); - } else { - response = await ChangeNowAPI.instance.createStandardExchangeTransaction( - fromTicker: from, - toTicker: to, - receivingAddress: addressTo, - amount: amount, - extraId: extraId ?? "", - refundAddress: addressRefund, - refundExtraId: refundExtraId, - ); - } + final response = await ChangeNowAPI.instance.createExchangeTransaction( + fromCurrency: from, + fromNetwork: fromNetwork ?? "", + toCurrency: to, + toNetwork: toNetwork ?? "", + address: addressTo, + rateId: estimate?.rateId, + refundAddress: addressRefund, + refundExtraId: refundExtraId, + fromAmount: reversed ? null : amount, + toAmount: reversed ? amount : null, + flow: fixedRate ? CNFlow.fixedRate : CNFlow.standard, + type: reversed ? CNExchangeType.reverse : CNExchangeType.direct, + ); + if (response.exception != null) { return ExchangeResponse(exception: response.exception); } - final statusResponse = await ChangeNowAPI.instance - .getTransactionStatus(id: response.value!.id); + final statusResponse = await ChangeNowAPI.instance.getTransactionStatus( + id: response.value!.id, + ); if (statusResponse.exception != null) { return ExchangeResponse(exception: statusResponse.exception); } return ExchangeResponse( - value: Trade.fromExchangeTransaction( - response.value!.copyWith( - statusObject: statusResponse.value!, - ), + value: Trade.fromCNExchangeTransaction( + response.value!.copyWithStatus(statusResponse.value!), reversed, ), ); @@ -92,75 +84,70 @@ class ChangeNowExchange extends Exchange { Future>> getAllCurrencies( bool fixedRate, ) async { - return await ChangeNowAPI.instance.getCurrenciesV2(); - // return await ChangeNowAPI.instance.getAvailableCurrencies( - // fixedRate: fixedRate ? true : null, - // active: true, - // ); - } - - @override - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ) async { - return await ChangeNowAPI.instance.getPairedCurrencies( - ticker: forCurrency, - fixedRate: fixedRate, - ); - } - - @override - Future>> getAllPairs(bool fixedRate) async { - if (fixedRate) { - final markets = - await ChangeNowAPI.instance.getAvailableFixedRateMarkets(); - - if (markets.value == null) { - return ExchangeResponse(exception: markets.exception); - } - - final List pairs = []; - for (final market in markets.value!) { - pairs.add( - Pair( - exchangeName: ChangeNowExchange.exchangeName, - from: market.from, - to: market.to, - rateType: SupportedRateType.fixed, - ), - ); - } - return ExchangeResponse(value: pairs); - } else { - return await ChangeNowAPI.instance.getAvailableFloatingRatePairs(); - } + return await ChangeNowAPI.instance.getAvailableCurrencies(); } + // + // @override + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ) async { + // return await ChangeNowAPI.instance.getPairedCurrencies( + // ticker: forCurrency, + // fixedRate: fixedRate, + // ); + // } + // + // @override + // Future>> getAllPairs(bool fixedRate) async { + // if (fixedRate) { + // final markets = + // await ChangeNowAPI.instance.getAvailableFixedRateMarkets(); + // + // if (markets.value == null) { + // return ExchangeResponse(exception: markets.exception); + // } + // + // final List pairs = []; + // for (final market in markets.value!) { + // pairs.add( + // Pair( + // exchangeName: ChangeNowExchange.exchangeName, + // from: market.from, + // to: market.to, + // rateType: SupportedRateType.fixed, + // ), + // ); + // } + // return ExchangeResponse(value: pairs); + // } else { + // return await ChangeNowAPI.instance.getAvailableFloatingRatePairs(); + // } + // } @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, ) async { - late final ExchangeResponse response; - if (fixedRate) { - response = - await ChangeNowAPI.instance.getEstimatedExchangeAmountFixedRate( - fromTicker: from, - toTicker: to, - fromAmount: amount, - reversed: reversed, - ); - } else { - response = await ChangeNowAPI.instance.getEstimatedExchangeAmount( - fromTicker: from, - toTicker: to, - fromAmount: amount, - ); - } + final response = await ChangeNowAPI.instance.getEstimatedExchangeAmount( + fromCurrency: from, + fromNetwork: fromNetwork, + toCurrency: to, + toNetwork: toNetwork, + flow: fixedRate ? CNFlow.fixedRate : CNFlow.standard, + type: reversed ? CNExchangeType.reverse : CNExchangeType.direct, + fromAmount: reversed ? null : amount, + toAmount: reversed ? amount : null, + + useRateId: fixedRate ? true : null, + ); + return ExchangeResponse( value: response.value == null ? null : [response.value!], exception: response.exception, @@ -170,13 +157,17 @@ class ChangeNowExchange extends Exchange { @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { return await ChangeNowAPI.instance.getRange( - fromTicker: from, - toTicker: to, - isFixedRate: fixedRate, + fromCurrency: from, + fromNetwork: fromNetwork, + toCurrency: to, + toNetwork: toNetwork, + flow: fixedRate ? CNFlow.fixedRate : CNFlow.standard, ); } @@ -191,8 +182,9 @@ class ChangeNowExchange extends Exchange { @override Future> getTrade(String tradeId) async { - final response = - await ChangeNowAPI.instance.getTransactionStatus(id: tradeId); + final response = await ChangeNowAPI.instance.getTransactionStatus( + id: tradeId, + ); if (response.exception != null) { return ExchangeResponse(exception: response.exception); } @@ -207,19 +199,19 @@ class ChangeNowExchange extends Exchange { timestamp: timestamp, updatedAt: DateTime.tryParse(t.updatedAt) ?? timestamp, payInCurrency: t.fromCurrency, - payInAmount: t.expectedSendAmountDecimal, + payInAmount: t.expectedAmountFrom ?? "", payInAddress: t.payinAddress, payInNetwork: "", - payInExtraId: t.payinExtraId, - payInTxid: t.payinHash, + payInExtraId: t.payinExtraId ?? "", + payInTxid: t.payinHash ?? "", payOutCurrency: t.toCurrency, - payOutAmount: t.expectedReceiveAmountDecimal, + payOutAmount: t.expectedAmountTo ?? "", payOutAddress: t.payoutAddress, payOutNetwork: "", - payOutExtraId: t.payoutExtraId, - payOutTxid: t.payoutHash, - refundAddress: t.refundAddress, - refundExtraId: t.refundExtraId, + payOutExtraId: t.payoutExtraId ?? "", + payOutTxid: t.payoutHash ?? "", + refundAddress: t.refundAddress ?? "", + refundExtraId: t.refundExtraId ?? "", status: t.status.name, exchangeName: ChangeNowExchange.exchangeName, ); @@ -229,8 +221,9 @@ class ChangeNowExchange extends Exchange { @override Future> updateTrade(Trade trade) async { - final response = - await ChangeNowAPI.instance.getTransactionStatus(id: trade.tradeId); + final response = await ChangeNowAPI.instance.getTransactionStatus( + id: trade.tradeId, + ); if (response.exception != null) { return ExchangeResponse(exception: response.exception); } @@ -245,23 +238,19 @@ class ChangeNowExchange extends Exchange { timestamp: timestamp, updatedAt: DateTime.tryParse(t.updatedAt) ?? timestamp, payInCurrency: t.fromCurrency, - payInAmount: t.amountSendDecimal.isEmpty - ? t.expectedSendAmountDecimal - : t.amountSendDecimal, + payInAmount: t.amountFrom ?? t.expectedAmountFrom ?? trade.payInAmount, payInAddress: t.payinAddress, payInNetwork: trade.payInNetwork, - payInExtraId: t.payinExtraId, - payInTxid: t.payinHash, + payInExtraId: t.payinExtraId ?? "", + payInTxid: t.payinHash ?? "", payOutCurrency: t.toCurrency, - payOutAmount: t.amountReceiveDecimal.isEmpty - ? t.expectedReceiveAmountDecimal - : t.amountReceiveDecimal, + payOutAmount: t.amountTo ?? t.expectedAmountTo ?? trade.payOutAmount, payOutAddress: t.payoutAddress, payOutNetwork: trade.payOutNetwork, - payOutExtraId: t.payoutExtraId, - payOutTxid: t.payoutHash, - refundAddress: t.refundAddress, - refundExtraId: t.refundExtraId, + payOutExtraId: t.payoutExtraId ?? "", + payOutTxid: t.payoutHash ?? "", + refundAddress: t.refundAddress ?? "", + refundExtraId: t.refundExtraId ?? "", status: t.status.name, exchangeName: ChangeNowExchange.exchangeName, ); diff --git a/lib/services/exchange/exchange.dart b/lib/services/exchange/exchange.dart index bf592fab3..05ca33e87 100644 --- a/lib/services/exchange/exchange.dart +++ b/lib/services/exchange/exchange.dart @@ -14,7 +14,6 @@ import '../../models/exchange/response_objects/estimate.dart'; import '../../models/exchange/response_objects/range.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../models/isar/exchange_cache/currency.dart'; -import '../../models/isar/exchange_cache/pair.dart'; import 'change_now/change_now_exchange.dart'; import 'exchange_response.dart'; import 'majestic_bank/majestic_bank_exchange.dart'; @@ -31,8 +30,8 @@ abstract class Exchange { return ChangeNowExchange.instance; case SimpleSwapExchange.exchangeName: return SimpleSwapExchange.instance; - case MajesticBankExchange.exchangeName: - return MajesticBankExchange.instance; + // case MajesticBankExchange.exchangeName: + // return MajesticBankExchange.instance; case TrocadorExchange.exchangeName: return TrocadorExchange.instance; case NanswapExchange.exchangeName: @@ -53,17 +52,17 @@ abstract class Exchange { Future>> getAllCurrencies(bool fixedRate); - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ); - - Future>> getPairsFor( - String currency, - bool fixedRate, - ); + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ); - Future>> getAllPairs(bool fixedRate); + // Future>> getPairsFor( + // String currency, + // bool fixedRate, + // ); + // + // Future>> getAllPairs(bool fixedRate); Future> getTrade(String tradeId); Future> updateTrade(Trade trade); @@ -72,13 +71,17 @@ abstract class Exchange { Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ); Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, @@ -87,6 +90,8 @@ abstract class Exchange { Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -101,10 +106,10 @@ abstract class Exchange { /// /// Add to this list when adding a new exchange which supports Tor. static List get exchangesWithTorSupport => [ - MajesticBankExchange.instance, - TrocadorExchange.instance, - NanswapExchange.instance, // Maybe?? - ]; + // MajesticBankExchange.instance, + TrocadorExchange.instance, + NanswapExchange.instance, // Maybe?? + ]; /// List of exchange names which support Tor. /// diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index 1cfaee23b..35a167795 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; @@ -23,7 +25,6 @@ import '../../utilities/logger.dart'; import '../../utilities/prefs.dart'; import '../../utilities/stack_file_system.dart'; import 'change_now/change_now_exchange.dart'; -import 'majestic_bank/majestic_bank_exchange.dart'; import 'nanswap/nanswap_exchange.dart'; import 'trocador/trocador_exchange.dart'; @@ -34,7 +35,10 @@ class ExchangeDataLoadingService { static ExchangeDataLoadingService get instance => _instance; Isar? _isar; - Isar get isar => _isar!; + Future get isar async { + if (_isar == null) await initDB(); + return _isar!; + } VoidCallback? onLoadingError; VoidCallback? onLoadingComplete; @@ -43,9 +47,10 @@ class ExchangeDataLoadingService { static int get currentCacheVersion => DB.instance.get( - boxName: DB.boxNameDBInfo, - key: "exchange_data_cache_version", - ) as int? ?? + boxName: DB.boxNameDBInfo, + key: "exchange_data_cache_version", + ) + as int? ?? 0; Future _updateCurrentCacheVersion(int version) async { @@ -56,9 +61,16 @@ class ExchangeDataLoadingService { ); } + Completer? _initCompleter; Future initDB() async { if (_isar != null) return; - await _isar?.close(); + + if (_initCompleter != null) { + return await _initCompleter!.future; + } + + _initCompleter = Completer(); + _isar = await Isar.open( [ CurrencySchema, @@ -70,6 +82,8 @@ class ExchangeDataLoadingService { name: "exchange_cache", maxSizeMiB: 64, ); + + _initCompleter!.complete(); } Future setCurrenciesIfEmpty( @@ -77,10 +91,11 @@ class ExchangeDataLoadingService { ExchangeRateType rateType, ) async { if (pair?.send == null && pair?.receive == null) { - if (await isar.currencies.count() > 0) { + if (await (await isar).currencies.count() > 0) { pair?.setSend( await getAggregateCurrency( AppConfig.swapDefaults.from, + AppConfig.swapDefaults.fromFuzzyNet, rateType, null, ), @@ -90,6 +105,7 @@ class ExchangeDataLoadingService { pair?.setReceive( await getAggregateCurrency( AppConfig.swapDefaults.to, + AppConfig.swapDefaults.toFuzzyNet, rateType, null, ), @@ -101,30 +117,55 @@ class ExchangeDataLoadingService { Future getAggregateCurrency( String ticker, + String fuzzyNet, ExchangeRateType rateType, String? contract, ) async { - final currencies = await ExchangeDataLoadingService.instance.isar.currencies - .filter() - .group( - (q) => rateType == ExchangeRateType.fixed - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .and() - .tickerEqualTo( - ticker, - caseSensitive: false, - ) - .and() - .tokenContractEqualTo(contract) - .findAll(); + final List currencies; + + if (contract != null) { + currencies = + await (await isar).currencies + .filter() + .tokenContractEqualTo(contract) + .and() + .group( + (q) => + rateType == ExchangeRateType.fixed + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .findAll(); + } else { + currencies = + await (await isar).currencies + .filter() + .group( + (q) => + rateType == ExchangeRateType.fixed + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .and() + .tickerEqualTo(ticker, caseSensitive: false) + .and() + .tokenContractIsNull() + .findAll(); + } + + currencies.retainWhere((e) => e.getFuzzyNet() == fuzzyNet); final items = currencies .map((e) => Tuple2(e.exchangeName, e)) @@ -145,9 +186,7 @@ class ExchangeDataLoadingService { if (_isar == null) { await initDB(); } - Logging.instance.d( - "ExchangeDataLoadingService.loadAll starting...", - ); + Logging.instance.d("ExchangeDataLoadingService.loadAll starting..."); final start = DateTime.now(); try { /* @@ -169,7 +208,7 @@ class ExchangeDataLoadingService { // Exchanges which support Tor just get treated normally. final futures = [ - loadMajesticBankCurrencies(), + // loadMajesticBankCurrencies(), loadTrocadorCurrencies(), loadNanswapCurrencies(), ]; @@ -208,14 +247,15 @@ class ExchangeDataLoadingService { final exchange = ChangeNowExchange.instance; final responseCurrencies = await exchange.getAllCurrencies(false); if (responseCurrencies.value != null) { - await isar.writeTxn(() async { - final idsToDelete = await isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .idProperty() - .findAll(); - await isar.currencies.deleteAll(idsToDelete); - await isar.currencies.putAll(responseCurrencies.value!); + await (await isar).writeTxn(() async { + final idsToDelete = + await (await isar).currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .idProperty() + .findAll(); + await (await isar).currencies.deleteAll(idsToDelete); + await (await isar).currencies.putAll(responseCurrencies.value!); }); } else { Logging.instance.w( @@ -333,29 +373,28 @@ class ExchangeDataLoadingService { // } // } - Future loadMajesticBankCurrencies() async { - if (_isar == null) { - await initDB(); - } - final exchange = MajesticBankExchange.instance; - final responseCurrencies = await exchange.getAllCurrencies(false); - - if (responseCurrencies.value != null) { - await isar.writeTxn(() async { - final idsToDelete = await isar.currencies - .where() - .exchangeNameEqualTo(MajesticBankExchange.exchangeName) - .idProperty() - .findAll(); - await isar.currencies.deleteAll(idsToDelete); - await isar.currencies.putAll(responseCurrencies.value!); - }); - } else { - Logging.instance.w( - "loadMajesticBankCurrencies: $responseCurrencies", - ); - } - } + // Future loadMajesticBankCurrencies() async { + // if (_isar == null) { + // await initDB(); + // } + // final exchange = MajesticBankExchange.instance; + // final responseCurrencies = await exchange.getAllCurrencies(false); + // + // if (responseCurrencies.value != null) { + // await isar.writeTxn(() async { + // final idsToDelete = + // await isar.currencies + // .where() + // .exchangeNameEqualTo(MajesticBankExchange.exchangeName) + // .idProperty() + // .findAll(); + // await isar.currencies.deleteAll(idsToDelete); + // await isar.currencies.putAll(responseCurrencies.value!); + // }); + // } else { + // Logging.instance.w("loadMajesticBankCurrencies: $responseCurrencies"); + // } + // } Future loadTrocadorCurrencies() async { if (_isar == null) { @@ -365,19 +404,18 @@ class ExchangeDataLoadingService { final responseCurrencies = await exchange.getAllCurrencies(false); if (responseCurrencies.value != null) { - await isar.writeTxn(() async { - final idsToDelete = await isar.currencies - .where() - .exchangeNameEqualTo(TrocadorExchange.exchangeName) - .idProperty() - .findAll(); - await isar.currencies.deleteAll(idsToDelete); - await isar.currencies.putAll(responseCurrencies.value!); + await (await isar).writeTxn(() async { + final idsToDelete = + await (await isar).currencies + .where() + .exchangeNameEqualTo(TrocadorExchange.exchangeName) + .idProperty() + .findAll(); + await (await isar).currencies.deleteAll(idsToDelete); + await (await isar).currencies.putAll(responseCurrencies.value!); }); } else { - Logging.instance.w( - "loadTrocadorCurrencies: $responseCurrencies", - ); + Logging.instance.w("loadTrocadorCurrencies: $responseCurrencies"); } } @@ -385,23 +423,23 @@ class ExchangeDataLoadingService { if (_isar == null) { await initDB(); } - final responseCurrencies = - await NanswapExchange.instance.getAllCurrencies(false); + final responseCurrencies = await NanswapExchange.instance.getAllCurrencies( + false, + ); if (responseCurrencies.value != null) { - await isar.writeTxn(() async { - final idsToDelete = await isar.currencies - .where() - .exchangeNameEqualTo(NanswapExchange.exchangeName) - .idProperty() - .findAll(); - await isar.currencies.deleteAll(idsToDelete); - await isar.currencies.putAll(responseCurrencies.value!); + await (await isar).writeTxn(() async { + final idsToDelete = + await (await isar).currencies + .where() + .exchangeNameEqualTo(NanswapExchange.exchangeName) + .idProperty() + .findAll(); + await (await isar).currencies.deleteAll(idsToDelete); + await (await isar).currencies.putAll(responseCurrencies.value!); }); } else { - Logging.instance.w( - "loadNanswapCurrencies: $responseCurrencies", - ); + Logging.instance.w("loadNanswapCurrencies: $responseCurrencies"); } } diff --git a/lib/services/exchange/majestic_bank/majestic_bank_api.dart b/lib/services/exchange/majestic_bank/majestic_bank_api.dart index 210be8af4..7d713d6d6 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_api.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_api.dart @@ -1,406 +1,385 @@ -/* - * 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:convert'; - -import 'package:decimal/decimal.dart'; - -import '../../../exceptions/exchange/exchange_exception.dart'; -import '../../../exceptions/exchange/majestic_bank/mb_exception.dart'; -import '../../../exceptions/exchange/pair_unavailable_exception.dart'; -import '../../../models/exchange/majestic_bank/mb_limit.dart'; -import '../../../models/exchange/majestic_bank/mb_order.dart'; -import '../../../models/exchange/majestic_bank/mb_order_calculation.dart'; -import '../../../models/exchange/majestic_bank/mb_order_status.dart'; -import '../../../models/exchange/majestic_bank/mb_rate.dart'; -import '../../../networking/http.dart'; -import '../../../utilities/logger.dart'; -import '../../../utilities/prefs.dart'; -import '../../tor_service.dart'; -import '../exchange_response.dart'; - -class MajesticBankAPI { - static const String scheme = "https"; - static const String authority = "majesticbank.sc"; - static const String version = "v1"; - static const kMajesticBankRefCode = "rjWugM"; - - MajesticBankAPI._(); - - static final MajesticBankAPI _instance = MajesticBankAPI._(); - - static MajesticBankAPI get instance => _instance; - - HTTP client = HTTP(); - - Uri _buildUri({required String endpoint, Map? params}) { - return Uri.https(authority, "/api/$version/$endpoint", params); - } - - Future _makeGetRequest(Uri uri) async { - // final client = this.client ?? http.Client(); - int code = -1; - try { - final response = await client.get( - url: uri, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - code = response.code; - - final parsed = jsonDecode(response.body); - - return parsed; - } catch (e, s) { - Logging.instance - .e("_makeRequest($uri) HTTP:$code threw: ", error: e, stackTrace: s); - rethrow; - } - } - - Future>> getRates() async { - final uri = _buildUri( - endpoint: "rates", - ); - - try { - final jsonObject = await _makeGetRequest(uri); - - final map = Map.from(jsonObject as Map); - final List rates = []; - for (final key in map.keys) { - final currencies = key.split("-"); - if (currencies.length == 2) { - final rate = MBRate( - fromCurrency: currencies.first, - toCurrency: currencies.last, - rate: Decimal.parse(map[key].toString()), - ); - rates.add(rate); - } - } - return ExchangeResponse(value: rates); - } catch (e, s) { - Logging.instance.e( - "getRates exception", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - Future> getLimit({ - required String fromCurrency, - }) async { - final uri = _buildUri( - endpoint: "limits", - params: { - "from_currency": fromCurrency, - }, - ); - - try { - final jsonObject = await _makeGetRequest(uri); - - final map = Map.from(jsonObject as Map); - - final limit = MBLimit( - currency: fromCurrency, - min: Decimal.parse(map["min"].toString()), - max: Decimal.parse(map["max"].toString()), - ); - - return ExchangeResponse(value: limit); - } catch (e, s) { - Logging.instance.e( - "getLimits exception", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - Future>> getLimits() async { - final uri = _buildUri( - endpoint: - "rates", // limits are included in the rates call for some reason??? - ); - - try { - final jsonObject = await _makeGetRequest(uri); - - final map = Map.from(jsonObject as Map)["limits"] as Map; - final List limits = []; - for (final key in map.keys) { - final limit = MBLimit( - currency: key as String, - min: Decimal.parse(map[key]["min"].toString()), - max: Decimal.parse(map[key]["max"].toString()), - ); - limits.add(limit); - } - - return ExchangeResponse(value: limits); - } catch (e, s) { - Logging.instance.e( - "getLimits exception", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - /// If [reversed] then the amount is the expected receive_amount, otherwise - /// the amount is assumed to be the from_amount. - Future> calculateOrder({ - required String amount, - required bool reversed, - required String fromCurrency, - required String receiveCurrency, - }) async { - final params = { - "from_currency": fromCurrency, - "receive_currency": receiveCurrency, - }; - - if (reversed) { - params["receive_amount"] = amount; - } else { - params["from_amount"] = amount; - } - - final uri = _buildUri( - endpoint: "calculate", - params: params, - ); - - try { - final jsonObject = await _makeGetRequest(uri); - final map = Map.from(jsonObject as Map); - - if (map["error"] != null) { - final errorMessage = map["extra"] as String?; - if (errorMessage != null && - errorMessage.startsWith("Bad") && - errorMessage.endsWith("currency symbol")) { - return ExchangeResponse( - exception: PairUnavailableException( - errorMessage, - ExchangeExceptionType.generic, - ), - ); - } else { - return ExchangeResponse( - exception: ExchangeException( - errorMessage ?? "Error: ${map["error"]}", - ExchangeExceptionType.generic, - ), - ); - } - } - - final result = MBOrderCalculation( - fromCurrency: map["from_currency"] as String, - fromAmount: Decimal.parse(map["from_amount"].toString()), - receiveCurrency: map["receive_currency"] as String, - receiveAmount: Decimal.parse(map["receive_amount"].toString()), - ); - - return ExchangeResponse(value: result); - } catch (e, s) { - Logging.instance.e( - "calculateOrder $fromCurrency-$receiveCurrency exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - Future> createOrder({ - required String fromAmount, - required String fromCurrency, - required String receiveCurrency, - required String receiveAddress, - }) async { - final params = { - "from_amount": fromAmount, - "from_currency": fromCurrency, - "receive_currency": receiveCurrency, - "receive_address": receiveAddress, - "referral_code": kMajesticBankRefCode, - }; - - final uri = _buildUri(endpoint: "exchange", params: params); - - try { - final now = DateTime.now(); - final jsonObject = await _makeGetRequest(uri); - final json = Map.from(jsonObject as Map); - - final order = MBOrder( - orderId: json["trx"] as String, - fromCurrency: json["from_currency"] as String, - fromAmount: Decimal.parse(json["from_amount"].toString()), - receiveCurrency: json["receive_currency"] as String, - receiveAmount: Decimal.parse(json["receive_amount"].toString()), - address: json["address"] as String, - orderType: MBOrderType.floating, - expiration: json["expiration"] as int, - createdAt: now, - ); - - return ExchangeResponse(value: order); - } catch (e, s) { - Logging.instance.e( - "createOrder exception", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - /// Fixed rate for 10 minutes, useful for payments. - /// If [reversed] then the amount is the expected receive_amount, otherwise - /// the amount is assumed to be the from_amount. - Future> createFixedRateOrder({ - required String amount, - required String fromCurrency, - required String receiveCurrency, - required String receiveAddress, - required bool reversed, - }) async { - final params = { - "from_currency": fromCurrency, - "receive_currency": receiveCurrency, - "receive_address": receiveAddress, - "referral_code": kMajesticBankRefCode, - }; - - if (reversed) { - params["receive_amount"] = amount; - } else { - params["from_amount"] = amount; - } - - final uri = _buildUri(endpoint: "pay", params: params); - - try { - final now = DateTime.now(); - final jsonObject = await _makeGetRequest(uri); - final json = Map.from(jsonObject as Map); - - final order = MBOrder( - orderId: json["trx"] as String, - fromCurrency: json["from_currency"] as String, - fromAmount: Decimal.parse(json["from_amount"].toString()), - receiveCurrency: json["receive_currency"] as String, - receiveAmount: Decimal.parse(json["receive_amount"].toString()), - address: json["address"] as String, - orderType: MBOrderType.fixed, - expiration: json["expiration"] as int, - createdAt: now, - ); - - return ExchangeResponse(value: order); - } catch (e, s) { - Logging.instance - .e("createFixedRateOrder exception: ", error: e, stackTrace: s); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - Future> trackOrder({ - required String orderId, - }) async { - final uri = _buildUri( - endpoint: "track", - params: { - "trx": orderId, - }, - ); - - try { - final jsonObject = await _makeGetRequest(uri); - final json = Map.from(jsonObject as Map); - - if (json.length == 2) { - return ExchangeResponse( - exception: MBException( - json["status"] as String, - ExchangeExceptionType.orderNotFound, - ), - ); - } - - final status = MBOrderStatus( - orderId: json["trx"] as String, - status: json["status"] as String, - fromCurrency: json["from_currency"] as String, - fromAmount: Decimal.parse(json["from_amount"].toString()), - receiveCurrency: json["receive_currency"] as String, - receiveAmount: Decimal.parse(json["receive_amount"].toString()), - address: json["address"] as String, - received: Decimal.parse(json["received"].toString()), - confirmed: Decimal.parse(json["confirmed"].toString()), - ); - - return ExchangeResponse(value: status); - } catch (e, s) { - Logging.instance.e( - "trackOrder exception when trying to parse $json: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } -} +// /* +// * 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:convert'; +// +// import 'package:decimal/decimal.dart'; +// +// import '../../../exceptions/exchange/exchange_exception.dart'; +// import '../../../exceptions/exchange/majestic_bank/mb_exception.dart'; +// import '../../../exceptions/exchange/pair_unavailable_exception.dart'; +// import '../../../models/exchange/majestic_bank/mb_limit.dart'; +// import '../../../models/exchange/majestic_bank/mb_order.dart'; +// import '../../../models/exchange/majestic_bank/mb_order_calculation.dart'; +// import '../../../models/exchange/majestic_bank/mb_order_status.dart'; +// import '../../../models/exchange/majestic_bank/mb_rate.dart'; +// import '../../../networking/http.dart'; +// import '../../../utilities/logger.dart'; +// import '../../../utilities/prefs.dart'; +// import '../../tor_service.dart'; +// import '../exchange_response.dart'; +// +// class MajesticBankAPI { +// static const String scheme = "https"; +// static const String authority = "majesticbank.sc"; +// static const String version = "v1"; +// static const kMajesticBankRefCode = "rjWugM"; +// +// MajesticBankAPI._(); +// +// static final MajesticBankAPI _instance = MajesticBankAPI._(); +// +// static MajesticBankAPI get instance => _instance; +// +// HTTP client = HTTP(); +// +// Uri _buildUri({required String endpoint, Map? params}) { +// return Uri.https(authority, "/api/$version/$endpoint", params); +// } +// +// Future _makeGetRequest(Uri uri) async { +// // final client = this.client ?? http.Client(); +// int code = -1; +// try { +// final response = await client.get( +// url: uri, +// proxyInfo: +// Prefs.instance.useTor +// ? TorService.sharedInstance.getProxyInfo() +// : null, +// ); +// +// code = response.code; +// +// final parsed = jsonDecode(response.body); +// +// return parsed; +// } catch (e, s) { +// Logging.instance.e( +// "_makeRequest($uri) HTTP:$code threw: ", +// error: e, +// stackTrace: s, +// ); +// rethrow; +// } +// } +// +// Future>> getRates() async { +// final uri = _buildUri(endpoint: "rates"); +// +// try { +// final jsonObject = await _makeGetRequest(uri); +// +// final map = Map.from(jsonObject as Map); +// final List rates = []; +// for (final key in map.keys) { +// final currencies = key.split("-"); +// if (currencies.length == 2) { +// final rate = MBRate( +// fromCurrency: currencies.first, +// toCurrency: currencies.last, +// rate: Decimal.parse(map[key].toString()), +// ); +// rates.add(rate); +// } +// } +// return ExchangeResponse(value: rates); +// } catch (e, s) { +// Logging.instance.e("getRates exception", error: e, stackTrace: s); +// return ExchangeResponse( +// exception: ExchangeException( +// e.toString(), +// ExchangeExceptionType.generic, +// ), +// ); +// } +// } +// +// Future> getLimit({ +// required String fromCurrency, +// }) async { +// final uri = _buildUri( +// endpoint: "limits", +// params: {"from_currency": fromCurrency}, +// ); +// +// try { +// final jsonObject = await _makeGetRequest(uri); +// +// final map = Map.from(jsonObject as Map); +// +// final limit = MBLimit( +// currency: fromCurrency, +// min: Decimal.parse(map["min"].toString()), +// max: Decimal.parse(map["max"].toString()), +// ); +// +// return ExchangeResponse(value: limit); +// } catch (e, s) { +// Logging.instance.e("getLimits exception", error: e, stackTrace: s); +// return ExchangeResponse( +// exception: ExchangeException( +// e.toString(), +// ExchangeExceptionType.generic, +// ), +// ); +// } +// } +// +// Future>> getLimits() async { +// final uri = _buildUri( +// endpoint: +// "rates", // limits are included in the rates call for some reason??? +// ); +// +// try { +// final jsonObject = await _makeGetRequest(uri); +// +// final map = Map.from(jsonObject as Map)["limits"] as Map; +// final List limits = []; +// for (final key in map.keys) { +// final limit = MBLimit( +// currency: key as String, +// min: Decimal.parse(map[key]["min"].toString()), +// max: Decimal.parse(map[key]["max"].toString()), +// ); +// limits.add(limit); +// } +// +// return ExchangeResponse(value: limits); +// } catch (e, s) { +// Logging.instance.e("getLimits exception", error: e, stackTrace: s); +// return ExchangeResponse( +// exception: ExchangeException( +// e.toString(), +// ExchangeExceptionType.generic, +// ), +// ); +// } +// } +// +// /// If [reversed] then the amount is the expected receive_amount, otherwise +// /// the amount is assumed to be the from_amount. +// Future> calculateOrder({ +// required String amount, +// required bool reversed, +// required String fromCurrency, +// required String receiveCurrency, +// }) async { +// final params = { +// "from_currency": fromCurrency, +// "receive_currency": receiveCurrency, +// }; +// +// if (reversed) { +// params["receive_amount"] = amount; +// } else { +// params["from_amount"] = amount; +// } +// +// final uri = _buildUri(endpoint: "calculate", params: params); +// +// try { +// final jsonObject = await _makeGetRequest(uri); +// final map = Map.from(jsonObject as Map); +// +// if (map["error"] != null) { +// final errorMessage = map["extra"] as String?; +// if (errorMessage != null && +// errorMessage.startsWith("Bad") && +// errorMessage.endsWith("currency symbol")) { +// return ExchangeResponse( +// exception: PairUnavailableException( +// errorMessage, +// ExchangeExceptionType.generic, +// ), +// ); +// } else { +// return ExchangeResponse( +// exception: ExchangeException( +// errorMessage ?? "Error: ${map["error"]}", +// ExchangeExceptionType.generic, +// ), +// ); +// } +// } +// +// final result = MBOrderCalculation( +// fromCurrency: map["from_currency"] as String, +// fromAmount: Decimal.parse(map["from_amount"].toString()), +// receiveCurrency: map["receive_currency"] as String, +// receiveAmount: Decimal.parse(map["receive_amount"].toString()), +// ); +// +// return ExchangeResponse(value: result); +// } catch (e, s) { +// Logging.instance.e( +// "calculateOrder $fromCurrency-$receiveCurrency exception: ", +// error: e, +// stackTrace: s, +// ); +// return ExchangeResponse( +// exception: ExchangeException( +// e.toString(), +// ExchangeExceptionType.generic, +// ), +// ); +// } +// } +// +// Future> createOrder({ +// required String fromAmount, +// required String fromCurrency, +// required String receiveCurrency, +// required String receiveAddress, +// }) async { +// final params = { +// "from_amount": fromAmount, +// "from_currency": fromCurrency, +// "receive_currency": receiveCurrency, +// "receive_address": receiveAddress, +// "referral_code": kMajesticBankRefCode, +// }; +// +// final uri = _buildUri(endpoint: "exchange", params: params); +// +// try { +// final now = DateTime.now(); +// final jsonObject = await _makeGetRequest(uri); +// final json = Map.from(jsonObject as Map); +// +// final order = MBOrder( +// orderId: json["trx"] as String, +// fromCurrency: json["from_currency"] as String, +// fromAmount: Decimal.parse(json["from_amount"].toString()), +// receiveCurrency: json["receive_currency"] as String, +// receiveAmount: Decimal.parse(json["receive_amount"].toString()), +// address: json["address"] as String, +// orderType: MBOrderType.floating, +// expiration: json["expiration"] as int, +// createdAt: now, +// ); +// +// return ExchangeResponse(value: order); +// } catch (e, s) { +// Logging.instance.e("createOrder exception", error: e, stackTrace: s); +// return ExchangeResponse( +// exception: ExchangeException( +// e.toString(), +// ExchangeExceptionType.generic, +// ), +// ); +// } +// } +// +// /// Fixed rate for 10 minutes, useful for payments. +// /// If [reversed] then the amount is the expected receive_amount, otherwise +// /// the amount is assumed to be the from_amount. +// Future> createFixedRateOrder({ +// required String amount, +// required String fromCurrency, +// required String receiveCurrency, +// required String receiveAddress, +// required bool reversed, +// }) async { +// final params = { +// "from_currency": fromCurrency, +// "receive_currency": receiveCurrency, +// "receive_address": receiveAddress, +// "referral_code": kMajesticBankRefCode, +// }; +// +// if (reversed) { +// params["receive_amount"] = amount; +// } else { +// params["from_amount"] = amount; +// } +// +// final uri = _buildUri(endpoint: "pay", params: params); +// +// try { +// final now = DateTime.now(); +// final jsonObject = await _makeGetRequest(uri); +// final json = Map.from(jsonObject as Map); +// +// final order = MBOrder( +// orderId: json["trx"] as String, +// fromCurrency: json["from_currency"] as String, +// fromAmount: Decimal.parse(json["from_amount"].toString()), +// receiveCurrency: json["receive_currency"] as String, +// receiveAmount: Decimal.parse(json["receive_amount"].toString()), +// address: json["address"] as String, +// orderType: MBOrderType.fixed, +// expiration: json["expiration"] as int, +// createdAt: now, +// ); +// +// return ExchangeResponse(value: order); +// } catch (e, s) { +// Logging.instance.e( +// "createFixedRateOrder exception: ", +// error: e, +// stackTrace: s, +// ); +// return ExchangeResponse( +// exception: ExchangeException( +// e.toString(), +// ExchangeExceptionType.generic, +// ), +// ); +// } +// } +// +// Future> trackOrder({ +// required String orderId, +// }) async { +// final uri = _buildUri(endpoint: "track", params: {"trx": orderId}); +// +// try { +// final jsonObject = await _makeGetRequest(uri); +// final json = Map.from(jsonObject as Map); +// +// if (json.length == 2) { +// return ExchangeResponse( +// exception: MBException( +// json["status"] as String, +// ExchangeExceptionType.orderNotFound, +// ), +// ); +// } +// +// final status = MBOrderStatus( +// orderId: json["trx"] as String, +// status: json["status"] as String, +// fromCurrency: json["from_currency"] as String, +// fromAmount: Decimal.parse(json["from_amount"].toString()), +// receiveCurrency: json["receive_currency"] as String, +// receiveAmount: Decimal.parse(json["receive_amount"].toString()), +// address: json["address"] as String, +// received: Decimal.parse(json["received"].toString()), +// confirmed: Decimal.parse(json["confirmed"].toString()), +// ); +// +// return ExchangeResponse(value: status); +// } catch (e, s) { +// Logging.instance.e( +// "trackOrder exception when trying to parse $json: ", +// error: e, +// stackTrace: s, +// ); +// return ExchangeResponse( +// exception: ExchangeException( +// e.toString(), +// ExchangeExceptionType.generic, +// ), +// ); +// } +// } +// } diff --git a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart index 2d283e69e..aacceb587 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart @@ -1,338 +1,346 @@ -/* - * 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:decimal/decimal.dart'; -import 'package:uuid/uuid.dart'; - -import '../../../app_config.dart'; -import '../../../exceptions/exchange/exchange_exception.dart'; -import '../../../exceptions/exchange/majestic_bank/mb_exception.dart'; -import '../../../models/exchange/majestic_bank/mb_order.dart'; -import '../../../models/exchange/response_objects/estimate.dart'; -import '../../../models/exchange/response_objects/range.dart'; -import '../../../models/exchange/response_objects/trade.dart'; -import '../../../models/isar/exchange_cache/currency.dart'; -import '../../../models/isar/exchange_cache/pair.dart'; -import '../exchange.dart'; -import '../exchange_response.dart'; -import 'majestic_bank_api.dart'; - -class MajesticBankExchange extends Exchange { - MajesticBankExchange._(); - - static MajesticBankExchange? _instance; - static MajesticBankExchange get instance => - _instance ??= MajesticBankExchange._(); - - static const exchangeName = "Majestic Bank"; - - static const kMajesticBankCurrencyNames = { - "BCH": "Bitcoin Cash", - "BTC": "Bitcoin", - "DOGE": "Dogecoin", - "EPIC": "Epic Cash", - "FIRO": "Firo", - "LTC": "Litecoin", - "NMC": "Namecoin", - "PART": "Particl", - "WOW": "Wownero", - "XMR": "Monero", - }; - - @override - bool get supportsRefundAddress => false; - - @override - Future> createTrade({ - required String from, - required String to, - required bool fixedRate, - required Decimal amount, - required String addressTo, - String? extraId, - required String addressRefund, - required String refundExtraId, - Estimate? estimate, - required bool reversed, - }) async { - ExchangeResponse? response; - - if (fixedRate) { - response = await MajesticBankAPI.instance.createFixedRateOrder( - amount: amount.toString(), - fromCurrency: from, - receiveCurrency: to, - receiveAddress: addressTo, - reversed: reversed, - ); - } else { - if (reversed) { - return ExchangeResponse( - exception: MBException( - "Reversed trade not available", - ExchangeExceptionType.generic, - ), - ); - } - response = await MajesticBankAPI.instance.createOrder( - fromAmount: amount.toString(), - fromCurrency: from, - receiveCurrency: to, - receiveAddress: addressTo, - ); - } - - if (response.value != null) { - final order = response.value!; - final trade = Trade( - uuid: const Uuid().v1(), - tradeId: order.orderId, - rateType: fixedRate ? "fixed" : "floating", - direction: reversed ? "reversed" : "direct", - timestamp: order.createdAt, - updatedAt: order.createdAt, - payInCurrency: order.fromCurrency, - payInAmount: order.fromAmount.toString(), - payInAddress: order.address, - payInNetwork: "", - payInExtraId: "", - payInTxid: "", - payOutCurrency: order.receiveCurrency, - payOutAmount: order.receiveAmount.toString(), - payOutAddress: addressTo, - payOutNetwork: "", - payOutExtraId: "", - payOutTxid: "", - refundAddress: addressRefund, - refundExtraId: refundExtraId, - status: "Waiting", - exchangeName: exchangeName, - ); - - return ExchangeResponse(value: trade); - } else { - return ExchangeResponse(exception: response.exception!); - } - } - - @override - Future>> getAllCurrencies( - bool fixedRate, - ) async { - final response = await MajesticBankAPI.instance.getLimits(); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final List currencies = []; - final limits = response.value!; - - for (final limit in limits) { - final currency = Currency( - exchangeName: MajesticBankExchange.exchangeName, - ticker: limit.currency, - name: kMajesticBankCurrencyNames[limit.currency] ?? - limit.currency, // todo: add more names if MB adds more - network: "", - image: "", - isFiat: false, - rateType: SupportedRateType.both, - isAvailable: true, - isStackCoin: AppConfig.isStackCoin(limit.currency), - tokenContract: null, - ); - currencies.add(currency); - } - - return ExchangeResponse(value: currencies); - } - - @override - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ) { - // TODO: change this if the api changes to allow getting by paired currency - return getAllCurrencies(fixedRate); - } - - @override - Future>> getAllPairs(bool fixedRate) async { - final response = await MajesticBankAPI.instance.getRates(); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final List pairs = []; - final rates = response.value!; - - for (final rate in rates) { - final pair = Pair( - exchangeName: MajesticBankExchange.exchangeName, - from: rate.fromCurrency, - to: rate.toCurrency, - rateType: SupportedRateType.both, - ); - pairs.add(pair); - } - - return ExchangeResponse(value: pairs); - } - - @override - Future>> getEstimates( - String from, - String to, - Decimal amount, - bool fixedRate, - bool reversed, - ) async { - final response = await MajesticBankAPI.instance.calculateOrder( - amount: amount.toString(), - reversed: reversed, - fromCurrency: from, - receiveCurrency: to, - ); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final calc = response.value!; - final estimate = Estimate( - estimatedAmount: reversed ? calc.fromAmount : calc.receiveAmount, - fixedRate: fixedRate, - reversed: reversed, - exchangeProvider: MajesticBankExchange.exchangeName, - ); - return ExchangeResponse(value: [estimate]); - } - - @override - Future>> getPairsFor( - String currency, - bool fixedRate, - ) async { - final response = await getAllPairs(fixedRate); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final pairs = response.value!.where( - (e) => - e.from.toUpperCase() == currency.toUpperCase() || - e.to.toUpperCase() == currency.toUpperCase(), - ); - - return ExchangeResponse(value: pairs.toList()); - } - - @override - Future> getRange( - String from, - String to, - bool fixedRate, - ) async { - final response = - await MajesticBankAPI.instance.getLimit(fromCurrency: from); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final limit = response.value!; - final range = Range(min: limit.min, max: limit.max); - - return ExchangeResponse(value: range); - } - - @override - Future> getTrade(String tradeId) async { - // TODO: implement getTrade - throw UnimplementedError(); - } - - @override - Future>> getTrades() async { - // TODO: implement getTrades - throw UnimplementedError(); - } - - @override - String get name => exchangeName; - - @override - Future> updateTrade(Trade trade) async { - final response = await MajesticBankAPI.instance.trackOrder( - orderId: trade.tradeId, - ); - - if (response.value != null) { - final status = response.value!; - final updatedTrade = Trade( - uuid: trade.uuid, - tradeId: status.orderId, - rateType: trade.rateType, - direction: trade.direction, - timestamp: trade.timestamp, - updatedAt: DateTime.now(), - payInCurrency: status.fromCurrency, - payInAmount: status.fromAmount.toString(), - payInAddress: status.address, - payInNetwork: trade.payInNetwork, - payInExtraId: trade.payInExtraId, - payInTxid: trade.payInTxid, - payOutCurrency: status.receiveCurrency, - payOutAmount: status.receiveAmount.toString(), - payOutAddress: trade.payOutAddress, - payOutNetwork: trade.payOutNetwork, - payOutExtraId: trade.payOutExtraId, - payOutTxid: trade.payOutTxid, - refundAddress: trade.refundAddress, - refundExtraId: trade.refundExtraId, - status: status.status, - exchangeName: exchangeName, - ); - - return ExchangeResponse(value: updatedTrade); - } else { - if (response.exception?.type == ExchangeExceptionType.orderNotFound) { - final updatedTrade = Trade( - uuid: trade.uuid, - tradeId: trade.tradeId, - rateType: trade.rateType, - direction: trade.direction, - timestamp: trade.timestamp, - updatedAt: DateTime.now(), - payInCurrency: trade.payInCurrency, - payInAmount: trade.payInAmount, - payInAddress: trade.payInAddress, - payInNetwork: trade.payInNetwork, - payInExtraId: trade.payInExtraId, - payInTxid: trade.payInTxid, - payOutCurrency: trade.payOutCurrency, - payOutAmount: trade.payOutAmount, - payOutAddress: trade.payOutAddress, - payOutNetwork: trade.payOutNetwork, - payOutExtraId: trade.payOutExtraId, - payOutTxid: trade.payOutTxid, - refundAddress: trade.refundAddress, - refundExtraId: trade.refundExtraId, - status: "Completed", - exchangeName: exchangeName, - ); - return ExchangeResponse(value: updatedTrade); - } - return ExchangeResponse(exception: response.exception); - } - } - - // Majestic Bank supports tor. - @override - bool get supportsTor => true; -} +// /* +// * 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:decimal/decimal.dart'; +// import 'package:uuid/uuid.dart'; +// +// import '../../../app_config.dart'; +// import '../../../exceptions/exchange/exchange_exception.dart'; +// import '../../../exceptions/exchange/majestic_bank/mb_exception.dart'; +// import '../../../models/exchange/majestic_bank/mb_order.dart'; +// import '../../../models/exchange/response_objects/estimate.dart'; +// import '../../../models/exchange/response_objects/range.dart'; +// import '../../../models/exchange/response_objects/trade.dart'; +// import '../../../models/isar/exchange_cache/currency.dart'; +// import '../../../models/isar/exchange_cache/pair.dart'; +// import '../exchange.dart'; +// import '../exchange_response.dart'; +// import 'majestic_bank_api.dart'; +// +// class MajesticBankExchange /*extends Exchange*/ { +// MajesticBankExchange._(); +// +// static MajesticBankExchange? _instance; +// static MajesticBankExchange get instance => +// _instance ??= MajesticBankExchange._(); +// +// static const exchangeName = "Majestic Bank"; +// +// static const kMajesticBankCurrencyNames = { +// "BCH": "Bitcoin Cash", +// "BTC": "Bitcoin", +// "DOGE": "Dogecoin", +// "EPIC": "Epic Cash", +// "FIRO": "Firo", +// "LTC": "Litecoin", +// "NMC": "Namecoin", +// "PART": "Particl", +// "WOW": "Wownero", +// "XMR": "Monero", +// }; +// +// @override +// bool get supportsRefundAddress => false; +// +// @override +// Future> createTrade({ +// required String from, +// required String to, +// required String? fromNetwork, +// required String? toNetwork, +// required bool fixedRate, +// required Decimal amount, +// required String addressTo, +// String? extraId, +// required String addressRefund, +// required String refundExtraId, +// Estimate? estimate, +// required bool reversed, +// }) async { +// ExchangeResponse? response; +// +// if (fixedRate) { +// response = await MajesticBankAPI.instance.createFixedRateOrder( +// amount: amount.toString(), +// fromCurrency: from, +// receiveCurrency: to, +// receiveAddress: addressTo, +// reversed: reversed, +// ); +// } else { +// if (reversed) { +// return ExchangeResponse( +// exception: MBException( +// "Reversed trade not available", +// ExchangeExceptionType.generic, +// ), +// ); +// } +// response = await MajesticBankAPI.instance.createOrder( +// fromAmount: amount.toString(), +// fromCurrency: from, +// receiveCurrency: to, +// receiveAddress: addressTo, +// ); +// } +// +// if (response.value != null) { +// final order = response.value!; +// final trade = Trade( +// uuid: const Uuid().v1(), +// tradeId: order.orderId, +// rateType: fixedRate ? "fixed" : "floating", +// direction: reversed ? "reversed" : "direct", +// timestamp: order.createdAt, +// updatedAt: order.createdAt, +// payInCurrency: order.fromCurrency, +// payInAmount: order.fromAmount.toString(), +// payInAddress: order.address, +// payInNetwork: "", +// payInExtraId: "", +// payInTxid: "", +// payOutCurrency: order.receiveCurrency, +// payOutAmount: order.receiveAmount.toString(), +// payOutAddress: addressTo, +// payOutNetwork: "", +// payOutExtraId: "", +// payOutTxid: "", +// refundAddress: addressRefund, +// refundExtraId: refundExtraId, +// status: "Waiting", +// exchangeName: exchangeName, +// ); +// +// return ExchangeResponse(value: trade); +// } else { +// return ExchangeResponse(exception: response.exception!); +// } +// } +// +// @override +// Future>> getAllCurrencies( +// bool fixedRate, +// ) async { +// final response = await MajesticBankAPI.instance.getLimits(); +// if (response.value == null) { +// return ExchangeResponse(exception: response.exception); +// } +// +// final List currencies = []; +// final limits = response.value!; +// +// for (final limit in limits) { +// final currency = Currency( +// exchangeName: MajesticBankExchange.exchangeName, +// ticker: limit.currency, +// name: +// kMajesticBankCurrencyNames[limit.currency] ?? +// limit.currency, // todo: add more names if MB adds more +// network: "", +// image: "", +// isFiat: false, +// rateType: SupportedRateType.both, +// isAvailable: true, +// isStackCoin: AppConfig.isStackCoin(limit.currency), +// tokenContract: null, +// ); +// currencies.add(currency); +// } +// +// return ExchangeResponse(value: currencies); +// } +// +// // @override +// // Future>> getPairedCurrencies( +// // String forCurrency, +// // bool fixedRate, +// // ) { +// // // TODO: change this if the api changes to allow getting by paired currency +// // return getAllCurrencies(fixedRate); +// // } +// // +// // @override +// // Future>> getAllPairs(bool fixedRate) async { +// // final response = await MajesticBankAPI.instance.getRates(); +// // if (response.value == null) { +// // return ExchangeResponse(exception: response.exception); +// // } +// // +// // final List pairs = []; +// // final rates = response.value!; +// // +// // for (final rate in rates) { +// // final pair = Pair( +// // exchangeName: MajesticBankExchange.exchangeName, +// // from: rate.fromCurrency, +// // to: rate.toCurrency, +// // rateType: SupportedRateType.both, +// // ); +// // pairs.add(pair); +// // } +// // +// // return ExchangeResponse(value: pairs); +// // } +// +// @override +// Future>> getEstimates( +// String from, +// String? fromNetwork, +// String to, +// String? toNetwork, +// Decimal amount, +// bool fixedRate, +// bool reversed, +// ) async { +// final response = await MajesticBankAPI.instance.calculateOrder( +// amount: amount.toString(), +// reversed: reversed, +// fromCurrency: from, +// receiveCurrency: to, +// ); +// if (response.value == null) { +// return ExchangeResponse(exception: response.exception); +// } +// +// final calc = response.value!; +// final estimate = Estimate( +// estimatedAmount: reversed ? calc.fromAmount : calc.receiveAmount, +// fixedRate: fixedRate, +// reversed: reversed, +// exchangeProvider: MajesticBankExchange.exchangeName, +// ); +// return ExchangeResponse(value: [estimate]); +// } +// +// // @override +// // Future>> getPairsFor( +// // String currency, +// // bool fixedRate, +// // ) async { +// // final response = await getAllPairs(fixedRate); +// // if (response.value == null) { +// // return ExchangeResponse(exception: response.exception); +// // } +// // +// // final pairs = response.value!.where( +// // (e) => +// // e.from.toUpperCase() == currency.toUpperCase() || +// // e.to.toUpperCase() == currency.toUpperCase(), +// // ); +// // +// // return ExchangeResponse(value: pairs.toList()); +// // } +// +// @override +// Future> getRange( +// String from, +// String? fromNetwork, +// String to, +// String? toNetwork, +// bool fixedRate, +// ) async { +// final response = await MajesticBankAPI.instance.getLimit( +// fromCurrency: from, +// ); +// if (response.value == null) { +// return ExchangeResponse(exception: response.exception); +// } +// +// final limit = response.value!; +// final range = Range(min: limit.min, max: limit.max); +// +// return ExchangeResponse(value: range); +// } +// +// @override +// Future> getTrade(String tradeId) async { +// // TODO: implement getTrade +// throw UnimplementedError(); +// } +// +// @override +// Future>> getTrades() async { +// // TODO: implement getTrades +// throw UnimplementedError(); +// } +// +// @override +// String get name => exchangeName; +// +// @override +// Future> updateTrade(Trade trade) async { +// final response = await MajesticBankAPI.instance.trackOrder( +// orderId: trade.tradeId, +// ); +// +// if (response.value != null) { +// final status = response.value!; +// final updatedTrade = Trade( +// uuid: trade.uuid, +// tradeId: status.orderId, +// rateType: trade.rateType, +// direction: trade.direction, +// timestamp: trade.timestamp, +// updatedAt: DateTime.now(), +// payInCurrency: status.fromCurrency, +// payInAmount: status.fromAmount.toString(), +// payInAddress: status.address, +// payInNetwork: trade.payInNetwork, +// payInExtraId: trade.payInExtraId, +// payInTxid: trade.payInTxid, +// payOutCurrency: status.receiveCurrency, +// payOutAmount: status.receiveAmount.toString(), +// payOutAddress: trade.payOutAddress, +// payOutNetwork: trade.payOutNetwork, +// payOutExtraId: trade.payOutExtraId, +// payOutTxid: trade.payOutTxid, +// refundAddress: trade.refundAddress, +// refundExtraId: trade.refundExtraId, +// status: status.status, +// exchangeName: exchangeName, +// ); +// +// return ExchangeResponse(value: updatedTrade); +// } else { +// if (response.exception?.type == ExchangeExceptionType.orderNotFound) { +// final updatedTrade = Trade( +// uuid: trade.uuid, +// tradeId: trade.tradeId, +// rateType: trade.rateType, +// direction: trade.direction, +// timestamp: trade.timestamp, +// updatedAt: DateTime.now(), +// payInCurrency: trade.payInCurrency, +// payInAmount: trade.payInAmount, +// payInAddress: trade.payInAddress, +// payInNetwork: trade.payInNetwork, +// payInExtraId: trade.payInExtraId, +// payInTxid: trade.payInTxid, +// payOutCurrency: trade.payOutCurrency, +// payOutAmount: trade.payOutAmount, +// payOutAddress: trade.payOutAddress, +// payOutNetwork: trade.payOutNetwork, +// payOutExtraId: trade.payOutExtraId, +// payOutTxid: trade.payOutTxid, +// refundAddress: trade.refundAddress, +// refundExtraId: trade.refundExtraId, +// status: "Completed", +// exchangeName: exchangeName, +// ); +// return ExchangeResponse(value: updatedTrade); +// } +// return ExchangeResponse(exception: response.exception); +// } +// } +// +// // Majestic Bank supports tor. +// @override +// bool get supportsTor => true; +// } diff --git a/lib/services/exchange/nanswap/nanswap_exchange.dart b/lib/services/exchange/nanswap/nanswap_exchange.dart index de89b886e..2392199e7 100644 --- a/lib/services/exchange/nanswap/nanswap_exchange.dart +++ b/lib/services/exchange/nanswap/nanswap_exchange.dart @@ -30,6 +30,8 @@ class NanswapExchange extends Exchange { Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -74,9 +76,7 @@ class NanswapExchange extends Exchange { ); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -108,9 +108,7 @@ class NanswapExchange extends Exchange { ), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -136,34 +134,31 @@ class NanswapExchange extends Exchange { final response = await NanswapAPI.instance.getSupportedCurrencies(); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } return ExchangeResponse( - value: response.value! - .where((e) => filter.contains(e.id)) - .map( - (e) => Currency( - exchangeName: exchangeName, - ticker: e.id, - name: e.name, - network: e.network, - image: e.image, - isFiat: false, - rateType: SupportedRateType.estimated, - isStackCoin: AppConfig.isStackCoin(e.id), - tokenContract: null, - isAvailable: true, - ), - ) - .toList(), + value: + response.value! + .where((e) => filter.contains(e.id)) + .map( + (e) => Currency( + exchangeName: exchangeName, + ticker: e.id, + name: e.name, + network: e.network, + image: e.image, + isFiat: false, + rateType: SupportedRateType.estimated, + isStackCoin: AppConfig.isStackCoin(e.id), + tokenContract: null, + isAvailable: true, + ), + ) + .toList(), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -174,15 +169,12 @@ class NanswapExchange extends Exchange { } } - @override - Future>> getAllPairs(bool fixedRate) async { - throw UnimplementedError(); - } - @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, @@ -211,9 +203,7 @@ class NanswapExchange extends Exchange { } if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -231,9 +221,7 @@ class NanswapExchange extends Exchange { ], ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -243,59 +231,53 @@ class NanswapExchange extends Exchange { ); } } - - @override - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ) async { - try { - if (fixedRate) { - throw ExchangeException( - "Nanswap fixedRate not available", - ExchangeExceptionType.generic, - ); - } - - final response = await getAllCurrencies( - fixedRate, - ); - - if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); - } - - return ExchangeResponse( - value: response.value!..removeWhere((e) => e.ticker == forCurrency), - ); - } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); - } catch (e) { - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - @override - Future>> getPairsFor( - String currency, - bool fixedRate, - ) async { - throw UnsupportedError("Not used"); - } + // + // @override + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ) async { + // try { + // if (fixedRate) { + // throw ExchangeException( + // "Nanswap fixedRate not available", + // ExchangeExceptionType.generic, + // ); + // } + // + // final response = await getAllCurrencies( + // fixedRate, + // ); + // + // if (response.exception != null) { + // return ExchangeResponse( + // exception: response.exception, + // ); + // } + // + // return ExchangeResponse( + // value: response.value!..removeWhere((e) => e.ticker == forCurrency), + // ); + // } on ExchangeException catch (e) { + // return ExchangeResponse( + // exception: e, + // ); + // } catch (e) { + // return ExchangeResponse( + // exception: ExchangeException( + // e.toString(), + // ExchangeExceptionType.generic, + // ), + // ); + // } + // } @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { try { @@ -312,9 +294,7 @@ class NanswapExchange extends Exchange { ); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -326,9 +306,7 @@ class NanswapExchange extends Exchange { ), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -342,14 +320,10 @@ class NanswapExchange extends Exchange { @override Future> getTrade(String tradeId) async { try { - final response = await NanswapAPI.instance.getOrder( - id: tradeId, - ); + final response = await NanswapAPI.instance.getOrder(id: tradeId); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -381,9 +355,7 @@ class NanswapExchange extends Exchange { ), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -406,14 +378,10 @@ class NanswapExchange extends Exchange { @override Future> updateTrade(Trade trade) async { try { - final response = await NanswapAPI.instance.getOrder( - id: trade.tradeId, - ); + final response = await NanswapAPI.instance.getOrder(id: trade.tradeId); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -445,9 +413,7 @@ class NanswapExchange extends Exchange { ), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( diff --git a/lib/services/exchange/simpleswap/simpleswap_exchange.dart b/lib/services/exchange/simpleswap/simpleswap_exchange.dart index dae14cbf7..f477d182b 100644 --- a/lib/services/exchange/simpleswap/simpleswap_exchange.dart +++ b/lib/services/exchange/simpleswap/simpleswap_exchange.dart @@ -36,6 +36,8 @@ class SimpleSwapExchange extends Exchange { Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -61,28 +63,31 @@ class SimpleSwapExchange extends Exchange { Future>> getAllCurrencies( bool fixedRate, ) async { - final response = - await SimpleSwapAPI.instance.getAllCurrencies(fixedRate: fixedRate); + final response = await SimpleSwapAPI.instance.getAllCurrencies( + fixedRate: fixedRate, + ); if (response.value != null) { - final List currencies = response.value! - .map( - (e) => Currency( - exchangeName: exchangeName, - ticker: e.symbol, - name: e.name, - network: e.network, - image: e.image, - externalId: e.extraId, - isFiat: false, - rateType: fixedRate - ? SupportedRateType.both - : SupportedRateType.estimated, - isAvailable: true, - isStackCoin: AppConfig.isStackCoin(e.symbol), - tokenContract: null, - ), - ) - .toList(); + final List currencies = + response.value! + .map( + (e) => Currency( + exchangeName: exchangeName, + ticker: e.symbol, + name: e.name, + network: e.network, + image: e.image, + externalId: e.extraId, + isFiat: false, + rateType: + fixedRate + ? SupportedRateType.both + : SupportedRateType.estimated, + isAvailable: true, + isStackCoin: AppConfig.isStackCoin(e.symbol), + tokenContract: null, + ), + ) + .toList(); return ExchangeResponse>( value: currencies, exception: response.exception, @@ -95,15 +100,17 @@ class SimpleSwapExchange extends Exchange { ); } - @override - Future>> getAllPairs(bool fixedRate) async { - return await SimpleSwapAPI.instance.getAllPairs(isFixedRate: fixedRate); - } + // @override + // Future>> getAllPairs(bool fixedRate) async { + // return await SimpleSwapAPI.instance.getAllPairs(isFixedRate: fixedRate); + // } @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, @@ -115,9 +122,7 @@ class SimpleSwapExchange extends Exchange { amount: amount.toString(), ); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } return ExchangeResponse( @@ -135,7 +140,9 @@ class SimpleSwapExchange extends Exchange { @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { return await SimpleSwapAPI.instance.getRange( @@ -145,15 +152,6 @@ class SimpleSwapExchange extends Exchange { ); } - @override - Future>> getPairsFor( - String currency, - bool fixedRate, - ) async { - // return await SimpleSwapAPI.instance.ge - throw UnimplementedError(); - } - @override Future> getTrade(String tradeId) async { return await SimpleSwapAPI.instance.getExchange(exchangeId: tradeId); diff --git a/lib/services/exchange/trocador/response_objects/trocador_trade.dart b/lib/services/exchange/trocador/response_objects/trocador_trade.dart index 5d1f5d72a..781ef596a 100644 --- a/lib/services/exchange/trocador/response_objects/trocador_trade.dart +++ b/lib/services/exchange/trocador/response_objects/trocador_trade.dart @@ -92,32 +92,37 @@ class TrocadorTrade { ); } + Map toMap() { + return { + "tradeId": tradeId, + "date": date.toIso8601String(), + "tickerFrom": tickerFrom, + "tickerTo": tickerTo, + "coinFrom": coinFrom, + "coinTo": coinTo, + "networkFrom": networkFrom, + "networkTo": networkTo, + "amountFrom": amountFrom.toString(), + "amountTo": amountTo.toString(), + "provider": provider, + "fixed": fixed, + "status": status, + "addressProvider": addressProvider, + "addressProviderMemo": addressProviderMemo, + "addressUser": addressUser, + "addressUserMemo": addressUserMemo, + "refundAddress": refundAddress, + "refundAddressMemo": refundAddressMemo, + "password": password, + "idProvider": idProvider, + "quotes": quotes, + "payment": payment, + }; + } + + @override String toString() { - return 'TrocadorTrade( ' - 'tradeId: $tradeId, ' - 'date: $date, ' - 'tickerFrom: $tickerFrom, ' - 'tickerTo: $tickerTo, ' - 'coinFrom: $coinFrom, ' - 'coinTo: $coinTo, ' - 'networkFrom: $networkFrom, ' - 'networkTo: $networkTo, ' - 'amountFrom: $amountFrom, ' - 'amountTo: $amountTo, ' - 'provider: $provider, ' - 'fixed: $fixed, ' - 'status: $status, ' - 'addressProvider: $addressProvider, ' - 'addressProviderMemo: $addressProviderMemo, ' - 'addressUser: $addressUser, ' - 'addressUserMemo: $addressUserMemo, ' - 'refundAddress: $refundAddress, ' - 'refundAddressMemo: $refundAddressMemo, ' - 'password: $password, ' - 'idProvider: $idProvider, ' - 'quotes: $quotes, ' - 'payment: $payment ' - ')'; + return "TrocadorTrade: ${toMap()}"; } } diff --git a/lib/services/exchange/trocador/trocador_api.dart b/lib/services/exchange/trocador/trocador_api.dart index 9aff6d872..917fee3f3 100644 --- a/lib/services/exchange/trocador/trocador_api.dart +++ b/lib/services/exchange/trocador/trocador_api.dart @@ -56,9 +56,10 @@ abstract class TrocadorAPI { "Content-Type": "application/json", "API-KEY": kTrocadorApiKey, }, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); code = response.code; @@ -68,10 +69,17 @@ abstract class TrocadorAPI { final json = jsonDecode(response.body); + if (code != 200) { + throw Exception(json["error"] as String? ?? json); + } + return json; } catch (e, s) { - Logging.instance - .e("_makeRequest($uri) HTTP:$code threw: ", error: e, stackTrace: s); + Logging.instance.e( + "_makeRequest($uri) HTTP:$code threw: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -83,9 +91,7 @@ abstract class TrocadorAPI { final uri = _buildUri( isOnion: isOnion, method: "coins", - params: { - "ref": kTrocadorRefCode, - }, + params: {"ref": kTrocadorRefCode}, ); try { @@ -93,22 +99,15 @@ abstract class TrocadorAPI { if (json is List) { final list = List>.from(json); - final List coins = list - .map( - (e) => TrocadorCoin.fromMap(e), - ) - .toList(); + final List coins = + list.map((e) => TrocadorCoin.fromMap(e)).toList(); return ExchangeResponse(value: coins); } else { throw Exception("unexpected json: $json"); } } catch (e, s) { - Logging.instance.e( - "getCoins exception", - error: e, - stackTrace: s, - ); + Logging.instance.e("getCoins exception", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -126,10 +125,7 @@ abstract class TrocadorAPI { final uri = _buildUri( isOnion: isOnion, method: "trade", - params: { - "ref": kTrocadorRefCode, - "id": tradeId, - }, + params: {"ref": kTrocadorRefCode, "id": tradeId}, ); try { @@ -138,11 +134,7 @@ abstract class TrocadorAPI { return ExchangeResponse(value: TrocadorTrade.fromMap(map)); } catch (e, s) { - Logging.instance.e( - "getTrade exception", - error: e, - stackTrace: s, - ); + Logging.instance.e("getTrade exception", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -204,11 +196,7 @@ abstract class TrocadorAPI { required bool isOnion, required Map params, }) async { - final uri = _buildUri( - isOnion: isOnion, - method: "new_rate", - params: params, - ); + final uri = _buildUri(isOnion: isOnion, method: "new_rate", params: params); try { final json = await _makeGetRequest(uri); @@ -216,11 +204,7 @@ abstract class TrocadorAPI { return ExchangeResponse(value: TrocadorRate.fromMap(map)); } catch (e, s) { - Logging.instance.e( - "getNewRate exception", - error: e, - stackTrace: s, - ); + Logging.instance.e("getNewRate exception", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), diff --git a/lib/services/exchange/trocador/trocador_exchange.dart b/lib/services/exchange/trocador/trocador_exchange.dart index cf02cc206..ffb217a9f 100644 --- a/lib/services/exchange/trocador/trocador_exchange.dart +++ b/lib/services/exchange/trocador/trocador_exchange.dart @@ -36,10 +36,28 @@ class TrocadorExchange extends Exchange { static const onlySupportedNetwork = "Mainnet"; + static ProviderWarning? checkFiro(Currency currency) { + if (currency.ticker.toLowerCase() == "firo" && + currency.name.contains("No Spark")) { + return ProviderWarning.noSpark; + } + return null; + } + + static ProviderWarning? checkLtc(Currency currency) { + if (currency.ticker.toLowerCase() == "ltc" && + currency.name.contains("not MW")) { + return ProviderWarning.noMWEB; + } + return null; + } + @override Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -49,37 +67,38 @@ class TrocadorExchange extends Exchange { Estimate? estimate, required bool reversed, }) async { - final response = reversed - ? await TrocadorAPI.createNewPaymentRateTrade( - isOnion: false, - rateId: estimate?.rateId, - fromTicker: from.toLowerCase(), - fromNetwork: onlySupportedNetwork, - toTicker: to.toLowerCase(), - toNetwork: onlySupportedNetwork, - toAmount: amount.toString(), - receivingAddress: addressTo, - receivingMemo: null, - refundAddress: addressRefund, - refundMemo: null, - exchangeProvider: estimate!.exchangeProvider!, - isFixedRate: fixedRate, - ) - : await TrocadorAPI.createNewStandardRateTrade( - isOnion: false, - rateId: estimate?.rateId, - fromTicker: from.toLowerCase(), - fromNetwork: onlySupportedNetwork, - toTicker: to.toLowerCase(), - toNetwork: onlySupportedNetwork, - fromAmount: amount.toString(), - receivingAddress: addressTo, - receivingMemo: null, - refundAddress: addressRefund, - refundMemo: null, - exchangeProvider: estimate!.exchangeProvider!, - isFixedRate: fixedRate, - ); + final response = + reversed + ? await TrocadorAPI.createNewPaymentRateTrade( + isOnion: false, + rateId: estimate?.rateId, + fromTicker: from.toLowerCase(), + fromNetwork: onlySupportedNetwork, + toTicker: to.toLowerCase(), + toNetwork: onlySupportedNetwork, + toAmount: amount.toString(), + receivingAddress: addressTo, + receivingMemo: null, + refundAddress: addressRefund, + refundMemo: null, + exchangeProvider: estimate!.exchangeProvider!, + isFixedRate: fixedRate, + ) + : await TrocadorAPI.createNewStandardRateTrade( + isOnion: false, + rateId: estimate?.rateId, + fromTicker: from.toLowerCase(), + fromNetwork: onlySupportedNetwork, + toTicker: to.toLowerCase(), + toNetwork: onlySupportedNetwork, + fromAmount: amount.toString(), + receivingAddress: addressTo, + receivingMemo: null, + refundAddress: addressRefund, + refundMemo: null, + exchangeProvider: estimate!.exchangeProvider!, + isFixedRate: fixedRate, + ); if (response.value == null) { return ExchangeResponse(exception: response.exception); @@ -125,22 +144,23 @@ class TrocadorExchange extends Exchange { _cachedCurrencies?.removeWhere((e) => e.network != onlySupportedNetwork); - final value = _cachedCurrencies - ?.map( - (e) => Currency( - exchangeName: exchangeName, - ticker: e.ticker, - name: e.name, - network: e.network, - image: e.image, - isFiat: false, - rateType: SupportedRateType.both, - isStackCoin: AppConfig.isStackCoin(e.ticker), - tokenContract: null, - isAvailable: true, - ), - ) - .toList(); + final value = + _cachedCurrencies + ?.map( + (e) => Currency( + exchangeName: exchangeName, + ticker: e.ticker, + name: e.name, + network: e.network, + image: e.image, + isFiat: false, + rateType: SupportedRateType.both, + isStackCoin: AppConfig.isStackCoin(e.ticker), + tokenContract: null, + isAvailable: true, + ), + ) + .toList(); if (value == null) { return ExchangeResponse( @@ -195,28 +215,31 @@ class TrocadorExchange extends Exchange { @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, ) async { - final response = reversed - ? await TrocadorAPI.getNewPaymentRate( - isOnion: false, - fromTicker: from, - fromNetwork: onlySupportedNetwork, - toTicker: to, - toNetwork: onlySupportedNetwork, - toAmount: amount.toString(), - ) - : await TrocadorAPI.getNewStandardRate( - isOnion: false, - fromTicker: from, - fromNetwork: onlySupportedNetwork, - toTicker: to, - toNetwork: onlySupportedNetwork, - fromAmount: amount.toString(), - ); + final response = + reversed + ? await TrocadorAPI.getNewPaymentRate( + isOnion: false, + fromTicker: from, + fromNetwork: onlySupportedNetwork, + toTicker: to, + toNetwork: onlySupportedNetwork, + toAmount: amount.toString(), + ) + : await TrocadorAPI.getNewStandardRate( + isOnion: false, + fromTicker: from, + fromNetwork: onlySupportedNetwork, + toTicker: to, + toNetwork: onlySupportedNetwork, + fromAmount: amount.toString(), + ); if (response.value == null) { return ExchangeResponse(exception: response.exception); @@ -265,43 +288,46 @@ class TrocadorExchange extends Exchange { } return ExchangeResponse( - value: estimates - ..sort((a, b) => b.estimatedAmount.compareTo(a.estimatedAmount)), + value: + estimates + ..sort((a, b) => b.estimatedAmount.compareTo(a.estimatedAmount)), ); } - @override - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ) async { - // TODO: implement getPairedCurrencies - throw UnimplementedError(); - } - - @override - Future>> getPairsFor( - String currency, - bool fixedRate, - ) async { - final response = await getAllPairs(fixedRate); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final pairs = response.value!.where( - (e) => - e.from.toUpperCase() == currency.toUpperCase() || - e.to.toUpperCase() == currency.toUpperCase(), - ); - - return ExchangeResponse(value: pairs.toList()); - } + // @override + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ) async { + // // TODO: implement getPairedCurrencies + // throw UnimplementedError(); + // } + // + // @override + // Future>> getPairsFor( + // String currency, + // bool fixedRate, + // ) async { + // final response = await getAllPairs(fixedRate); + // if (response.value == null) { + // return ExchangeResponse(exception: response.exception); + // } + // + // final pairs = response.value!.where( + // (e) => + // e.from.toUpperCase() == currency.toUpperCase() || + // e.to.toUpperCase() == currency.toUpperCase(), + // ); + // + // return ExchangeResponse(value: pairs.toList()); + // } @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { if (_cachedCurrencies == null) { @@ -316,14 +342,12 @@ class TrocadorExchange extends Exchange { ); } - final fromCoin = _cachedCurrencies! - .firstWhere((e) => e.ticker.toLowerCase() == from.toLowerCase()); + final fromCoin = _cachedCurrencies!.firstWhere( + (e) => e.ticker.toLowerCase() == from.toLowerCase(), + ); return ExchangeResponse( - value: Range( - max: fromCoin.maximum, - min: fromCoin.minimum, - ), + value: Range(max: fromCoin.maximum, min: fromCoin.minimum), ); } @@ -413,3 +437,25 @@ class TrocadorExchange extends Exchange { @override bool get supportsTor => true; } + +enum ProviderWarning { + noSpark("NO SPARK"), + noMWEB("NO MW"); + + final String value; + const ProviderWarning(this.value); + + String get message => switch (this) { + ProviderWarning.noSpark => "No Spark", + ProviderWarning.noMWEB => "No MimbleWimble", + }; + + String get messageDetail => switch (this) { + ProviderWarning.noSpark => + "Trocador does not support Firo transactions involving Spark addresses," + " including both sending to and receiving from them.", + ProviderWarning.noMWEB => + "Trocador does not support Litecoin transactions involving MWEB " + "(MimbleWimble Extension Block) addresses.", + }; +} diff --git a/lib/services/frost.dart b/lib/services/frost.dart index 76c0b5db9..33066de92 100644 --- a/lib/services/frost.dart +++ b/lib/services/frost.dart @@ -12,12 +12,11 @@ import '../utilities/amount/amount.dart'; import '../utilities/extensions/extensions.dart'; import '../utilities/logger.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; +import '../wallets/models/tx_recipient.dart'; abstract class Frost { //==================== utility =============================================== - static List getParticipants({ - required String multisigConfig, - }) { + static List getParticipants({required String multisigConfig}) { try { final numberOfParticipants = multisigParticipants( multisigConfig: multisigConfig, @@ -26,10 +25,7 @@ abstract class Frost { final List participants = []; for (int i = 0; i < numberOfParticipants; i++) { participants.add( - multisigParticipant( - multisigConfig: multisigConfig, - index: i, - ), + multisigParticipant(multisigConfig: multisigConfig, index: i), ); } @@ -45,18 +41,18 @@ abstract class Frost { decodeMultisigConfig(multisigConfig: encodedConfig); return true; } catch (e, s) { - Logging.instance.f("validateEncodedMultisigConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "validateEncodedMultisigConfig failed: ", + error: e, + stackTrace: s, + ); return false; } } - static int getThreshold({ - required String multisigConfig, - }) { + static int getThreshold({required String multisigConfig}) { try { - final threshold = multisigThreshold( - multisigConfig: multisigConfig, - ); + final threshold = multisigThreshold(multisigConfig: multisigConfig); return threshold; } catch (e, s) { @@ -70,7 +66,8 @@ abstract class Frost { String changeAddress, int feePerWeight, List inputs, - }) extractDataFromSignConfig({ + }) + extractDataFromSignConfig({ required String serializedKeys, required String signConfig, required CryptoCurrency coin, @@ -85,8 +82,9 @@ abstract class Frost { ); // get various data from config - final feePerWeight = - signFeePerWeight(signConfigPointer: signConfigPointer); + final feePerWeight = signFeePerWeight( + signConfigPointer: signConfigPointer, + ); final changeAddress = signChange(signConfigPointer: signConfigPointer); final recipientsCount = signPayments( signConfigPointer: signConfigPointer, @@ -103,15 +101,13 @@ abstract class Frost { signConfigPointer: signConfigPointer, index: i, ); - recipients.add( - ( - address: address, - amount: Amount( - rawValue: BigInt.from(amount), - fractionDigits: coin.fractionDigits, - ), + recipients.add(( + address: address, + amount: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.fractionDigits, ), - ); + )); } // get utxos @@ -135,7 +131,11 @@ abstract class Frost { inputs: outputs, ); } catch (e, s) { - Logging.instance.f("extractDataFromSignConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "extractDataFromSignConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -156,7 +156,11 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("createMultisigConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "createMultisigConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -166,10 +170,8 @@ abstract class Frost { String commitments, Pointer multisigConfigWithNamePtr, Pointer secretShareMachineWrapperPtr, - }) startKeyGeneration({ - required String multisigConfig, - required String myName, - }) { + }) + startKeyGeneration({required String multisigConfig, required String myName}) { try { final startKeyGenResPtr = startKeyGen( multisigConfig: multisigConfig, @@ -189,15 +191,17 @@ abstract class Frost { secretShareMachineWrapperPtr: machinePtr, ); } catch (e, s) { - Logging.instance.f("startKeyGeneration failed: ", error: e, stackTrace: s); + Logging.instance.f( + "startKeyGeneration failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - String share, - Pointer secretSharesResPtr, - }) generateSecretShares({ + static ({String share, Pointer secretSharesResPtr}) + generateSecretShares({ required Pointer multisigConfigWithNamePtr, required String mySeed, required Pointer secretShareMachineWrapperPtr, @@ -216,16 +220,17 @@ abstract class Frost { return (share: share, secretSharesResPtr: secretSharesResPtr); } catch (e, s) { - Logging.instance.f("generateSecretShares failed: ", error: e, stackTrace: s); + Logging.instance.f( + "generateSecretShares failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - Uint8List multisigId, - String recoveryString, - String serializedKeys, - }) completeKeyGeneration({ + static ({Uint8List multisigId, String recoveryString, String serializedKeys}) + completeKeyGeneration({ required Pointer multisigConfigWithNamePtr, required Pointer secretSharesResPtr, required List shares, @@ -254,7 +259,11 @@ abstract class Frost { serializedKeys: serializedKeys, ); } catch (e, s) { - Logging.instance.f("completeKeyGeneration failed: ", error: e, stackTrace: s); + Logging.instance.f( + "completeKeyGeneration failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -265,13 +274,14 @@ abstract class Frost { required String serializedKeys, required int network, required List< - ({ - UTXO utxo, - Uint8List scriptPubKey, - AddressDerivationData addressDerivationData - })> - inputs, - required List<({String address, Amount amount, bool isChange})> outputs, + ({ + UTXO utxo, + Uint8List scriptPubKey, + AddressDerivationData addressDerivationData, + }) + > + inputs, + required List outputs, required String changeAddress, required int feePerWeight, }) { @@ -279,17 +289,18 @@ abstract class Frost { final signConfig = newSignConfig( thresholdKeysWrapperPointer: deserializeKeys(keys: serializedKeys), network: network, - outputs: inputs - .map( - (e) => Output( - hash: e.utxo.txid.toUint8ListFromHex, - vout: e.utxo.vout, - value: e.utxo.value, - scriptPubKey: e.scriptPubKey, - addressDerivationData: e.addressDerivationData, - ), - ) - .toList(), + outputs: + inputs + .map( + (e) => Output( + hash: e.utxo.txid.toUint8ListFromHex, + vout: e.utxo.vout, + value: e.utxo.value, + scriptPubKey: e.scriptPubKey, + addressDerivationData: e.addressDerivationData, + ), + ) + .toList(), paymentAddresses: outputs.map((e) => e.address).toList(), paymentAmounts: outputs.map((e) => e.amount.raw.toInt()).toList(), change: changeAddress, @@ -306,7 +317,8 @@ abstract class Frost { static ({ Pointer machinePtr, String preprocess, - }) attemptSignConfig({ + }) + attemptSignConfig({ required int network, required String config, required String serializedKeys, @@ -333,7 +345,8 @@ abstract class Frost { static ({ Pointer machinePtr, String share, - }) continueSigning({ + }) + continueSigning({ required Pointer machinePtr, required List preprocesses, }) { @@ -358,10 +371,7 @@ abstract class Frost { required List shares, }) { try { - final rawTransaction = completeSign( - machine: machinePtr, - shares: shares, - ); + final rawTransaction = completeSign(machine: machinePtr, shares: shares); return rawTransaction; } catch (e, s) { @@ -404,28 +414,24 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("createResharerConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "createResharerConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - String resharerStart, - Pointer machine, - }) beginResharer({ - required String serializedKeys, - required String config, - }) { + 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, - ); + return (resharerStart: result.encoded, machine: result.machine); } catch (e, s) { Logging.instance.f("beginResharer failed: ", error: e, stackTrace: s); rethrow; @@ -433,10 +439,8 @@ abstract class Frost { } /// expects [resharerStarts] of length equal to resharers. - static ({ - String resharedStart, - Pointer prior, - }) beginReshared({ + static ({String resharedStart, Pointer prior}) + beginReshared({ required String myName, required String resharerConfig, required List resharerStarts, @@ -448,10 +452,7 @@ abstract class Frost { resharerConfig: resharerConfig, resharerStarts: resharerStarts, ); - return ( - resharedStart: result.encoded, - prior: result.machine, - ); + return (resharedStart: result.encoded, prior: result.machine); } catch (e, s) { Logging.instance.f("beginReshared failed: ", error: e, stackTrace: s); rethrow; @@ -476,11 +477,8 @@ abstract class Frost { } /// expects [resharerCompletes] of length equal to resharers - static ({ - String multisigConfig, - String serializedKeys, - String resharedId, - }) finishReshared({ + static ({String multisigConfig, String serializedKeys, String resharedId}) + finishReshared({ required StartResharedRes prior, required List resharerCompletes, }) { @@ -504,7 +502,11 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("decodedResharerConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "decodedResharerConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -513,9 +515,8 @@ abstract class Frost { int newThreshold, Map resharers, List newParticipants, - }) extractResharerConfigData({ - required String rConfig, - }) { + }) + extractResharerConfigData({required String rConfig}) { final decoded = _decodeRConfigWithResharers(rConfig); final resharerConfig = decoded.config; @@ -564,8 +565,9 @@ abstract class Frost { for (final resharer in resharers) { resharersMap[decoded.resharers.entries - .firstWhere((e) => e.value == resharer) - .key] = resharer; + .firstWhere((e) => e.value == resharer) + .key] = + resharer; } return ( @@ -574,28 +576,25 @@ abstract class Frost { newParticipants: newParticipants, ); } catch (e, s) { - Logging.instance.f("extractResharerConfigData failed: ", error: e, stackTrace: s); + Logging.instance.f( + "extractResharerConfigData failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static String encodeRConfig( - String config, - Map resharers, - ) { + static String encodeRConfig(String config, Map resharers) { return base64Encode("$config@${jsonEncode(resharers)}".toUint8ListFromUtf8); } - static String decodeRConfig( - String rConfig, - ) { + static String decodeRConfig(String rConfig) { return base64Decode(rConfig).toUtf8String.split("@").first; } static ({Map resharers, String config}) - _decodeRConfigWithResharers( - String rConfig, - ) { + _decodeRConfigWithResharers(String rConfig) { final parts = base64Decode(rConfig).toUtf8String.split("@"); final config = parts[0]; diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart new file mode 100644 index 000000000..cc5e6bdc0 --- /dev/null +++ b/lib/services/mwebd_service.dart @@ -0,0 +1,478 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_mwebd/flutter_mwebd.dart'; +import 'package:mutex/mutex.dart'; +import 'package:mweb_client/mweb_client.dart'; + +import '../utilities/logger.dart'; +import '../utilities/prefs.dart'; +import '../utilities/stack_file_system.dart'; +import '../wallets/crypto_currency/crypto_currency.dart'; +import 'event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'event_bus/events/global/tor_status_changed_event.dart'; +import 'event_bus/global_event_bus.dart'; +import 'tor_service.dart'; + +final class MwebdService { + static String defaultPeer(CryptoCurrencyNetwork net) => switch (net) { + CryptoCurrencyNetwork.main => "litecoin.stackwallet.com:9333", + CryptoCurrencyNetwork.test => "litecoin.stackwallet.com:19335", + CryptoCurrencyNetwork.stage => throw UnimplementedError(), + CryptoCurrencyNetwork.test4 => throw UnimplementedError(), + }; + + final Map + _map = {}; + + late final StreamSubscription + _torStatusListener; + late final StreamSubscription + _torPreferenceListener; + + final Mutex _torConnectingLock = Mutex(); + + static final instance = MwebdService._(); + + MwebdService._() { + final bus = GlobalEventBus.instance; + + // Listen for tor status changes. + _torStatusListener = bus.on().listen(( + event, + ) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + if (!_torConnectingLock.isLocked) { + await _torConnectingLock.acquire(); + } + break; + + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + break; + } + }); + + // Listen for tor preference changes. + _torPreferenceListener = bus.on().listen(( + event, + ) async { + if (Prefs.instance.useTor) { + return await _torConnectingLock.protect(() async { + final proxyInfo = TorService.sharedInstance.getProxyInfo(); + return await _update(proxyInfo); + }); + } else { + return await _update(null); + } + }); + } + + // locked while mweb servers and clients are updating + final _updateLock = Mutex(); + + // update function called when Tor pref changed + Future _update(({InternetAddress host, int port})? proxyInfo) async { + await _updateLock.protect(() async { + final proxy = + proxyInfo == null + ? "" + : "socks5://${proxyInfo.host.address}:${proxyInfo.port}"; + final nets = _map.keys; + for (final net in nets) { + final old = _map.remove(net)!; + + await old.client.cleanup(); + await old.server.stopServer(); + + final port = await _getRandomUnusedPort(); + if (port == null) { + throw Exception("Could not find an unused port for mwebd"); + } + + final newServer = MwebdServer( + chain: old.server.chain, + dataDir: old.server.dataDir, + peer: old.server.peer, + proxy: proxy, + serverPort: port, + ); + await newServer.createServer(); + await newServer.startServer(); + + final newClient = MwebClient.fromHost( + "127.0.0.1", + newServer.serverPort, + ); + + _map[net] = (server: newServer, client: newClient); + } + }); + } + + Future initService(CryptoCurrencyNetwork net) async { + Logging.instance.i("MwebdService init($net) called..."); + await _updateLock.protect(() async { + if (_map[net] != null) { + Logging.instance.i("MwebdService init($net) was already called."); + return; + } + + if (_map.isNotEmpty) { + for (final old in _map.values) { + try { + await old.client.cleanup(); + await old.server.stopServer(); + } catch (e, s) { + Logging.instance.i( + "Switching mwebd chain. Error likely expected here.", + error: e, + stackTrace: s, + ); + } + } + _map.clear(); + } + + final port = await _getRandomUnusedPort(); + + if (port == null) { + throw Exception("Could not find an unused port for mwebd"); + } + + final chain = switch (net) { + CryptoCurrencyNetwork.main => "mainnet", + CryptoCurrencyNetwork.test => "testnet", + CryptoCurrencyNetwork.stage => throw UnimplementedError(), + CryptoCurrencyNetwork.test4 => throw UnimplementedError(), + }; + + final dir = await StackFileSystem.applicationMwebdDirectory(chain); + + final String proxy; + if (Prefs.instance.useTor) { + final proxyInfo = TorService.sharedInstance.getProxyInfo(); + proxy = "socks5://${proxyInfo.host.address}:${proxyInfo.port}"; + } else { + proxy = ""; + } + + final newServer = MwebdServer( + chain: chain, + dataDir: dir.path, + peer: defaultPeer(net), + proxy: proxy, + serverPort: port, + ); + await newServer.createServer(); + await newServer.startServer(); + + final newClient = MwebClient.fromHost("127.0.0.1", newServer.serverPort); + + _map[net] = (server: newServer, client: newClient); + + Logging.instance.i("MwebdService init($net) completed!"); + }); + } + + /// Get server status. Returns null if no server was initialized. + Future getServerStatus(CryptoCurrencyNetwork net) async { + return await _updateLock.protect(() async { + return await _map[net]?.server.getStatus(); + }); + } + + /// Get client for network. Returns null if no server was initialized. + Future getClient(CryptoCurrencyNetwork net) async { + return await _updateLock.protect(() async { + return _map[net]?.client; + }); + } + + Future> logsStream( + CryptoCurrencyNetwork net, { + Duration pollInterval = const Duration(milliseconds: 200), + }) async { + final controller = StreamController(); + int offset = 0; + String leftover = ''; + Timer? timer; + + final path = + "${(await StackFileSystem.applicationMwebdDirectory(net == CryptoCurrencyNetwork.main ? "mainnet" : "testnet")).path}" + "${Platform.pathSeparator}logs" + "${Platform.pathSeparator}debug.log"; + + Future poll() async { + if (!controller.isClosed) { + final file = File(path); + + if (!file.existsSync()) { + return; + } + + final length = await file.length(); + + if (length > offset) { + final raf = await file.open(); + await raf.setPosition(offset); + final bytes = await raf.read(length - offset); + await raf.close(); + + final chunk = utf8.decode(bytes); + final lines = (leftover + chunk).split('\n'); + leftover = lines.removeLast(); // possibly incomplete + + for (final line in lines) { + controller.add(line); + } + + offset = length; + } + } + } + + timer = Timer.periodic(pollInterval, (_) => poll()); + + controller.onCancel = () { + timer?.cancel(); + controller.close(); + }; + + return controller.stream; + } +} + +// ============================================================================ +Future _getRandomUnusedPort({Set excluded = const {}}) async { + const int minPort = 1024; + const int maxPort = 65535; + const int maxAttempts = 1000; + + final random = Random.secure(); + + for (int i = 0; i < maxAttempts; i++) { + final int potentialPort = minPort + random.nextInt(maxPort - minPort + 1); + + if (excluded.contains(potentialPort)) { + continue; + } + + try { + final ServerSocket socket = await ServerSocket.bind( + InternetAddress.anyIPv4, + potentialPort, + ); + await socket.close(); + return potentialPort; + } catch (_) { + excluded.add(potentialPort); + continue; + } + } + + return null; +} + +// final class MwebdService { +// static String defaultPeer(CryptoCurrencyNetwork net) => switch (net) { +// CryptoCurrencyNetwork.main => "litecoin.stackwallet.com:9333", +// CryptoCurrencyNetwork.test => "litecoin.stackwallet.com:19335", +// CryptoCurrencyNetwork.stage => throw UnimplementedError(), +// CryptoCurrencyNetwork.test4 => throw UnimplementedError(), +// }; +// +// final Map +// _map = {}; +// +// late final StreamSubscription +// _torStatusListener; +// late final StreamSubscription +// _torPreferenceListener; +// +// final Mutex _torConnectingLock = Mutex(); +// +// static final instance = MwebdService._(); +// +// MwebdService._() { +// final bus = GlobalEventBus.instance; +// +// // Listen for tor status changes. +// _torStatusListener = bus.on().listen(( +// event, +// ) async { +// switch (event.newStatus) { +// case TorConnectionStatus.connecting: +// if (!_torConnectingLock.isLocked) { +// await _torConnectingLock.acquire(); +// } +// break; +// +// case TorConnectionStatus.connected: +// case TorConnectionStatus.disconnected: +// if (_torConnectingLock.isLocked) { +// _torConnectingLock.release(); +// } +// break; +// } +// }); +// +// // Listen for tor preference changes. +// _torPreferenceListener = bus.on().listen(( +// event, +// ) async { +// if (Prefs.instance.useTor) { +// return await _torConnectingLock.protect(() async { +// final proxyInfo = TorService.sharedInstance.getProxyInfo(); +// return await _update(proxyInfo); +// }); +// } else { +// return await _update(null); +// } +// }); +// } +// +// // locked while mweb servers and clients are updating +// final _updateLock = Mutex(); +// +// // update function called when Tor pref changed +// Future _update(({InternetAddress host, int port})? proxyInfo) async { +// await _updateLock.protect(() async { +// final proxy = +// proxyInfo == null +// ? "" +// : "${proxyInfo.host.address}:${proxyInfo.port}"; +// final nets = _map.keys; +// for (final net in nets) { +// final old = _map.remove(net)!; +// +// await old.client.cleanup(); +// await old.server.stopServer(); +// +// final port = await _getRandomUnusedPort(); +// if (port == null) { +// throw Exception("Could not find an unused port for mwebd"); +// } +// +// final newServer = MwebdServer( +// chain: old.server.chain, +// dataDir: old.server.dataDir, +// peer: old.server.peer, +// proxy: proxy, +// serverPort: port, +// ); +// await newServer.createServer(); +// await newServer.startServer(); +// +// final newClient = MwebClient.fromHost( +// "127.0.0.1", +// newServer.serverPort, +// ); +// +// _map[net] = (server: newServer, client: newClient); +// } +// }); +// } +// +// Future init(CryptoCurrencyNetwork net) async { +// if (net == CryptoCurrencyNetwork.test) return; +// +// Logging.instance.i("MwebdService init($net) called..."); +// await _updateLock.protect(() async { +// if (_map[net] != null) { +// Logging.instance.i("MwebdService init($net) was already called."); +// return; +// } +// +// final port = await _getRandomUnusedPort(); +// +// if (port == null) { +// throw Exception("Could not find an unused port for mwebd"); +// } +// +// final chain = switch (net) { +// CryptoCurrencyNetwork.main => "mainnet", +// CryptoCurrencyNetwork.test => "testnet", +// CryptoCurrencyNetwork.stage => throw UnimplementedError(), +// CryptoCurrencyNetwork.test4 => throw UnimplementedError(), +// }; +// +// final dir = await StackFileSystem.applicationMwebdDirectory(chain); +// +// final String proxy; +// if (Prefs.instance.useTor) { +// final proxyInfo = TorService.sharedInstance.getProxyInfo(); +// proxy = "${proxyInfo.host.address}:${proxyInfo.port}"; +// } else { +// proxy = ""; +// } +// +// final newServer = MwebdServer( +// chain: chain, +// dataDir: dir.path, +// peer: defaultPeer(net), +// proxy: proxy, +// serverPort: port, +// ); +// await newServer.createServer(); +// await newServer.startServer(); +// +// final newClient = MwebClient.fromHost("127.0.0.1", newServer.serverPort); +// +// _map[net] = (server: newServer, client: newClient); +// +// Logging.instance.i("MwebdService init($net) completed!"); +// }); +// } +// +// /// Get server status. Returns null if no server was initialized. +// Future getServerStatus(CryptoCurrencyNetwork net) async { +// return await _updateLock.protect(() async { +// return await _map[net]?.server.getStatus(); +// }); +// } +// +// /// Get client for network. Returns null if no server was initialized. +// Future getClient(CryptoCurrencyNetwork net) async { +// return await _updateLock.protect(() async { +// return _map[net]?.client; +// }); +// } +// } +// +// // ============================================================================ +// Future _getRandomUnusedPort({Set excluded = const {}}) async { +// const int minPort = 1024; +// const int maxPort = 65535; +// const int maxAttempts = 1000; +// +// final random = Random.secure(); +// +// for (int i = 0; i < maxAttempts; i++) { +// final int potentialPort = minPort + random.nextInt(maxPort - minPort + 1); +// +// if (excluded.contains(potentialPort)) { +// continue; +// } +// +// try { +// final ServerSocket socket = await ServerSocket.bind( +// InternetAddress.anyIPv4, +// potentialPort, +// ); +// await socket.close(); +// return potentialPort; +// } catch (_) { +// excluded.add(potentialPort); +// continue; +// } +// } +// +// return null; +// } diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 35f595050..c1bc338b3 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -27,9 +27,7 @@ class NodeService extends ChangeNotifier { final SecureStorageInterface secureStorageInterface; /// Exposed [secureStorageInterface] in order to inject mock for tests - NodeService({ - required this.secureStorageInterface, - }); + NodeService({required this.secureStorageInterface}); Future updateDefaults() async { // hack @@ -64,6 +62,7 @@ class NodeService extends ChangeNotifier { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: false, ); await DB.instance.put( @@ -76,16 +75,16 @@ class NodeService extends ChangeNotifier { } for (final defaultNode in AppConfig.coins.map( - (e) => e.defaultNode, + (e) => e.defaultNode(isPrimary: true), )) { - final savedNode = DB.instance - .get(boxName: DB.boxNameNodeModels, key: defaultNode.id); + final savedNode = DB.instance.get( + boxName: DB.boxNameNodeModels, + key: defaultNode.id, + ); if (savedNode == null) { // save the default node to hive only if no other nodes for the specific coin exist if (getNodesFor( - AppConfig.getCryptoCurrencyByPrettyName( - defaultNode.coinName, - ), + AppConfig.getCryptoCurrencyByPrettyName(defaultNode.coinName), ).isEmpty) { await DB.instance.put( boxName: DB.boxNameNodeModels, @@ -105,25 +104,8 @@ class NodeService extends ChangeNotifier { torEnabled: savedNode.torEnabled, clearnetEnabled: savedNode.clearnetEnabled, loginName: savedNode.loginName, - ), - ); - } - - // check if a default node is the primary node for the crypto currency - // and update it if needed - final coin = - AppConfig.getCryptoCurrencyByPrettyName(defaultNode.coinName); - final primaryNode = getPrimaryNodeFor(currency: coin); - if (primaryNode != null && primaryNode.id == defaultNode.id) { - await setPrimaryNodeFor( - coin: coin, - node: defaultNode.copyWith( - enabled: primaryNode.enabled, - isFailover: primaryNode.isFailover, - trusted: primaryNode.trusted, - torEnabled: primaryNode.torEnabled, - clearnetEnabled: primaryNode.clearnetEnabled, - loginName: primaryNode.loginName, + isPrimary: savedNode.isPrimary, + forceNoTor: savedNode.forceNoTor, ), ); } @@ -135,25 +117,48 @@ class NodeService extends ChangeNotifier { required NodeModel node, bool shouldNotifyListeners = false, }) async { - await DB.instance.put( - boxName: DB.boxNamePrimaryNodes, - key: coin.identifier, - value: node, + // current + final currentPrimaries = primaryNodes.where( + (e) => e.coinName == coin.identifier && e.id != node.id, ); + final List toStore = []; + for (final node in currentPrimaries) { + final updated = node.copyWith( + loginName: node.loginName, + trusted: node.trusted, + isPrimary: false, + ); + toStore.add(updated); + } + toStore.add( + node.copyWith( + loginName: node.loginName, + trusted: node.trusted, + isPrimary: true, + ), + ); + + for (final node in toStore) { + await DB.instance.put( + boxName: DB.boxNameNodeModels, + key: node.id, + value: node, + ); + } + if (shouldNotifyListeners) { notifyListeners(); } } NodeModel? getPrimaryNodeFor({required CryptoCurrency currency}) { - return DB.instance.get( - boxName: DB.boxNamePrimaryNodes, - key: currency.identifier, - ); + return primaryNodes + .where((e) => e.coinName == currency.identifier) + .firstOrNull; } List get primaryNodes { - return DB.instance.values(boxName: DB.boxNamePrimaryNodes); + return nodes.where((e) => e.isPrimary).toList(); } List get nodes { @@ -161,14 +166,15 @@ class NodeService extends ChangeNotifier { } List getNodesFor(CryptoCurrency coin) { - final list = DB.instance - .values(boxName: DB.boxNameNodeModels) - .where( - (e) => - e.coinName == coin.identifier && - !e.id.startsWith(DefaultNodes.defaultNodeIdPrefix), - ) - .toList(); + final list = + DB.instance + .values(boxName: DB.boxNameNodeModels) + .where( + (e) => + e.coinName == coin.identifier && + !e.id.startsWith(DefaultNodes.defaultNodeIdPrefix), + ) + .toList(); // add default to end of list list.addAll( @@ -191,25 +197,27 @@ class NodeService extends ChangeNotifier { } List failoverNodesFor({required CryptoCurrency currency}) { - return getNodesFor(currency) - .where((e) => e.isFailover && !e.isDown) - .toList(); + return getNodesFor( + currency, + ).where((e) => e.isFailover && !e.isDown).toList(); } - // should probably just combine this and edit into a save() func at some point - /// Over write node in hive if a node with existing id already exists. - /// Otherwise add node to hive - Future add( + /// Adds a new node to the database. + /// + /// If a node with the same ID already exists, it will be overwritten. + Future save( NodeModel node, String? password, bool shouldNotifyListeners, ) async { + // Save to database (logic from add()). await DB.instance.put( boxName: DB.boxNameNodeModels, key: node.id, value: node, ); + // Save password to secure storage. if (password != null) { await secureStorageInterface.write( key: "${node.id}_nodePW", @@ -224,9 +232,34 @@ class NodeService extends ChangeNotifier { } Future delete(String id, bool shouldNotifyListeners) async { - await DB.instance.delete(boxName: DB.boxNameNodeModels, key: id); + final nodeToDelete = getNodeById(id: id); + + if (nodeToDelete == null) { + // doesn't exist + Logging.instance.w( + "Attempted delete of a node model that does not exist", + ); + return; + } + + final coin = AppConfig.getCryptoCurrencyByPrettyName(nodeToDelete.coinName); + final remaining = getNodesFor(coin); + + if (remaining.length - 1 < 1) { + // doesn't exist + Logging.instance.w("Attempted delete the last remaining node for $coin"); + return; + } + remaining.retainWhere((e) => e.id != nodeToDelete.id); + + await DB.instance.delete(boxName: DB.boxNameNodeModels, key: id); await secureStorageInterface.delete(key: "${id}_nodePW"); + + if (nodeToDelete.isPrimary) { + await setPrimaryNodeFor(coin: coin, node: remaining.first); + } + if (shouldNotifyListeners) { notifyListeners(); } @@ -237,10 +270,8 @@ class NodeService extends ChangeNotifier { bool enabled, bool shouldNotifyListeners, ) async { - final model = DB.instance.get( - boxName: DB.boxNameNodeModels, - key: id, - )!; + final model = + DB.instance.get(boxName: DB.boxNameNodeModels, key: id)!; await DB.instance.put( boxName: DB.boxNameNodeModels, key: model.id, @@ -255,26 +286,6 @@ class NodeService extends ChangeNotifier { } } - /// convenience wrapper for add - Future edit( - NodeModel editedNode, - String? password, - bool shouldNotifyListeners, - ) async { - // check if the node being edited is the primary one; if it is, setPrimaryNodeFor coin - final coin = AppConfig.getCryptoCurrencyByPrettyName(editedNode.coinName); - final primaryNode = getPrimaryNodeFor(currency: coin); - if (primaryNode?.id == editedNode.id) { - await setPrimaryNodeFor( - coin: coin, - node: editedNode, - shouldNotifyListeners: true, - ); - } - - return add(editedNode, password, shouldNotifyListeners); - } - //============================================================================ Future updateCommunityNodes() async { @@ -284,10 +295,7 @@ class NodeService extends ChangeNotifier { final response = await client.post( uri, headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - "jsonrpc": "2.0", - "id": "0", - }), + body: jsonEncode({"jsonrpc": "2.0", "id": "0"}), ); final json = jsonDecode(response.body) as Map; @@ -312,6 +320,7 @@ class NodeService extends ChangeNotifier { torEnabled: nodeMap["torEnabled"] == "true", isDown: nodeMap["isDown"] == "true", clearnetEnabled: nodeMap["plainEnabled"] == "true", + isPrimary: nodeMap["plainEnabled"] == "true", ); final currentNode = getNodeById(id: nodeMap["id"] as String); if (currentNode != null) { @@ -326,7 +335,7 @@ class NodeService extends ChangeNotifier { trusted: node.trusted, ); } - await add(node, null, false); + await save(node, null, false); } } } catch (e, s) { diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 93e09aceb..4eca542e0 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -50,8 +50,9 @@ class NotificationsService extends ChangeNotifier { // watched transactions List get _watchedTransactionNotifications { - return DB.instance - .values(boxName: DB.boxNameWatchedTransactions); + return DB.instance.values( + boxName: DB.boxNameWatchedTransactions, + ); } Future _addWatchedTxNotification(NotificationModel notification) async { @@ -73,8 +74,9 @@ class NotificationsService extends ChangeNotifier { // watched trades List get _watchedChangeNowTradeNotifications { - return DB.instance - .values(boxName: DB.boxNameWatchedTrades); + return DB.instance.values( + boxName: DB.boxNameWatchedTrades, + ); } Future _addWatchedTradeNotification( @@ -127,8 +129,9 @@ class NotificationsService extends ChangeNotifier { void _checkTransactions() async { for (final notification in _watchedTransactionNotifications) { try { - final CryptoCurrency coin = - AppConfig.getCryptoCurrencyByPrettyName(notification.coinName); + final CryptoCurrency coin = AppConfig.getCryptoCurrencyByPrettyName( + notification.coinName, + ); final txid = notification.txid!; final wallet = Wallets.sharedInstance.getWallet(notification.walletId); @@ -156,20 +159,21 @@ class NotificationsService extends ChangeNotifier { torEnabled: node.torEnabled, clearnetEnabled: node.clearnetEnabled, ); - final failovers = nodeService - .failoverNodesFor(currency: coin) - .map( - (e) => ElectrumXNode( - address: e.host, - port: e.port, - name: e.name, - id: e.id, - useSSL: e.useSSL, - torEnabled: node.torEnabled, - clearnetEnabled: node.clearnetEnabled, - ), - ) - .toList(); + final failovers = + nodeService + .failoverNodesFor(currency: coin) + .map( + (e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + torEnabled: node.torEnabled, + clearnetEnabled: node.clearnetEnabled, + ), + ) + .toList(); final client = ElectrumXClient.from( node: eNode, @@ -193,13 +197,16 @@ class NotificationsService extends ChangeNotifier { // grab confirms string to compare final String newConfirms = "($confirmations/${wallet.cryptoCurrency.minConfirms})"; - final String oldConfirms = notification.title - .substring(notification.title.lastIndexOf("(")); + final String oldConfirms = notification.title.substring( + notification.title.lastIndexOf("("), + ); // only update if they don't match if (oldConfirms != newConfirms) { - final String newTitle = - notification.title.replaceFirst(oldConfirms, newConfirms); + final String newTitle = notification.title.replaceFirst( + oldConfirms, + newConfirms, + ); final updatedNotification = notification.copyWith( title: newTitle, @@ -230,8 +237,10 @@ class NotificationsService extends ChangeNotifier { for (final notification in _watchedChangeNowTradeNotifications) { final id = notification.changeNowId!; - final trades = - tradesService.trades.where((element) => element.tradeId == id); + final trades = tradesService.trades.where( + (element) => + element.tradeId == id && element.exchangeName != "Majestic Bank", + ); if (trades.isEmpty) { return; @@ -362,8 +371,11 @@ class NotificationsService extends ChangeNotifier { } Future markAsRead(int id, bool shouldNotifyListeners) async { - final model = DB.instance - .get(boxName: DB.boxNameNotifications, key: id)!; + final model = + DB.instance.get( + boxName: DB.boxNameNotifications, + key: id, + )!; await DB.instance.put( boxName: DB.boxNameNotifications, key: model.id, diff --git a/lib/services/price.dart b/lib/services/price.dart index 801e720e2..9641d0328 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -13,7 +13,6 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; -import 'package:tuple/tuple.dart'; import '../app_config.dart'; import '../db/hive/db.dart'; @@ -37,6 +36,7 @@ class PriceAPI { Epiccash: "epic-cash", Ecash: "ecash", Ethereum: "ethereum", + Fact0rn: "fact0rn", Firo: "zcoin", Monero: "monero", Particl: "particl", @@ -49,18 +49,21 @@ class PriceAPI { Nano: "nano", Banano: "banano", Xelis: "xelis", + Salvium: "salvium", }; static const refreshInterval = 60; // initialize to older than current time minus at least refreshInterval - static DateTime _lastCalled = - DateTime.now().subtract(const Duration(seconds: refreshInterval + 10)); + static DateTime _lastCalled = DateTime.now().subtract( + const Duration(seconds: refreshInterval + 10), + ); static String _lastUsedBaseCurrency = ""; - static const Duration refreshIntervalDuration = - Duration(seconds: refreshInterval); + static const Duration refreshIntervalDuration = Duration( + seconds: refreshInterval, + ); final HTTP client; @@ -72,7 +75,7 @@ class PriceAPI { } Future _updateCachedPrices( - Map> data, + Map data, ) async { final Map map = {}; @@ -81,30 +84,29 @@ class PriceAPI { if (entry == null) { map[coin.prettyName] = ["0", 0.0]; } else { - map[coin.prettyName] = [entry.item1.toString(), entry.item2]; + map[coin.prettyName] = [entry.value.toString(), entry.change24h]; } } - await DB.instance - .put(boxName: DB.boxNamePriceCache, key: 'cache', value: map); + await DB.instance.put( + boxName: DB.boxNamePriceCache, + key: 'cache', + value: map, + ); } - Map> get _cachedPrices { + Map get _cachedPrices { final map = DB.instance.get(boxName: DB.boxNamePriceCache, key: 'cache') - as Map? ?? - {}; + as Map? ?? + {}; // init with 0 - final result = { - for (final coin in AppConfig.coins) coin: Tuple2(Decimal.zero, 0.0), - }; + final Map result = {}; for (final entry in map.entries) { - result[AppConfig.getCryptoCurrencyByPrettyName( - entry.key as String, - )] = Tuple2( - Decimal.parse(entry.value[0] as String), - entry.value[1] as double, + result[AppConfig.getCryptoCurrencyByPrettyName(entry.key as String)] = ( + value: Decimal.parse(entry.value[0] as String), + change24h: entry.value[1] as double, ); } @@ -117,9 +119,8 @@ class PriceAPI { .where((e) => e != null) .join(","); - Future>> getPricesAnd24hChange({ - required String baseCurrency, - }) async { + Future> + getPricesAnd24hChange({required String baseCurrency}) async { final now = DateTime.now(); if (_lastUsedBaseCurrency != baseCurrency || now.difference(_lastCalled) > refreshIntervalDuration) { @@ -132,12 +133,10 @@ class PriceAPI { final externalCalls = Prefs.instance.externalCalls; if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return _cachedPrices; } - final Map> result = {}; + final Map result = {}; try { final uri = Uri.parse( "https://api.coingecko.com/api/v3/coins/markets?vs_currency" @@ -148,9 +147,10 @@ class PriceAPI { final coinGeckoResponse = await client.get( url: uri, headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List; @@ -159,12 +159,17 @@ class PriceAPI { final String coinName = map["name"] as String; final coin = AppConfig.getCryptoCurrencyByPrettyName(coinName); - final price = Decimal.parse(map["current_price"].toString()); - final change24h = map["price_change_percentage_24h"] != null - ? double.parse(map["price_change_percentage_24h"].toString()) - : 0.0; - - result[coin] = Tuple2(price, change24h); + try { + final price = Decimal.parse(map["current_price"].toString()); + final change24h = + map["price_change_percentage_24h"] != null + ? double.parse(map["price_change_percentage_24h"].toString()) + : 0.0; + + result[coin] = (value: price, change24h: change24h); + } catch (_) { + result.remove(coin); + } } // update cache @@ -172,8 +177,11 @@ class PriceAPI { return _cachedPrices; } catch (e, s) { - Logging.instance - .e("getPricesAnd24hChange($baseCurrency): ", error: e, stackTrace: s); + Logging.instance.e( + "getPricesAnd24hChange($baseCurrency): ", + error: e, + stackTrace: s, + ); // return previous cached values return _cachedPrices; } @@ -185,9 +193,7 @@ class PriceAPI { if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return null; } const uriString = @@ -197,9 +203,10 @@ class PriceAPI { final response = await client.get( url: uri, headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final json = jsonDecode(response.body) as List; @@ -214,22 +221,22 @@ class PriceAPI { } } - Future>> - getPricesAnd24hChangeForEthTokens({ + Future> + getPricesAnd24hChangeForEthTokens({ required Set contractAddresses, required String baseCurrency, }) async { - final Map> tokenPrices = {}; + final Map tokenPrices = {}; if (AppConfig.coins.whereType().isEmpty || - contractAddresses.isEmpty) return tokenPrices; + contractAddresses.isEmpty) { + return tokenPrices; + } final externalCalls = Prefs.instance.externalCalls; if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return tokenPrices; } diff --git a/lib/services/price_service.dart b/lib/services/price_service.dart index fb30c8477..43eb68e32 100644 --- a/lib/services/price_service.dart +++ b/lib/services/price_service.dart @@ -13,13 +13,12 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; + import '../db/isar/main_db.dart'; import '../models/isar/models/isar_models.dart'; import '../networking/http.dart'; -import 'price.dart'; -import '../app_config.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; -import 'package:tuple/tuple.dart'; +import 'price.dart'; class PriceService extends ChangeNotifier { late final String baseTicker; @@ -29,25 +28,26 @@ class PriceService extends ChangeNotifier { final Duration updateInterval = const Duration(seconds: 60); Timer? _timer; - final Map> _cachedPrices = { - for (final coin in AppConfig.coins) coin: Tuple2(Decimal.zero, 0.0), - }; + final Map _cachedPrices = + {}; - final Map> _cachedTokenPrices = {}; + final Map _cachedTokenPrices = + {}; final _priceAPI = PriceAPI(HTTP()); - Tuple2 getPrice(CryptoCurrency coin) => _cachedPrices[coin]!; + ({Decimal value, double change24h})? getPrice(CryptoCurrency coin) => + _cachedPrices[coin]; - Tuple2 getTokenPrice(String contractAddress) => - _cachedTokenPrices[contractAddress.toLowerCase()] ?? - Tuple2(Decimal.zero, 0); + ({Decimal value, double change24h})? getTokenPrice(String contractAddress) => + _cachedTokenPrices[contractAddress.toLowerCase()]; PriceService(this.baseTicker); Future updatePrice() async { - final priceMap = - await _priceAPI.getPricesAnd24hChange(baseCurrency: baseTicker); + final priceMap = await _priceAPI.getPricesAnd24hChange( + baseCurrency: baseTicker, + ); bool shouldNotify = false; for (final map in priceMap.entries) { diff --git a/lib/services/spark_names_service.dart b/lib/services/spark_names_service.dart new file mode 100644 index 000000000..9a9a4aa96 --- /dev/null +++ b/lib/services/spark_names_service.dart @@ -0,0 +1,126 @@ +import 'package:mutex/mutex.dart'; + +import '../utilities/logger.dart'; +import '../wallets/crypto_currency/crypto_currency.dart'; + +class _BiMap { + final Map _byKey = {}; + final Map _byValue = {}; + + _BiMap(); + + void addAll(Map other) { + for (final e in other.entries) { + add(e.key, e.value); + } + } + + void add(K key, V value) { + _byKey[key] = value; + _byValue[value] = key; + } + + void clear() { + _byValue.clear(); + _byKey.clear(); + } + + K? getByValue(V value) => _byValue[value]; + V? getByKey(K key) => _byKey[key]; +} + +/// Basic service to track all spark names on test net and main net. +/// Data is currently stored in memory only. +abstract final class SparkNamesService { + static final _lock = { + CryptoCurrencyNetwork.main: Mutex(), + CryptoCurrencyNetwork.test: Mutex(), + }; + + static const _minUpdateInterval = Duration(seconds: 10); + static DateTime _lastUpdated = DateTime(2000); // some default + + static final _cache = { + // key is address, uppercase name is value + CryptoCurrencyNetwork.main: _BiMap(), + CryptoCurrencyNetwork.test: _BiMap(), + }; + + static final _nameMap = { + // key is uppercase, value is as entered + CryptoCurrencyNetwork.main: {}, + CryptoCurrencyNetwork.test: {}, + }; + + /// Get the address for the given spark name. + static Future getAddressFor( + String name, { + CryptoCurrencyNetwork network = CryptoCurrencyNetwork.main, + }) async { + if (_cache[network] == null) { + throw UnsupportedError( + "CryptoCurrencyNetwork \"${network.name}\" is not currently allowed.", + ); + } + + return await _lock[network]!.protect( + () async => _cache[network]?.getByValue(name.toUpperCase()), + ); + } + + /// Get the name for the given spark address. + static Future getNameFor( + String address, { + CryptoCurrencyNetwork network = CryptoCurrencyNetwork.main, + }) async { + if (_cache[network] == null) { + throw UnsupportedError( + "CryptoCurrencyNetwork \"${network.name}\" is not currently allowed.", + ); + } + + return await _lock[network]!.protect( + () async => _nameMap[network]![_cache[network]?.getByKey(address)], + ); + } + + static Future update( + List<({String name, String address})> names, { + CryptoCurrencyNetwork network = CryptoCurrencyNetwork.main, + }) async { + Logging.instance.t("SparkNamesService.update called"); + if (_cache[network] == null) { + throw UnsupportedError( + "CryptoCurrencyNetwork \"${network.name}\" is not currently allowed.", + ); + } + + final now = DateTime.now(); + if (now.difference(_lastUpdated) > _minUpdateInterval) { + _lastUpdated = now; + } else { + Logging.instance.t( + "SparkNamesService.update called too soon. Returning early.", + ); + // too soon, return; + return; + } + + await _lock[network]!.protect(() async { + Logging.instance.t( + "SparkNamesService.update lock acquired and updating cache", + ); + _cache[network]!.clear(); + _nameMap[network]!.clear(); + + for (final pair in names) { + final upperName = pair.name.toUpperCase(); + _nameMap[network]![upperName] = pair.name; + + _cache[network]!.add(pair.address, upperName); + } + + Logging.instance.t("SparkNamesService.update updating cache complete"); + }); + } +} diff --git a/lib/services/trade_service.dart b/lib/services/trade_service.dart index 6207b102e..e15216a33 100644 --- a/lib/services/trade_service.dart +++ b/lib/services/trade_service.dart @@ -38,8 +38,11 @@ class TradesService extends ChangeNotifier { required Trade trade, required bool shouldNotifyListeners, }) async { - await DB.instance - .put(boxName: DB.boxNameTradesV2, key: trade.uuid, value: trade); + await DB.instance.put( + boxName: DB.boxNameTradesV2, + key: trade.uuid, + value: trade, + ); if (shouldNotifyListeners) { notifyListeners(); diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 07856e6ee..7b5f9a376 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -9,6 +9,7 @@ */ import 'dart:async'; +import 'dart:io'; import 'package:compat/compat.dart' as lib_monero_compat; import 'package:isar/isar.dart'; @@ -22,9 +23,11 @@ import '../utilities/logger.dart'; import '../utilities/prefs.dart'; import '../utilities/stack_file_system.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; +import '../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../wallets/isar/models/wallet_info.dart'; import '../wallets/wallet/impl/epiccash_wallet.dart'; import '../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../wallets/wallet/wallet.dart'; import 'event_bus/events/wallet_added_event.dart'; import 'event_bus/global_event_bus.dart'; @@ -67,6 +70,38 @@ class Wallets { } } + Future _deleteCryptonoteWalletFilesHelper(WalletInfo info) async { + final walletId = info.walletId; + if (info.coin is Wownero) { + await lib_monero_compat.deleteWalletFiles( + name: walletId, + type: lib_monero_compat.WalletType.wownero, + appRoot: await StackFileSystem.applicationRootDirectory(), + ); + Logging.instance.d("Wownero wallet: $walletId deleted"); + } else if (info.coin is Monero) { + await lib_monero_compat.deleteWalletFiles( + name: walletId, + type: lib_monero_compat.WalletType.monero, + appRoot: await StackFileSystem.applicationRootDirectory(), + ); + Logging.instance.d("Monero wallet: $walletId deleted"); + } else if (info.coin is Salvium) { + final path = await salviumWalletDir( + walletId: walletId, + appRoot: await StackFileSystem.applicationRootDirectory(), + ); + final file = Directory(path); + final isExist = file.existsSync(); + + if (isExist) { + await file.delete(recursive: true); + } + } else { + throw Exception("Not a valid CN coin"); + } + } + Future deleteWallet( WalletInfo info, SecureStorageInterface secureStorage, @@ -87,20 +122,8 @@ class Wallets { key: Wallet.getViewOnlyWalletDataSecStoreKey(walletId: walletId), ); - if (info.coin is Wownero) { - await lib_monero_compat.deleteWalletFiles( - name: walletId, - type: lib_monero_compat.WalletType.wownero, - appRoot: await StackFileSystem.applicationRootDirectory(), - ); - Logging.instance.d("monero wallet: $walletId deleted"); - } else if (info.coin is Monero) { - await lib_monero_compat.deleteWalletFiles( - name: walletId, - type: lib_monero_compat.WalletType.monero, - appRoot: await StackFileSystem.applicationRootDirectory(), - ); - Logging.instance.d("monero wallet: $walletId deleted"); + if (info.coin is CryptonoteCurrency) { + await _deleteCryptonoteWalletFilesHelper(info); } else if (info.coin is Epiccash) { final deleteResult = await deleteEpicWallet( walletId: walletId, @@ -152,10 +175,10 @@ class Wallets { } } - Future load(Prefs prefs, MainDB mainDB) async { + Future load(Prefs prefs, MainDB mainDB, bool isDuress) async { // return await _loadV1(prefs, mainDB); // return await _loadV2(prefs, mainDB); - return await _loadV3(prefs, mainDB); + return await _loadV3(prefs, mainDB, isDuress); } Future _loadV1(Prefs prefs, MainDB mainDB) async { @@ -165,18 +188,21 @@ class Wallets { 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)) { + for (final walletId in DB.instance.values( + boxName: DB.boxNameWalletsToDeleteOnStart, + )) { await mainDB.isar.writeTxn( - () async => await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .deleteAll(), + () async => + await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll(), ); } // clear list - await DB.instance - .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + await DB.instance.deleteAll( + boxName: DB.boxNameWalletsToDeleteOnStart, + ); final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); if (walletInfoList.isEmpty) { @@ -204,9 +230,10 @@ class Wallets { for (final walletInfo in walletInfoList) { try { final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); - Logging.instance - .d("LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " - "IS VERIFIED: $isVerified"); + Logging.instance.d( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + ); if (isVerified) { // TODO: integrate this into the new wallets somehow? @@ -222,10 +249,11 @@ class Wallets { prefs: prefs, ); - final shouldSetAutoSync = shouldAutoSyncAll || + final shouldSetAutoSync = + shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(walletInfo.walletId); - if (wallet is LibMoneroWallet) { + if (wallet is LibMoneroWallet || wallet is LibSalviumWallet) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add( @@ -269,18 +297,21 @@ class Wallets { 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)) { + for (final walletId in DB.instance.values( + boxName: DB.boxNameWalletsToDeleteOnStart, + )) { await mainDB.isar.writeTxn( - () async => await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .deleteAll(), + () async => + await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll(), ); } // clear list - await DB.instance - .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + await DB.instance.deleteAll( + boxName: DB.boxNameWalletsToDeleteOnStart, + ); final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); if (walletInfoList.isEmpty) { @@ -309,9 +340,10 @@ class Wallets { for (final walletInfo in walletInfoList) { try { final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); - Logging.instance - .d("LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " - "IS VERIFIED: $isVerified"); + Logging.instance.d( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + ); if (isVerified) { // TODO: integrate this into the new wallets somehow? @@ -330,7 +362,7 @@ class Wallets { nodeService: nodeService, prefs: prefs, ).then((wallet) { - if (wallet is LibMoneroWallet) { + if (wallet is LibMoneroWallet || wallet is LibSalviumWallet) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); walletIdCompleter.complete("dummy_ignore"); @@ -345,11 +377,7 @@ class Wallets { deleteFutures.add(_deleteWallet(walletInfo.walletId)); } } catch (e, s) { - Logging.instance.w( - "$e $s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e $s", error: e, stackTrace: s); continue; } } @@ -357,17 +385,17 @@ class Wallets { 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(); + 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( @@ -387,34 +415,43 @@ class Wallets { } /// should be best performance - Future _loadV3(Prefs prefs, MainDB mainDB) async { + Future _loadV3(Prefs prefs, MainDB mainDB, bool isDuress) 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)) { + for (final walletId in DB.instance.values( + boxName: DB.boxNameWalletsToDeleteOnStart, + )) { await mainDB.isar.writeTxn( - () async => await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .deleteAll(), + () 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() - .filter() - .anyOf( - AppConfig.coins.map((e) => e.identifier), - (q, element) => q.coinNameMatches(element), - ) - .findAll(); + await DB.instance.deleteAll( + boxName: DB.boxNameWalletsToDeleteOnStart, + ); + + final walletInfoList = + await mainDB.isar.walletInfo + .where() + .filter() + .anyOf( + AppConfig.coins.map((e) => e.identifier), + (q, element) => q.coinNameMatches(element), + ) + .findAll(); + + if (isDuress) { + walletInfoList.retainWhere((e) => e.isDuressVisible); + } + if (walletInfoList.isEmpty) { return; } @@ -441,9 +478,10 @@ class Wallets { for (final walletInfo in walletInfoList) { try { final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); - Logging.instance - .d("LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " - "IS VERIFIED: $isVerified"); + Logging.instance.d( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + ); if (isVerified) { // TODO: integrate this into the new wallets somehow? @@ -462,7 +500,7 @@ class Wallets { nodeService: nodeService, prefs: prefs, ).then((wallet) { - if (wallet is LibMoneroWallet) { + if (wallet is LibMoneroWallet || wallet is LibSalviumWallet) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); walletIdCompleter.complete("dummy_ignore"); @@ -477,11 +515,7 @@ class Wallets { deleteFutures.add(_deleteWallet(walletInfo.walletId)); } } catch (e, s) { - Logging.instance.w( - "$e $s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e $s", error: e, stackTrace: s); continue; } } @@ -490,24 +524,21 @@ class Wallets { 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(); + 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.d( - "Initial refresh start: ${start.toUtc()}", - ); + Logging.instance.d("Initial refresh start: ${start.toUtc()}"); const groupCount = 3; for (int i = 0; i < idsToRefresh.length; i += groupCount) { final List> futures = []; @@ -515,14 +546,13 @@ class Wallets { if (i + j >= idsToRefresh.length) { break; } - futures.add( - _wallets[idsToRefresh[i + j]]!.refresh(), - ); + futures.add(_wallets[idsToRefresh[i + j]]!.refresh()); } await Future.wait(futures); } - Logging.instance - .d("Initial refresh duration: ${DateTime.now().difference(start)}"); + Logging.instance.d( + "Initial refresh duration: ${DateTime.now().difference(start)}", + ); } if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { @@ -530,15 +560,13 @@ class Wallets { Future.wait([ _initLinearly(walletsToInitLinearly), ...walletInitFutures, - ]).then( - (value) => _refreshFutures(idsToRefresh), - ), + ]).then((value) => _refreshFutures(idsToRefresh)), ); } else if (walletInitFutures.isNotEmpty) { unawaited( - Future.wait(walletInitFutures).then( - (value) => _refreshFutures(idsToRefresh), - ), + Future.wait( + walletInitFutures, + ).then((value) => _refreshFutures(idsToRefresh)), ); } else if (walletsToInitLinearly.isNotEmpty) { unawaited(_initLinearly(walletsToInitLinearly)); @@ -578,11 +606,12 @@ class Wallets { ); if (isVerified) { - final shouldSetAutoSync = shouldAutoSyncAll || + final shouldSetAutoSync = + shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(wallet.walletId); if (isDesktop) { - if (wallet is LibMoneroWallet) { + if (wallet is LibMoneroWallet || wallet is LibSalviumWallet) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add( diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index 542c6375e..3479f7ecf 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -31,7 +31,7 @@ final pThemeService = Provider((ref) { }); class ThemeService { - static const _currentDefaultThemeVersion = 17; + static const _currentDefaultThemeVersion = 18; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._(); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 171dc09b2..9e63370e1 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -33,8 +33,9 @@ class AddressUtils { /// Return only recognized parameters. static Map _filterParams(Map params) { return Map.fromEntries( - params.entries - .where((entry) => recognizedParams.contains(entry.key.toLowerCase())), + params.entries.where( + (entry) => recognizedParams.contains(entry.key.toLowerCase()), + ), ); } @@ -52,8 +53,10 @@ class AddressUtils { result["address"] = u.path; } else if (result["scheme"] == "monero") { // Monero addresses can contain '?' which Uri.parse interprets as query start. - final addressEnd = - uri.indexOf('?', 7); // 7 is the length of "monero:". + final addressEnd = uri.indexOf( + '?', + 7, + ); // 7 is the length of "monero:". if (addressEnd != -1) { result["address"] = uri.substring(7, addressEnd); } else { @@ -130,10 +133,21 @@ class AddressUtils { /// Centralized method to handle various cryptocurrency URIs and return a common object. /// /// Returns null on failure to parse - static PaymentUriData? parsePaymentUri( - String uri, { - Logging? logging, - }) { + static PaymentUriData? parsePaymentUri(String uri, {Logging? logging}) { + // hacky check its not just a bcash, ecash, or xel address + final parts = uri.split(":"); + if (parts.length == 2) { + if ([ + "xel", + "bitcoincash", + "bchtest", + "ecash", + "ectest", + ].contains(parts.first.toLowerCase())) { + return null; + } + } + try { final Map parsedData = _parseUri(uri); @@ -155,7 +169,7 @@ class AddressUtils { additionalParams: filteredParams, ); } catch (e, s) { - logging?.e("", error: e, stackTrace: s); + logging?.i("Invalid payment URI: $uri", error: e, stackTrace: s); return null; } } @@ -229,7 +243,7 @@ class AddressUtils { } /// Formats an address string to remove any unnecessary prefixes or suffixes. - String formatAddress(String epicAddress) { + String formatEpicCashAddress(String epicAddress) { // strip http:// or https:// prefixes if the address contains an @ symbol (and is thus an epicbox address) if ((epicAddress.startsWith("http://") || epicAddress.startsWith("https://")) && @@ -259,8 +273,8 @@ class PaymentUriData { final Map additionalParams; CryptoCurrency? get coin => AddressUtils._getCryptoCurrencyByScheme( - scheme ?? "", // empty will just return null - ); + scheme ?? "", // empty will just return null + ); PaymentUriData({ required this.address, @@ -273,7 +287,8 @@ class PaymentUriData { }); @override - String toString() => "PaymentUriData { " + String toString() => + "PaymentUriData { " "coin: $coin, " "address: $address, " "amount: $amount, " diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 73c520410..d14cdc38c 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -11,7 +11,6 @@ import 'package:flutter/material.dart'; import '../services/exchange/change_now/change_now_exchange.dart'; -import '../services/exchange/majestic_bank/majestic_bank_exchange.dart'; import '../services/exchange/nanswap/nanswap_exchange.dart'; import '../services/exchange/simpleswap/simpleswap_exchange.dart'; import '../services/exchange/trocador/trocador_exchange.dart'; @@ -33,7 +32,7 @@ class _SOCIALS { String get discord => "${_path}discord.svg"; String get reddit => "${_path}reddit-alien-brands.svg"; - String get twitter => "${_path}twitter-brands.svg"; + String get twitter => "${_path}x.svg"; String get telegram => "${_path}telegram-brands.svg"; } @@ -44,8 +43,8 @@ class _EXCHANGE { String get changeNow => "${_path}change_now_logo_1.svg"; String get simpleSwap => "${_path}simpleswap-icon.svg"; - String get majesticBankBlue => "${_path}mb_blue.svg"; - String get majesticBankGreen => "${_path}mb_green.svg"; + // String get majesticBankBlue => "${_path}mb_blue.svg"; + // String get majesticBankGreen => "${_path}mb_green.svg"; String get trocador => "${_path}trocador.svg"; String get nanswap => "${_path}nanswap.svg"; @@ -55,15 +54,17 @@ class _EXCHANGE { return simpleSwap; case ChangeNowExchange.exchangeName: return changeNow; - case MajesticBankExchange.exchangeName: - return majesticBankBlue; + // case MajesticBankExchange.exchangeName: + // return majesticBankBlue; case TrocadorExchange.exchangeName: return trocador; case NanswapExchange.exchangeName: return nanswap; default: - throw ArgumentError("Invalid exchange name passed to " - "Assets.exchange.getIconFor()"); + throw ArgumentError( + "Invalid exchange name passed to " + "Assets.exchange.getIconFor()", + ); } } } @@ -232,7 +233,7 @@ class _SVG { String get trocadorRatingC => "assets/svg/trocador_rating_c.svg"; String get trocadorRatingD => "assets/svg/trocador_rating_d.svg"; -// TODO provide proper assets + // TODO provide proper assets String get bitcoinTestnet => "assets/svg/coin_icons/Bitcoin.svg"; String get bitcoincashTestnet => "assets/svg/coin_icons/Bitcoincash.svg"; String get firoTestnet => "assets/svg/coin_icons/Firo.svg"; diff --git a/lib/utilities/barcode_scanner_interface.dart b/lib/utilities/barcode_scanner_interface.dart index 77df4904f..f8256f2e5 100644 --- a/lib/utilities/barcode_scanner_interface.dart +++ b/lib/utilities/barcode_scanner_interface.dart @@ -8,22 +8,94 @@ * */ -import 'package:barcode_scan2/barcode_scan2.dart'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../widgets/desktop/primary_button.dart'; +import '../widgets/desktop/secondary_button.dart'; +import '../widgets/qr_scanner.dart'; +import '../widgets/stack_dialog.dart'; +import 'logger.dart'; + +class ScanResult { + final String rawContent; + + ScanResult({required this.rawContent}); +} abstract class BarcodeScannerInterface { - Future scan({ScanOptions options = const ScanOptions()}); + Future scan({required BuildContext context}); } class BarcodeScannerWrapper implements BarcodeScannerInterface { const BarcodeScannerWrapper(); @override - Future scan({ScanOptions options = const ScanOptions()}) async { + Future scan({required BuildContext context}) async { try { - final result = await BarcodeScanner.scan(options: options); - return result; + final data = await showDialog( + context: context, + builder: (context) => const QrScanner(), + ); + + return ScanResult(rawContent: data.toString()); } catch (e) { rethrow; } } } + +/// Check if cam perms permanently denied on mobile and open app settings +Future checkCamPermDeniedMobileAndOpenAppSettings( + BuildContext context, { + required Logging logging, +}) async { + if (Platform.isAndroid || Platform.isIOS) { + final status = await Permission.camera.status; + final androidShow = + Platform.isAndroid && status == PermissionStatus.permanentlyDenied; + final iosShow = Platform.isIOS && status == PermissionStatus.denied; + + if ((iosShow || androidShow) && context.mounted) { + final trySettings = await showDialog( + context: context, + builder: + (context) => StackDialog( + title: "Camera permissions required", + message: "Open settings?", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + rightButton: PrimaryButton( + label: "Continue", + onPressed: () => Navigator.of(context).pop(true), + ), + ), + ); + + if (trySettings == true) { + final success = await openAppSettings(); + if (!success) { + logging.e("Failed to open app settings"); + if (context.mounted) { + await showDialog( + context: context, + builder: + (context) => StackDialog( + title: "Could not open app settings", + message: "You will need manually go find your app settings", + rightButton: PrimaryButton( + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ); + } + } + } + } + } +} diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 1c3b908a1..69205f1f0 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -40,7 +40,7 @@ abstract class Constants { // Enable Logger.print statements static const bool disableLogger = false; - static const int currentDataVersion = 14; + static const int currentDataVersion = 15; static const int rescanV1 = 1; diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index c105412db..3bb8557de 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -12,36 +12,45 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; import "package:hex/hex.dart"; + import '../wallets/crypto_currency/crypto_currency.dart'; class GasTracker { + final Decimal low; final Decimal average; - final Decimal fast; - final Decimal slow; + final Decimal high; final int numberOfBlocksFast; final int numberOfBlocksAverage; final int numberOfBlocksSlow; + final Decimal suggestBaseFee; + final String lastBlock; const GasTracker({ + required this.low, required this.average, - required this.fast, - required this.slow, + required this.high, required this.numberOfBlocksFast, required this.numberOfBlocksAverage, required this.numberOfBlocksSlow, + required this.suggestBaseFee, required this.lastBlock, }); + Decimal get lowPriority => (low - suggestBaseFee).zeroIfNegative; + Decimal get averagePriority => (average - suggestBaseFee).zeroIfNegative; + Decimal get highPriority => (high - suggestBaseFee).zeroIfNegative; + factory GasTracker.fromJson(Map json) { final targetTime = Ethereum(CryptoCurrencyNetwork.main).targetBlockTimeSeconds; return GasTracker( - fast: Decimal.parse(json["FastGasPrice"].toString()), + high: Decimal.parse(json["FastGasPrice"].toString()), average: Decimal.parse(json["ProposeGasPrice"].toString()), - slow: Decimal.parse(json["SafeGasPrice"].toString()), + low: Decimal.parse(json["SafeGasPrice"].toString()), + suggestBaseFee: Decimal.parse(json["suggestBaseFee"].toString()), // TODO fix hardcoded numberOfBlocksFast: 30 ~/ targetTime, numberOfBlocksAverage: 180 ~/ targetTime, @@ -49,10 +58,34 @@ class GasTracker { lastBlock: json["LastBlock"] as String, ); } + + @override + String toString() { + return 'GasTracker(' + 'slow: $low, ' + 'average: $average, ' + 'fast: $high, ' + 'suggestBaseFee: $suggestBaseFee, ' + 'numberOfBlocksFast: $numberOfBlocksFast, ' + 'numberOfBlocksAverage: $numberOfBlocksAverage, ' + 'numberOfBlocksSlow: $numberOfBlocksSlow, ' + 'lastBlock: $lastBlock' + ')'; + } +} + +extension DecimalExt on Decimal { + Decimal get zeroIfNegative { + if (this < Decimal.zero) return Decimal.zero; + return this; + } } const hdPathEthereum = "m/44'/60'/0'/0"; +const kEthereumMinGasLimit = 21000; +const kEthereumTokenMinGasLimit = 65000; + // equal to "0x${keccak256("Transfer(address,address,uint256)".toUint8ListFromUtf8).toHex}"; const kTransferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; diff --git a/lib/utilities/git_status.dart b/lib/utilities/git_status.dart index 0a3f081a6..c89cf2439 100644 --- a/lib/utilities/git_status.dart +++ b/lib/utilities/git_status.dart @@ -2,9 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_libepiccash/git_versions.dart' as epic_versions; -// import 'package:flutter_libmonero/git_versions.dart' as monero_versions; import 'package:http/http.dart'; -import 'package:lelantus/git_versions.dart' as firo_versions; import '../../../themes/stack_colors.dart'; import '../../../utilities/logger.dart'; @@ -18,37 +16,11 @@ const kGithubHead = "/repos"; enum CommitStatus { isHead, isOldCommit, notACommit, notLoaded } abstract class GitStatus { - static String get firoCommit => firo_versions.getPluginVersion(); static String get epicCashCommit => epic_versions.getPluginVersion(); // static String get moneroCommit => monero_versions.getPluginVersion(); static String get appCommitHash => AppConfig.commitHash; - static CommitStatus? _cachedFiroStatus; - static Future getFiroCommitStatus() async { - if (_cachedFiroStatus != null) { - return _cachedFiroStatus!; - } - - final List results = await Future.wait([ - _doesCommitExist("cypherstack", "flutter_liblelantus", firoCommit), - _isHeadCommit("cypherstack", "flutter_liblelantus", "main", firoCommit), - ]); - - final commitExists = results[0]; - final commitIsHead = results[1]; - - if (commitExists && commitIsHead) { - _cachedFiroStatus = CommitStatus.isHead; - } else if (commitExists) { - _cachedFiroStatus = CommitStatus.isOldCommit; - } else { - _cachedFiroStatus = CommitStatus.notACommit; - } - - return _cachedFiroStatus!; - } - static CommitStatus? _cachedEpicStatus; static Future getEpicCommitStatus() async { if (_cachedEpicStatus != null) { @@ -78,59 +50,24 @@ abstract class GitStatus { return _cachedEpicStatus!; } - // - // static CommitStatus? _cachedMoneroStatus; - // static Future getMoneroCommitStatus() async { - // if (_cachedMoneroStatus != null) { - // return _cachedMoneroStatus!; - // } - // - // final List results = await Future.wait([ - // _doesCommitExist("cypherstack", "flutter_libmonero", moneroCommit), - // _isHeadCommit("cypherstack", "flutter_libmonero", "main", moneroCommit), - // ]); - // - // final commitExists = results[0]; - // final commitIsHead = results[1]; - // - // if (commitExists && commitIsHead) { - // _cachedMoneroStatus = CommitStatus.isHead; - // } else if (commitExists) { - // _cachedMoneroStatus = CommitStatus.isOldCommit; - // } else { - // _cachedMoneroStatus = CommitStatus.notACommit; - // } - // - // return _cachedMoneroStatus!; - // } static TextStyle styleForStatus(CommitStatus status, BuildContext context) { final Color color; switch (status) { case CommitStatus.isHead: - color = Theme.of( - context, - ).extension()!.accentColorGreen; + color = Theme.of(context).extension()!.accentColorGreen; break; case CommitStatus.isOldCommit: - color = Theme.of( - context, - ).extension()!.accentColorYellow; + color = Theme.of(context).extension()!.accentColorYellow; break; case CommitStatus.notACommit: - color = Theme.of( - context, - ).extension()!.accentColorRed; + color = Theme.of(context).extension()!.accentColorRed; break; default: - return STextStyles.itemSubtitle( - context, - ); + return STextStyles.itemSubtitle(context); } - return STextStyles.itemSubtitle( - context, - ).copyWith(color: color); + return STextStyles.itemSubtitle(context).copyWith(color: color); } static Future _doesCommitExist( diff --git a/lib/utilities/idle_monitor.dart b/lib/utilities/idle_monitor.dart new file mode 100644 index 000000000..554f78c2a --- /dev/null +++ b/lib/utilities/idle_monitor.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class IdleMonitor with WidgetsBindingObserver { + final Duration timeout; + final VoidCallback onIdle; + + final WidgetsBinding binding = WidgetsBinding.instance; + + IdleMonitor({required this.timeout, required this.onIdle}); + + Timer? _idleTimer; + bool _isAttached = false; + void Function(PointerDataPacket)? _prevPointerHandler; + KeyEventCallback? _keyboardHandler; + + void attach() { + if (_isAttached) return; + _isAttached = true; + _resetTimer(); + _prevPointerHandler = binding.platformDispatcher.onPointerDataPacket; + binding.platformDispatcher.onPointerDataPacket = (packet) { + _onUserActivity(); + _prevPointerHandler?.call(packet); + }; + _keyboardHandler = (event) { + _onUserActivity(); + return false; + }; + binding.keyboard.addHandler(_keyboardHandler!); + binding.addObserver(this); + } + + void detach() { + if (!_isAttached) return; + _isAttached = false; + binding.platformDispatcher.onPointerDataPacket = _prevPointerHandler; + if (_keyboardHandler != null) { + binding.keyboard.removeHandler(_keyboardHandler!); + } + binding.removeObserver(this); + _cancelTimer(); + } + + void _onUserActivity() { + _resetTimer(); + } + + void _resetTimer() { + _cancelTimer(); + _idleTimer = Timer(timeout, onIdle); + } + + void _cancelTimer() { + _idleTimer?.cancel(); + _idleTimer = null; + } + + // @override + // void didChangeAppLifecycleState(AppLifecycleState state) { + // if (!_isAttached) return; + // if (state == AppLifecycleState.paused || + // state == AppLifecycleState.inactive || + // state == AppLifecycleState.detached) { + // _cancelTimer(); + // } else if (state == AppLifecycleState.resumed) { + // _resetTimer(); + // } + // } +} diff --git a/lib/utilities/logger.dart b/lib/utilities/logger.dart index f62b1ea4e..d4cc03dcf 100644 --- a/lib/utilities/logger.dart +++ b/lib/utilities/logger.dart @@ -53,87 +53,79 @@ class Logging { SendPort get _sendPort { final port = IsolateNameServer.lookupPortByName(_kLoggerPortName); if (port == null) { - throw Exception( - "Did you forget to call Logging.initialize()?", - ); + throw Exception("Did you forget to call Logging.initialize()?"); } return port; } - Future initialize(String logsPath, {required Level level}) async { + Future initialize( + String logsPath, { + required Level level, + Level? debugConsoleLevel, + }) async { if (Isolate.current.debugName != "main") { throw Exception( "Logging.initialize() must be called on the main isolate.", ); } if (IsolateNameServer.lookupPortByName(_kLoggerPortName) != null) { - throw Exception( - "Logging was already initialized", - ); + throw Exception("Logging was already initialized"); } logsDirPath = logsPath; final receivePort = ReceivePort(); - await Isolate.spawn( - (sendPort) { - final ReceivePort receivePort = ReceivePort(); - sendPort.send(receivePort.sendPort); + await Isolate.spawn((sendPort) { + final ReceivePort receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + + PrettyPrinter prettyPrinter(bool toFile) => PrettyPrinter( + printEmojis: false, + methodCount: 0, + dateTimeFormat: + toFile ? DateTimeFormat.none : DateTimeFormat.dateAndTime, + colors: !toFile, + noBoxingByDefault: toFile, + ); - PrettyPrinter prettyPrinter(bool toFile) => PrettyPrinter( - printEmojis: false, - methodCount: 0, - dateTimeFormat: - toFile ? DateTimeFormat.none : DateTimeFormat.dateAndTime, - colors: !toFile, - noBoxingByDefault: toFile, - ); + final consoleLogger = Logger( + printer: PrefixPrinter(prettyPrinter(false)), + filter: ProductionFilter(), + level: debugConsoleLevel ?? level, + ); - final consoleLogger = Logger( - printer: PrefixPrinter(prettyPrinter(false)), - filter: ProductionFilter(), - level: level, - ); + final fileLogger = Logger( + printer: PrefixPrinter(prettyPrinter(true)), + filter: ProductionFilter(), + level: level, + output: AdvancedFileOutput( + path: logsDirPath, + overrideExisting: false, + latestFileName: "latest.txt", + writeImmediately: [Level.error, Level.fatal, Level.warning], + ), + ); - final fileLogger = Logger( - printer: PrefixPrinter(prettyPrinter(true)), - filter: ProductionFilter(), - level: level, - output: AdvancedFileOutput( - path: logsDirPath, - overrideExisting: false, - latestFileName: "latest.txt", - writeImmediately: [ - Level.error, - Level.fatal, - Level.warning, - Level.trace, // mainly for spark debugging. TODO: Remove later - ], - ), + receivePort.listen((message) { + final event = (message as (LogEvent, bool)).$1; + consoleLogger.log( + event.level, + event.message, + stackTrace: event.stackTrace, + error: event.error, + time: event.time.toUtc(), ); - - receivePort.listen((message) { - final event = (message as (LogEvent, bool)).$1; - consoleLogger.log( + if (message.$2) { + fileLogger.log( event.level, - event.message, + "${event.time.toUtc().toIso8601String()} ${event.message}", stackTrace: event.stackTrace, error: event.error, - time: event.time.toUtc(), + time: event.time, ); - if (message.$2) { - fileLogger.log( - event.level, - "${event.time.toUtc().toIso8601String()} ${event.message}", - stackTrace: event.stackTrace, - error: event.error, - time: event.time, - ); - } - }); - }, - receivePort.sendPort, - ); + } + }); + }, receivePort.sendPort); final loggerPort = await receivePort.first as SendPort; IsolateNameServer.registerPortWithName(loggerPort, _kLoggerPortName); } @@ -155,18 +147,16 @@ class Logging { toFile = false; } try { - _sendPort.send( - ( - LogEvent( - level, - _stringifyMessage(message), - time: time, - error: error, - stackTrace: stackTrace, - ), - toFile + _sendPort.send(( + LogEvent( + level, + _stringifyMessage(message), + time: time, + error: error, + stackTrace: stackTrace, ), - ); + toFile, + )); } catch (e, s) { t("Isolates suck", error: e, stackTrace: s); } @@ -177,82 +167,76 @@ class Logging { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.trace, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.trace, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void d( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.debug, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.debug, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void i( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.info, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.info, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void w( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.warning, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.warning, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void e( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.error, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.error, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void f( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.fatal, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.fatal, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); } diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 395e5ecd6..09b2bbd97 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -26,6 +26,8 @@ import 'enums/backup_frequency_type.dart'; import 'enums/languages_enum.dart'; import 'enums/sync_type_enum.dart'; +typedef AutoLockInfo = ({bool enabled, int minutes}); + class Prefs extends ChangeNotifier { Prefs._(); static final Prefs _instance = Prefs._(); @@ -41,6 +43,8 @@ class Prefs extends ChangeNotifier { _randomizePIN = await _getRandomizePIN(); _useBiometrics = await _getUseBiometrics(); _hasPin = await _getHasPin(); + _hasDuressPin = await _getHasDuressPin(); + _biometricsDuress = await _getBiometricsDuress(); _language = await _getPreferredLanguage(); _showFavoriteWallets = await _getShowFavoriteWallets(); _wifiOnly = await _getUseWifiOnly(); @@ -76,6 +80,7 @@ class Prefs extends ChangeNotifier { _advancedFiroFeatures = await _getAdvancedFiroFeatures(); _logsPath = await _getLogsPath(); _logLevel = await _getLogLevel(); + _autoLockInfo = await _getAutoLockInfo(); _initialized = true; } @@ -101,9 +106,10 @@ class Prefs extends ChangeNotifier { Future _getLastUnlockedTimeout() async { return (DB.instance.get( - boxName: DB.boxNamePrefs, - key: "lastUnlockedTimeout", - )) as int? ?? + boxName: DB.boxNamePrefs, + key: "lastUnlockedTimeout", + )) + as int? ?? 60; } @@ -127,9 +133,10 @@ class Prefs extends ChangeNotifier { Future _getLastUnlocked() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "lastUnlocked", - ) as int? ?? + boxName: DB.boxNamePrefs, + key: "lastUnlocked", + ) + as int? ?? 0; } @@ -155,9 +162,10 @@ class Prefs extends ChangeNotifier { Future _getCurrentNotificationIndex() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "currentNotificationId", - ) as int? ?? + boxName: DB.boxNamePrefs, + key: "currentNotificationId", + ) + as int? ?? 0; } @@ -180,10 +188,12 @@ class Prefs extends ChangeNotifier { } Future> _getWalletIdsSyncOnStartup() async { - final list = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "walletIdsSyncOnStartup", - ) as List? ?? + final list = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "walletIdsSyncOnStartup", + ) + as List? ?? []; return List.from(list); } @@ -207,10 +217,12 @@ class Prefs extends ChangeNotifier { } Future _getSyncType() async { - final int index = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "syncTypeIndex", - ) as int? ?? + final int index = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "syncTypeIndex", + ) + as int? ?? SyncingType.allWalletsOnStartup.index; return SyncingType.values[index]; } @@ -234,8 +246,11 @@ class Prefs extends ChangeNotifier { } Future _getUseWifiOnly() async { - return await DB.instance - .get(boxName: DB.boxNamePrefs, key: "wifiOnly") as bool? ?? + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "wifiOnly", + ) + as bool? ?? false; } @@ -259,9 +274,10 @@ class Prefs extends ChangeNotifier { Future _getShowFavoriteWallets() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showFavoriteWallets", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "showFavoriteWallets", + ) + as bool? ?? true; } @@ -285,9 +301,10 @@ class Prefs extends ChangeNotifier { Future _getPreferredLanguage() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "language", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "language", + ) + as String? ?? Language.englishUS.description; } @@ -311,9 +328,10 @@ class Prefs extends ChangeNotifier { Future _getPreferredCurrency() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "currency", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "currency", + ) + as String? ?? "USD"; } @@ -378,9 +396,10 @@ class Prefs extends ChangeNotifier { Future _getRandomizePIN() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "randomizePIN", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "randomizePIN", + ) + as bool? ?? false; } @@ -404,9 +423,10 @@ class Prefs extends ChangeNotifier { Future _getUseBiometrics() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "useBiometrics", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "useBiometrics", + ) + as bool? ?? false; } @@ -418,16 +438,76 @@ class Prefs extends ChangeNotifier { set hasPin(bool hasPin) { if (_hasPin != hasPin) { - DB.instance - .put(boxName: DB.boxNamePrefs, key: "hasPin", value: hasPin); + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "hasPin", + value: hasPin, + ); _hasPin = hasPin; notifyListeners(); } } Future _getHasPin() async { - return await DB.instance - .get(boxName: DB.boxNamePrefs, key: "hasPin") as bool? ?? + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "hasPin", + ) + as bool? ?? + false; + } + + // has set up pin + + bool _hasDuressPin = false; + + bool get hasDuressPin => _hasDuressPin; + + set hasDuressPin(bool hasDuressPin) { + if (_hasDuressPin != hasDuressPin) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "hasDuressPin", + value: hasDuressPin, + ); + _hasDuressPin = hasDuressPin; + notifyListeners(); + } + } + + Future _getHasDuressPin() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "hasDuressPin", + ) + as bool? ?? + false; + } + + // has toggled biometrics to act for duress instead of normal auth + + bool _biometricsDuress = false; + + bool get biometricsDuress => _biometricsDuress; + + set biometricsDuress(bool biometricsDuress) { + if (_biometricsDuress != biometricsDuress) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "biometricsDuress", + value: biometricsDuress, + ); + _biometricsDuress = biometricsDuress; + notifyListeners(); + } + } + + Future _getBiometricsDuress() async { + return await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "biometricsDuress", + ) + as bool? ?? false; } @@ -451,9 +531,10 @@ class Prefs extends ChangeNotifier { Future _getHasFamiliarity() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "familiarity", - ) as int? ?? + boxName: DB.boxNamePrefs, + key: "familiarity", + ) + as int? ?? 0; } @@ -477,9 +558,10 @@ class Prefs extends ChangeNotifier { Future _getTorKillswitch() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "torKillswitch", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "torKillswitch", + ) + as bool? ?? true; } @@ -503,9 +585,10 @@ class Prefs extends ChangeNotifier { Future _getShowTestNetCoins() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showTestNetCoins", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "showTestNetCoins", + ) + as bool? ?? false; } @@ -519,22 +602,23 @@ class Prefs extends ChangeNotifier { if (_isAutoBackupEnabled != isAutoBackupEnabled) { DB.instance .put( - boxName: DB.boxNamePrefs, - key: "isAutoBackupEnabled", - value: isAutoBackupEnabled, - ) + boxName: DB.boxNamePrefs, + key: "isAutoBackupEnabled", + value: isAutoBackupEnabled, + ) .then((_) { - _isAutoBackupEnabled = isAutoBackupEnabled; - notifyListeners(); - }); + _isAutoBackupEnabled = isAutoBackupEnabled; + notifyListeners(); + }); } } Future _getIsAutoBackupEnabled() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "isAutoBackupEnabled", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "isAutoBackupEnabled", + ) + as bool? ?? false; } @@ -558,9 +642,10 @@ class Prefs extends ChangeNotifier { Future _getAutoBackupLocation() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoBackupLocation", - ) as String?; + boxName: DB.boxNamePrefs, + key: "autoBackupLocation", + ) + as String?; } // auto backup frequency type @@ -601,10 +686,12 @@ class Prefs extends ChangeNotifier { } Future _getBackupFrequencyType() async { - String? rate = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "backupFrequencyType", - ) as String?; + String? rate = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "backupFrequencyType", + ) + as String?; rate ??= "10Min"; switch (rate) { case "10Min": @@ -638,9 +725,10 @@ class Prefs extends ChangeNotifier { Future _getLastAutoBackup() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoBackupFileUri", - ) as DateTime?; + boxName: DB.boxNamePrefs, + key: "autoBackupFileUri", + ) + as DateTime?; } // auto backup @@ -653,22 +741,23 @@ class Prefs extends ChangeNotifier { if (_hideBlockExplorerWarning != hideBlockExplorerWarning) { DB.instance .put( - boxName: DB.boxNamePrefs, - key: "hideBlockExplorerWarning", - value: hideBlockExplorerWarning, - ) + boxName: DB.boxNamePrefs, + key: "hideBlockExplorerWarning", + value: hideBlockExplorerWarning, + ) .then((_) { - _hideBlockExplorerWarning = hideBlockExplorerWarning; - notifyListeners(); - }); + _hideBlockExplorerWarning = hideBlockExplorerWarning; + notifyListeners(); + }); } } Future _getHideBlockExplorerWarning() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "hideBlockExplorerWarning", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "hideBlockExplorerWarning", + ) + as bool? ?? false; } @@ -682,22 +771,23 @@ class Prefs extends ChangeNotifier { if (_gotoWalletOnStartup != gotoWalletOnStartup) { DB.instance .put( - boxName: DB.boxNamePrefs, - key: "gotoWalletOnStartup", - value: gotoWalletOnStartup, - ) + boxName: DB.boxNamePrefs, + key: "gotoWalletOnStartup", + value: gotoWalletOnStartup, + ) .then((_) { - _gotoWalletOnStartup = gotoWalletOnStartup; - notifyListeners(); - }); + _gotoWalletOnStartup = gotoWalletOnStartup; + notifyListeners(); + }); } } Future _getGotoWalletOnStartup() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "gotoWalletOnStartup", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "gotoWalletOnStartup", + ) + as bool? ?? false; } @@ -721,9 +811,10 @@ class Prefs extends ChangeNotifier { Future _getStartupWalletId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "startupWalletId", - ) as String?; + boxName: DB.boxNamePrefs, + key: "startupWalletId", + ) + as String?; } // incognito mode off by default @@ -736,28 +827,31 @@ class Prefs extends ChangeNotifier { if (_externalCalls != externalCalls) { DB.instance .put( - boxName: DB.boxNamePrefs, - key: "externalCalls", - value: externalCalls, - ) + boxName: DB.boxNamePrefs, + key: "externalCalls", + value: externalCalls, + ) .then((_) { - _externalCalls = externalCalls; - notifyListeners(); - }); + _externalCalls = externalCalls; + notifyListeners(); + }); } } Future _getHasExternalCalls() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "externalCalls", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "externalCalls", + ) + as bool? ?? true; } Future isExternalCallsSet() async { - if (await DB.instance - .get(boxName: DB.boxNamePrefs, key: "externalCalls") == + if (await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "externalCalls", + ) == null) { return false; } @@ -768,8 +862,9 @@ class Prefs extends ChangeNotifier { String? get userID => _userId; Future _getUserId() async { - String? userID = await DB.instance - .get(boxName: DB.boxNamePrefs, key: "userID") as String?; + String? userID = + await DB.instance.get(boxName: DB.boxNamePrefs, key: "userID") + as String?; if (userID == null) { userID = const Uuid().v4(); await saveUserID(userID); @@ -779,8 +874,11 @@ class Prefs extends ChangeNotifier { Future saveUserID(String userId) async { _userId = userId; - await DB.instance - .put(boxName: DB.boxNamePrefs, key: "userID", value: _userId); + await DB.instance.put( + boxName: DB.boxNamePrefs, + key: "userID", + value: _userId, + ); // notifyListeners(); } @@ -788,10 +886,15 @@ class Prefs extends ChangeNotifier { int? get signupEpoch => _signupEpoch; Future _getSignupEpoch() async { - int? signupEpoch = await DB.instance - .get(boxName: DB.boxNamePrefs, key: "signupEpoch") as int?; + int? signupEpoch = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "signupEpoch", + ) + as int?; if (signupEpoch == null) { - signupEpoch = DateTime.now().millisecondsSinceEpoch ~/ + signupEpoch = + DateTime.now().millisecondsSinceEpoch ~/ Duration.millisecondsPerSecond; await saveSignupEpoch(signupEpoch); } @@ -828,9 +931,10 @@ class Prefs extends ChangeNotifier { Future _getEnableCoinControl() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "enableCoinControl", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "enableCoinControl", + ) + as bool? ?? false; } @@ -854,9 +958,10 @@ class Prefs extends ChangeNotifier { Future _getEnableSystemBrightness() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "enableSystemBrightness", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "enableSystemBrightness", + ) + as bool? ?? false; } @@ -880,9 +985,10 @@ class Prefs extends ChangeNotifier { Future _getThemeId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "themeId", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "themeId", + ) + as String? ?? "light"; } @@ -906,9 +1012,10 @@ class Prefs extends ChangeNotifier { Future _getSystemBrightnessLightThemeId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "systemBrightnessLightThemeId", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "systemBrightnessLightThemeId", + ) + as String? ?? "light"; } @@ -932,9 +1039,10 @@ class Prefs extends ChangeNotifier { Future _getSystemBrightnessDarkTheme() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "systemBrightnessDarkThemeId", - ) as String? ?? + boxName: DB.boxNamePrefs, + key: "systemBrightnessDarkThemeId", + ) + as String? ?? "dark"; } @@ -962,10 +1070,12 @@ class Prefs extends ChangeNotifier { Future _setAmountUnits() async { for (final coin in AppConfig.coins) { - final unitIndex = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "amountUnitFor${coin.identifier}", - ) as int? ?? + final unitIndex = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "amountUnitFor${coin.identifier}", + ) + as int? ?? 0; // 0 is "normal" _amountUnits[coin] = AmountUnit.values[unitIndex]; } @@ -995,10 +1105,12 @@ class Prefs extends ChangeNotifier { Future _setMaxDecimals() async { for (final coin in AppConfig.coins) { - final decimals = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "maxDecimalsFor${coin.identifier}", - ) as int? ?? + final decimals = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "maxDecimalsFor${coin.identifier}", + ) + as int? ?? (coin.fractionDigits > 18 ? 18 : coin.fractionDigits); // use some sane max rather than up to 30 that nano uses _amountDecimals[coin.identifier] = decimals; @@ -1031,9 +1143,10 @@ class Prefs extends ChangeNotifier { Future _getUseTor() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "useTor", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "useTor", + ) + as bool? ?? false; } @@ -1054,10 +1167,7 @@ class Prefs extends ChangeNotifier { boxName: DB.boxNamePrefs, key: "fusionServerInfoMap", value: _fusionServerInfo.map( - (key, value) => MapEntry( - key, - value.toJsonString(), - ), + (key, value) => MapEntry(key, value.toJsonString()), ), ); notifyListeners(); @@ -1065,29 +1175,30 @@ class Prefs extends ChangeNotifier { } Future> _getFusionServerInfo() async { - final map = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "fusionServerInfoMap", - ) as Map?; + final map = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "fusionServerInfoMap", + ) + as Map?; if (map == null) { return _fusionServerInfo; } - final actualMap = Map.from(map).map( - (key, value) => MapEntry( - key, - FusionInfo.fromJsonString(value), - ), - ); + final actualMap = Map.from( + map, + ).map((key, value) => MapEntry(key, FusionInfo.fromJsonString(value))); // legacy bch check if (actualMap["bitcoincash"] == null || actualMap["bitcoincashTestnet"] == null) { - final saved = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "fusionServerInfo", - ) as String?; + final saved = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "fusionServerInfo", + ) + as String?; if (saved != null) { final bchInfo = FusionInfo.fromJsonString(saved); @@ -1098,10 +1209,7 @@ class Prefs extends ChangeNotifier { boxName: DB.boxNamePrefs, key: "fusionServerInfoMap", value: actualMap.map( - (key, value) => MapEntry( - key, - value.toJsonString(), - ), + (key, value) => MapEntry(key, value.toJsonString()), ), ), ); @@ -1131,9 +1239,10 @@ class Prefs extends ChangeNotifier { Future _getAutoPin() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoPin", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "autoPin", + ) + as bool? ?? false; } @@ -1157,13 +1266,14 @@ class Prefs extends ChangeNotifier { Future _getEnableExchange() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showExchange", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "showExchange", + ) + as bool? ?? true; } - // Show/hide lelantus and spark coins. Defaults to false + // Show/hide spark coins. Defaults to false bool _advancedFiroFeatures = false; bool get advancedFiroFeatures => _advancedFiroFeatures; set advancedFiroFeatures(bool advancedFiroFeatures) { @@ -1180,9 +1290,10 @@ class Prefs extends ChangeNotifier { Future _getAdvancedFiroFeatures() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "advancedFiroFeatures", - ) as bool? ?? + boxName: DB.boxNamePrefs, + key: "advancedFiroFeatures", + ) + as bool? ?? false; } @@ -1203,9 +1314,10 @@ class Prefs extends ChangeNotifier { Future _getLogsPath() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "logsPath", - ) as String?; + boxName: DB.boxNamePrefs, + key: "logsPath", + ) + as String?; } // log level pref @@ -1224,10 +1336,12 @@ class Prefs extends ChangeNotifier { } Future _getLogLevel() async { - final value = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "logLevel", - ) as int?; + final value = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "logLevel", + ) + as int?; try { return Level.values.firstWhere((e) => e.value == value); @@ -1236,4 +1350,37 @@ class Prefs extends ChangeNotifier { return Level.warning; } } + + // auto lock timeout + + AutoLockInfo _autoLockInfo = (enabled: false, minutes: 10); + + AutoLockInfo get autoLockInfo => _autoLockInfo; + + set autoLockInfo(AutoLockInfo autoLockInfo) { + if (_autoLockInfo != autoLockInfo) { + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "autoLockInfo", + value: { + "enabled": autoLockInfo.enabled, + "minutes": autoLockInfo.minutes, + }, + ); + _autoLockInfo = autoLockInfo; + notifyListeners(); + } + } + + Future _getAutoLockInfo() async { + final map = + await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "autoLockInfo", + ) + as Map? ?? + {"enabled": false, "minutes": 10}; + + return (enabled: map["enabled"] as bool, minutes: map["minutes"] as int); + } } diff --git a/lib/utilities/show_node_tor_settings_mismatch.dart b/lib/utilities/show_node_tor_settings_mismatch.dart index 6c2490b51..ce9f595f1 100644 --- a/lib/utilities/show_node_tor_settings_mismatch.dart +++ b/lib/utilities/show_node_tor_settings_mismatch.dart @@ -20,7 +20,8 @@ Future checkShowNodeTorSettingsMismatch({ bool rootNavigator = false, }) async { final node = - nodeService.getPrimaryNodeFor(currency: currency) ?? currency.defaultNode; + nodeService.getPrimaryNodeFor(currency: currency) ?? + currency.defaultNode(isPrimary: true); if (prefs.useTor) { if (node.torEnabled) { return true; @@ -34,63 +35,57 @@ Future checkShowNodeTorSettingsMismatch({ final result = await showDialog( context: context, barrierDismissible: false, - builder: (context) => ConditionalParent( - condition: Util.isDesktop, - builder: (child) => DesktopDialog( - maxHeight: double.infinity, - child: Padding( - padding: const EdgeInsets.all(32), - child: child, - ), - ), - child: ConditionalParent( - condition: !Util.isDesktop, - builder: (child) => StackDialogBase( - child: child, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Attention! Node connection issue detected. " - "The current node will not sync due to its connectivity settings. " - "Please adjust the node settings or enable/disable TOR.", - style: STextStyles.w600_16(context), - ), - SizedBox( - height: Util.isDesktop ? 32 : 24, - ), - Row( + builder: + (context) => ConditionalParent( + condition: Util.isDesktop, + builder: + (child) => DesktopDialog( + maxHeight: double.infinity, + child: Padding(padding: const EdgeInsets.all(32), child: child), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => StackDialogBase(child: child), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - allowCancel - ? Expanded( - child: SecondaryButton( - buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - label: "Cancel", - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - ) - : const Spacer(), - SizedBox( - width: Util.isDesktop ? 24 : 16, + Text( + "Attention! Node connection issue detected. " + "The current node will not sync due to its connectivity settings. " + "Please adjust the node settings or enable/disable TOR.", + style: STextStyles.w600_16(context), ), - Expanded( - child: PrimaryButton( - buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - label: "Continue", - onPressed: () { - Navigator.of(context).pop(true); - }, - ), + SizedBox(height: Util.isDesktop ? 32 : 24), + Row( + children: [ + allowCancel + ? Expanded( + child: SecondaryButton( + buttonHeight: + Util.isDesktop ? ButtonHeight.l : null, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ) + : const Spacer(), + SizedBox(width: Util.isDesktop ? 24 : 16), + Expanded( + child: PrimaryButton( + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + label: "Continue", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), + ], ), ], ), - ], + ), ), - ), - ), ); return result ?? true; diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 795ea9720..e13557739 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -10,8 +10,8 @@ import 'dart:io'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../app_config.dart'; import 'prefs.dart'; @@ -40,14 +40,17 @@ abstract class StackFileSystem { if (Util.isArmLinux) { appDirectory = await getApplicationDocumentsDirectory(); appDirectory = Directory( - "${appDirectory.path}/.${AppConfig.appDefaultDataDirName}", + path.join(appDirectory.path, ".${AppConfig.appDefaultDataDirName}"), ); } else if (Platform.isLinux) { if (_overrideDesktopDirPath != null) { appDirectory = Directory(_overrideDesktopDirPath!); } else { appDirectory = Directory( - "${Platform.environment['HOME']}/.${AppConfig.appDefaultDataDirName}", + path.join( + Platform.environment['HOME']!, + ".${AppConfig.appDefaultDataDirName}", + ), ); } } else if (Platform.isWindows) { @@ -62,7 +65,7 @@ abstract class StackFileSystem { } else { appDirectory = await getLibraryDirectory(); appDirectory = Directory( - "${appDirectory.path}/${AppConfig.appDefaultDataDirName}", + path.join(appDirectory.path, AppConfig.appDefaultDataDirName), ); } } else if (Platform.isIOS) { @@ -86,7 +89,20 @@ abstract class StackFileSystem { static Future applicationIsarDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/isar"); + final dir = Directory(path.join(root.path, "isar")); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } else { + return root; + } + } + + static Future applicationDriftDirectory() async { + final root = await applicationRootDirectory(); + if (_createSubDirs) { + final dir = Directory(path.join(root.path, "drift")); if (!dir.existsSync()) { await dir.create(); } @@ -113,7 +129,7 @@ abstract class StackFileSystem { static Future applicationTorDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/tor"); + final dir = Directory(path.join(root.path, "tor")); if (!dir.existsSync()) { await dir.create(); } @@ -123,10 +139,19 @@ abstract class StackFileSystem { } } + static Future applicationMwebdDirectory(String network) async { + final root = await applicationRootDirectory(); + final dir = Directory(path.join(root.path, "mwebd", network)); + if (!dir.existsSync()) { + await dir.create(recursive: true); + } + return dir; + } + static Future applicationFiroCacheSQLiteDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/sqlite/firo_cache"); + final dir = Directory(path.join(root.path, "sqlite", "firo_cache")); if (!dir.existsSync()) { await dir.create(recursive: true); } @@ -139,7 +164,7 @@ abstract class StackFileSystem { static Future applicationHiveDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { - final dir = Directory("${root.path}/hive"); + final dir = Directory(path.join(root.path, "hive")); if (!dir.existsSync()) { await dir.create(); } @@ -151,7 +176,7 @@ abstract class StackFileSystem { static Future applicationXelisDirectory() async { final root = await applicationRootDirectory(); - final dir = Directory("${root.path}${Platform.pathSeparator}xelis"); + final dir = Directory(path.join(root.path, "xelis")); if (!dir.existsSync()) { await dir.create(); } @@ -160,7 +185,7 @@ abstract class StackFileSystem { static Future applicationXelisTableDirectory() async { final xelis = await applicationXelisDirectory(); - final dir = Directory("${xelis.path}${Platform.pathSeparator}table"); + final dir = Directory(path.join(xelis.path, "table")); if (!dir.existsSync()) { await dir.create(); } @@ -170,7 +195,7 @@ abstract class StackFileSystem { static Future initThemesDir() async { final root = await applicationRootDirectory(); - final dir = Directory("${root.path}/themes"); + final dir = Directory(path.join(root.path, "themes")); if (!dir.existsSync()) { await dir.create(); } @@ -193,13 +218,16 @@ abstract class StackFileSystem { final Directory logsDir; if (Platform.isIOS) { - logsDir = Directory("${appDocsDir.path}/logs"); + logsDir = Directory(path.join(appDocsDir.path, "logs")); } else if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) { // TODO check this is correct for macos - logsDir = Directory("${appDocsDir.path}/$logsDirName"); + logsDir = Directory(path.join(appDocsDir.path, logsDirName)); } else if (Platform.isAndroid) { - await Permission.storage.request(); - logsDir = Directory("/storage/emulated/0/Documents/$logsDirName"); + // final dir = await wtfAndroidDocumentsPath(); + // final logsDirPath = path.join(dir.path, logsDirName); + // logsDir = Directory(logsDirPath); + + logsDir = Directory(path.join(appDocsDir.path, "logs")); } else { throw Exception("Unsupported Platform"); } @@ -210,4 +238,18 @@ abstract class StackFileSystem { return logsDir; } + + static Future wtfAndroidDocumentsPath() async { + const base = "/storage/emulated/"; + final rootDir = await applicationRootDirectory(); + final parts = rootDir.path.replaceFirst("/data/user/", "").split("/"); + if (parts.isNotEmpty) { + final id = int.tryParse(parts.first); + + if (id != null) { + return Directory(path.join(base, id.toString(), "Documents")); + } + } + throw Exception("Unsupported Android flavor"); + } } diff --git a/lib/utilities/wallet_tools.dart b/lib/utilities/wallet_tools.dart index 8d869ea63..b38578370 100644 --- a/lib/utilities/wallet_tools.dart +++ b/lib/utilities/wallet_tools.dart @@ -2,7 +2,6 @@ import 'package:isar/isar.dart'; import '../db/isar/main_db.dart'; import '../models/isar/models/blockchain_data/v2/transaction_v2.dart'; -import '../models/isar/models/firo_specific/lelantus_coin.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import '../wallets/isar/models/spark_coin.dart'; import '../wallets/wallet/impl/firo_wallet.dart'; @@ -20,10 +19,11 @@ abstract class WalletDevTools { maxDecimals: 8, ); - final all = MainDB.instance.isar.transactionV2s - .where() - .walletIdEqualTo(wallet.walletId) - .findAllSync(); + final all = + MainDB.instance.isar.transactionV2s + .where() + .walletIdEqualTo(wallet.walletId) + .findAllSync(); final totalCount = all.length; @@ -47,14 +47,11 @@ abstract class WalletDevTools { fractionDigits: 8, ); - final lelantusCoinsCount = MainDB.instance.isar.lelantusCoins - .where() - .walletIdEqualTo(wallet.walletId) - .countSync(); - final sparkCoinsCount = MainDB.instance.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(wallet.walletId) - .countSync(); + final sparkCoinsCount = + MainDB.instance.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(wallet.walletId) + .countSync(); final buffer = StringBuffer(); buffer.writeln("============= ${wallet.info.name} ============="); @@ -63,7 +60,6 @@ abstract class WalletDevTools { buffer.writeln( "balanceAccordingToTxns: ${amtFmt.format(balanceAccordingToTxHistory)}", ); - buffer.writeln("lelantusCoinsCount: $lelantusCoinsCount"); buffer.writeln("sparkCoinsCount: $sparkCoinsCount"); buffer.writeln("=================================================="); diff --git a/lib/wallets/api/lelantus_ffi_wrapper.dart b/lib/wallets/api/lelantus_ffi_wrapper.dart deleted file mode 100644 index 3ee60d994..000000000 --- a/lib/wallets/api/lelantus_ffi_wrapper.dart +++ /dev/null @@ -1,551 +0,0 @@ -import 'package:bip32/bip32.dart'; -import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; -import 'package:flutter/foundation.dart'; -import 'package:lelantus/lelantus.dart' as lelantus; - -import '../../models/isar/models/isar_models.dart' as isar_models; -import '../../models/isar/models/isar_models.dart'; -import '../../models/lelantus_fee_data.dart'; -import '../../utilities/amount/amount.dart'; -import '../../utilities/extensions/impl/string.dart'; -import '../../utilities/extensions/impl/uint8_list.dart'; -import '../../utilities/format.dart'; -import '../../utilities/logger.dart'; -import '../crypto_currency/intermediate/bip39_hd_currency.dart'; -import '../models/tx_data.dart'; - -abstract final class LelantusFfiWrapper { - static const MINT_LIMIT = 5001 * 100000000; - static const MINT_LIMIT_TESTNET = 1001 * 100000000; - - static const JMINT_INDEX = 5; - static const MINT_INDEX = 2; - static const TRANSACTION_LELANTUS = 8; - static const ANONYMITY_SET_EMPTY_ID = 0; - - // partialDerivationPath should be something like "m/$purpose'/$coinType'/$account'/" - static Future<({List spendTxIds, List lelantusCoins})> - restore({ - required final String hexRootPrivateKey, - required final Uint8List chaincode, - required final Bip39HDCurrency cryptoCurrency, - required final int latestSetId, - required final Map setDataMap, - required final Set usedSerialNumbers, - required final String walletId, - required final String partialDerivationPath, - }) async { - final args = ( - hexRootPrivateKey: hexRootPrivateKey, - chaincode: chaincode, - cryptoCurrency: cryptoCurrency, - latestSetId: latestSetId, - setDataMap: setDataMap, - usedSerialNumbers: usedSerialNumbers, - walletId: walletId, - partialDerivationPath: partialDerivationPath, - ); - try { - return await compute(_restore, args); - } catch (e, s) { - Logging.instance.i("Exception rethrown from _restore(): ", error: e, stackTrace: s); - rethrow; - } - } - - // partialDerivationPath should be something like "m/$purpose'/$coinType'/$account'/" - static Future<({List spendTxIds, List lelantusCoins})> - _restore( - ({ - String hexRootPrivateKey, - Uint8List chaincode, - Bip39HDCurrency cryptoCurrency, - int latestSetId, - Map setDataMap, - Set usedSerialNumbers, - String walletId, - String partialDerivationPath, - }) args, - ) async { - final List jindexes = []; - final List lelantusCoins = []; - - final List spendTxIds = []; - int lastFoundIndex = 0; - int currentIndex = 0; - - final root = BIP32.fromPrivateKey( - args.hexRootPrivateKey.toUint8ListFromHex, - args.chaincode, - ); - - while (currentIndex < lastFoundIndex + 50) { - final mintKeyPair = root.derivePath( - "${args.partialDerivationPath}$MINT_INDEX/$currentIndex", - ); - - final String mintTag = lelantus.CreateTag( - mintKeyPair.privateKey!.toHex, - currentIndex, - mintKeyPair.identifier.toHex, - isTestnet: args.cryptoCurrency.network.isTestNet, - ); - - for (int setId = 1; setId <= args.latestSetId; setId++) { - final setData = args.setDataMap[setId] as Map; - final foundCoin = (setData["coins"] as List).firstWhere( - (e) => e[1] == mintTag, - orElse: () => [], - ); - - if (foundCoin.length == 4) { - lastFoundIndex = currentIndex; - - final String publicCoin = foundCoin[0] as String; - final String txId = foundCoin[3] as String; - - // this value will either be an int or a String - final dynamic thirdValue = foundCoin[2]; - - if (thirdValue is int) { - final int amount = thirdValue; - final String serialNumber = lelantus.GetSerialNumber( - amount, - mintKeyPair.privateKey!.toHex, - currentIndex, - isTestnet: args.cryptoCurrency.network.isTestNet, - ); - final bool isUsed = args.usedSerialNumbers.contains(serialNumber); - - lelantusCoins.removeWhere( - (e) => - e.txid == txId && - e.mintIndex == currentIndex && - e.anonymitySetId != setId, - ); - - lelantusCoins.add( - isar_models.LelantusCoin( - walletId: args.walletId, - mintIndex: currentIndex, - value: amount.toString(), - txid: txId, - anonymitySetId: setId, - isUsed: isUsed, - isJMint: false, - otherData: - publicCoin, // not really needed but saved just in case - ), - ); - debugPrint("serial=$serialNumber amount=$amount used=$isUsed"); - } else if (thirdValue is String) { - final int keyPath = lelantus.GetAesKeyPath(publicCoin); - - final aesKeyPair = root.derivePath( - "${args.partialDerivationPath}$JMINT_INDEX/$keyPath", - ); - - try { - final String aesPrivateKey = aesKeyPair.privateKey!.toHex; - - final int amount = lelantus.decryptMintAmount( - aesPrivateKey, - thirdValue, - ); - - final String serialNumber = lelantus.GetSerialNumber( - amount, - aesPrivateKey, - currentIndex, - isTestnet: args.cryptoCurrency.network.isTestNet, - ); - final bool isUsed = args.usedSerialNumbers.contains(serialNumber); - - lelantusCoins.removeWhere( - (e) => - e.txid == txId && - e.mintIndex == currentIndex && - e.anonymitySetId != setId, - ); - - lelantusCoins.add( - isar_models.LelantusCoin( - walletId: args.walletId, - mintIndex: currentIndex, - value: amount.toString(), - txid: txId, - anonymitySetId: setId, - isUsed: isUsed, - isJMint: true, - otherData: - publicCoin, // not really needed but saved just in case - ), - ); - jindexes.add(currentIndex); - - spendTxIds.add(txId); - } catch (_) { - debugPrint("AES keypair derivation issue for key path: $keyPath"); - } - } else { - debugPrint("Unexpected coin found: $foundCoin"); - } - } - } - - currentIndex++; - } - - return (spendTxIds: spendTxIds, lelantusCoins: lelantusCoins); - } - - static Future estimateJoinSplitFee({ - required Amount spendAmount, - required bool subtractFeeFromAmount, - required List lelantusEntries, - required bool isTestNet, - }) async { - return await compute( - LelantusFfiWrapper._estimateJoinSplitFee, - ( - spendAmount: spendAmount.raw.toInt(), - subtractFeeFromAmount: subtractFeeFromAmount, - lelantusEntries: lelantusEntries, - isTestNet: isTestNet, - ), - ); - } - - static Future _estimateJoinSplitFee( - ({ - int spendAmount, - bool subtractFeeFromAmount, - List lelantusEntries, - bool isTestNet, - }) data, - ) async { - debugPrint("estimateJoinSplit fee"); - // for (int i = 0; i < lelantusEntries.length; i++) { - // Logging.instance.log(lelantusEntries[i], addToDebugMessagesDB: false); - // } - debugPrint( - "${data.spendAmount} ${data.subtractFeeFromAmount}", - ); - - final List changeToMint = List.empty(growable: true); - final List spendCoinIndexes = List.empty(growable: true); - // Logging.instance.log(lelantusEntries, addToDebugMessagesDB: false); - final fee = lelantus.estimateFee( - data.spendAmount, - data.subtractFeeFromAmount, - data.lelantusEntries, - changeToMint, - spendCoinIndexes, - isTestnet: data.isTestNet, - ); - - final estimateFeeData = LelantusFeeData( - changeToMint[0], - fee, - spendCoinIndexes, - ); - debugPrint( - "estimateFeeData ${estimateFeeData.changeToMint}" - " ${estimateFeeData.fee}" - " ${estimateFeeData.spendCoinIndexes}", - ); - return estimateFeeData; - } - - static Future createJoinSplitTransaction({ - required TxData txData, - required bool subtractFeeFromAmount, - required int nextFreeMintIndex, - required int locktime, // set to current chain height - required List lelantusEntries, - required List> anonymitySets, - required Bip39HDCurrency cryptoCurrency, - required String partialDerivationPath, - required String hexRootPrivateKey, - required Uint8List chaincode, - }) async { - final arg = ( - txData: txData, - subtractFeeFromAmount: subtractFeeFromAmount, - index: nextFreeMintIndex, - lelantusEntries: lelantusEntries, - locktime: locktime, - cryptoCurrency: cryptoCurrency, - anonymitySetsArg: anonymitySets, - partialDerivationPath: partialDerivationPath, - hexRootPrivateKey: hexRootPrivateKey, - chaincode: chaincode, - ); - - return await compute(_createJoinSplitTransaction, arg); - } - - static Future _createJoinSplitTransaction( - ({ - TxData txData, - bool subtractFeeFromAmount, - int index, - List lelantusEntries, - int locktime, - Bip39HDCurrency cryptoCurrency, - List> anonymitySetsArg, - String partialDerivationPath, - String hexRootPrivateKey, - Uint8List chaincode, - }) arg, - ) async { - final spendAmount = arg.txData.recipients!.first.amount.raw.toInt(); - final address = arg.txData.recipients!.first.address; - final isChange = arg.txData.recipients!.first.isChange; - - final estimateJoinSplitFee = await _estimateJoinSplitFee( - ( - spendAmount: spendAmount, - subtractFeeFromAmount: arg.subtractFeeFromAmount, - lelantusEntries: arg.lelantusEntries, - isTestNet: arg.cryptoCurrency.network.isTestNet, - ), - ); - final changeToMint = estimateJoinSplitFee.changeToMint; - final fee = estimateJoinSplitFee.fee; - final spendCoinIndexes = estimateJoinSplitFee.spendCoinIndexes; - debugPrint("$changeToMint $fee $spendCoinIndexes"); - if (spendCoinIndexes.isEmpty) { - throw Exception("Error, Not enough funds."); - } - - final params = arg.cryptoCurrency.networkParams; - final _network = bitcoindart.NetworkType( - messagePrefix: params.messagePrefix, - bech32: params.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: params.pubHDPrefix, - private: params.privHDPrefix, - ), - pubKeyHash: params.p2pkhPrefix, - scriptHash: params.p2shPrefix, - wif: params.wifPrefix, - ); - - final tx = bitcoindart.TransactionBuilder(network: _network); - tx.setLockTime(arg.locktime); - - tx.setVersion(3 | (TRANSACTION_LELANTUS << 16)); - - tx.addInput( - '0000000000000000000000000000000000000000000000000000000000000000', - 4294967295, - 4294967295, - Uint8List(0), - ); - final derivePath = "${arg.partialDerivationPath}$MINT_INDEX/${arg.index}"; - - final root = BIP32.fromPrivateKey( - arg.hexRootPrivateKey.toUint8ListFromHex, - arg.chaincode, - ); - - final jmintKeyPair = root.derivePath(derivePath); - - final String jmintprivatekey = jmintKeyPair.privateKey!.toHex; - - final keyPath = lelantus.getMintKeyPath( - changeToMint, - jmintprivatekey, - arg.index, - isTestnet: arg.cryptoCurrency.network.isTestNet, - ); - - final _derivePath = "${arg.partialDerivationPath}$JMINT_INDEX/$keyPath"; - - final aesKeyPair = root.derivePath(_derivePath); - final aesPrivateKey = aesKeyPair.privateKey!.toHex; - - final jmintData = lelantus.createJMintScript( - changeToMint, - jmintprivatekey, - arg.index, - Format.uint8listToString(jmintKeyPair.identifier), - aesPrivateKey, - isTestnet: arg.cryptoCurrency.network.isTestNet, - ); - - tx.addOutput( - Format.stringToUint8List(jmintData), - 0, - ); - - int amount = spendAmount; - if (arg.subtractFeeFromAmount) { - amount -= fee; - } - tx.addOutput( - address, - amount, - ); - - final extractedTx = tx.buildIncomplete(); - extractedTx.setPayload(Uint8List(0)); - final txHash = extractedTx.getId(); - - final List setIds = []; - final List> anonymitySets = []; - final List anonymitySetHashes = []; - final List groupBlockHashes = []; - for (var i = 0; i < arg.lelantusEntries.length; i++) { - final anonymitySetId = arg.lelantusEntries[i].anonymitySetId; - if (!setIds.contains(anonymitySetId)) { - setIds.add(anonymitySetId); - final anonymitySet = arg.anonymitySetsArg.firstWhere( - (element) => element["setId"] == anonymitySetId, - orElse: () => {}, - ); - if (anonymitySet.isNotEmpty) { - anonymitySetHashes.add(anonymitySet['setHash'] as String); - groupBlockHashes.add(anonymitySet['blockHash'] as String); - final List list = []; - for (int i = 0; i < (anonymitySet['coins'] as List).length; i++) { - list.add(anonymitySet['coins'][i][0] as String); - } - anonymitySets.add(list); - } - } - } - - final String spendScript = lelantus.createJoinSplitScript( - txHash, - spendAmount, - arg.subtractFeeFromAmount, - jmintprivatekey, - arg.index, - arg.lelantusEntries, - setIds, - anonymitySets, - anonymitySetHashes, - groupBlockHashes, - isTestnet: arg.cryptoCurrency.network.isTestNet, - ); - - final finalTx = bitcoindart.TransactionBuilder(network: _network); - finalTx.setLockTime(arg.locktime); - - finalTx.setVersion(3 | (TRANSACTION_LELANTUS << 16)); - - finalTx.addOutput( - Format.stringToUint8List(jmintData), - 0, - ); - - finalTx.addOutput( - address, - amount, - ); - - final extTx = finalTx.buildIncomplete(); - extTx.addInput( - Format.stringToUint8List( - '0000000000000000000000000000000000000000000000000000000000000000', - ), - 4294967295, - 4294967295, - Format.stringToUint8List("c9"), - ); - // debugPrint("spendscript: $spendScript"); - extTx.setPayload(Format.stringToUint8List(spendScript)); - - final txHex = extTx.toHex(); - final txId = extTx.getId(); - - final amountAmount = Amount( - rawValue: BigInt.from(amount), - fractionDigits: arg.cryptoCurrency.fractionDigits, - ); - - return arg.txData.copyWith( - txid: txId, - raw: txHex, - recipients: [ - (address: address, amount: amountAmount, isChange: isChange), - ], - fee: Amount( - rawValue: BigInt.from(fee), - fractionDigits: arg.cryptoCurrency.fractionDigits, - ), - vSize: extTx.virtualSize(), - jMintValue: changeToMint, - spendCoinIndexes: spendCoinIndexes, - height: arg.locktime, - txType: TransactionType.outgoing, - txSubType: TransactionSubType.join, - // "confirmed_status": false, - // "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000, - ); - - // return { - // "txid": txId, - // "txHex": txHex, - // "value": amount, - // "fees": Amount( - // rawValue: BigInt.from(fee), - // fractionDigits: arg.cryptoCurrency.fractionDigits, - // ).decimal.toDouble(), - // "fee": fee, - // "vSize": extTx.virtualSize(), - // "jmintValue": changeToMint, - // "spendCoinIndexes": spendCoinIndexes, - // "height": arg.locktime, - // "txType": "Sent", - // "confirmed_status": false, - // "amount": amountAmount.decimal.toDouble(), - // "recipientAmt": amountAmount, - // "address": arg.address, - // "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000, - // "subType": "join", - // }; - } - - // =========================================================================== - - static Future _getMintScriptWrapper( - ({ - int amount, - String privateKeyHex, - int index, - String seedId, - bool isTestNet - }) data, - ) async { - final String mintHex = lelantus.getMintScript( - data.amount, - data.privateKeyHex, - data.index, - data.seedId, - isTestnet: data.isTestNet, - ); - return mintHex; - } - - static Future getMintScript({ - required Amount amount, - required String privateKeyHex, - required int index, - required String seedId, - required bool isTestNet, - }) async { - return await compute( - LelantusFfiWrapper._getMintScriptWrapper, - ( - amount: amount.raw.toInt(), - privateKeyHex: privateKeyHex, - index: index, - seedId: seedId, - isTestNet: isTestNet - ), - ); - } -} diff --git a/lib/wallets/crypto_currency/coins/banano.dart b/lib/wallets/crypto_currency/coins/banano.dart index 8b1e1114b..adae4331e 100644 --- a/lib/wallets/crypto_currency/coins/banano.dart +++ b/lib/wallets/crypto_currency/coins/banano.dart @@ -45,9 +45,7 @@ class Banano extends NanoCurrency { int get fractionDigits => 29; @override - BigInt get satsPerCoin => BigInt.parse( - "100000000000000000000000000000", - ); // 1*10^29 + BigInt get satsPerCoin => BigInt.parse("100000000000000000000000000000"); // 1*10^29 @override int get minConfirms => 1; @@ -63,7 +61,7 @@ class Banano extends NanoCurrency { int get nanoAccountType => NanoAccountType.BANANO; @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -79,6 +77,7 @@ class Banano extends NanoCurrency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -90,7 +89,7 @@ class Banano extends NanoCurrency { Uri defaultBlockExplorer(String txid) { switch (network) { case CryptoCurrencyNetwork.main: - return Uri.parse("https://www.bananolooker.com/block/$txid"); + return Uri.parse("https://creeper.banano.cc/hash/$txid"); default: throw Exception( "Unsupported network for defaultBlockExplorer(): $network", @@ -99,7 +98,16 @@ class Banano extends NanoCurrency { } @override - DerivePathType get defaultDerivePathType => throw UnsupportedError( + DerivePathType get defaultDerivePathType => + throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.banano; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index 35a9cf8f0..e6862edb5 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -62,11 +62,11 @@ class Bitcoin extends Bip39HDCurrency @override List get supportedDerivationPathTypes => [ - DerivePathType.bip44, - DerivePathType.bip49, - DerivePathType.bip84, - DerivePathType.bip86, // P2TR. - ]; + DerivePathType.bip44, + DerivePathType.bip49, + DerivePathType.bip84, + DerivePathType.bip86, // P2TR. + ]; @override String get genesisHash { @@ -83,10 +83,8 @@ class Bitcoin extends Bip39HDCurrency } @override - Amount get dustLimit => Amount( - rawValue: BigInt.from(294), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(294), fractionDigits: fractionDigits); @override coinlib.Network get networkParams { @@ -180,10 +178,11 @@ class Bitcoin extends Bip39HDCurrency // TODO: [prio=high] verify this works similarly to bitcoindart's p2sh or something(!!) case DerivePathType.bip49: - final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( - publicKey, - hrp: networkParams.bech32Hrp, - ).program.script; + final p2wpkhScript = + coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ).program.script; final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, @@ -226,7 +225,7 @@ class Bitcoin extends Bip39HDCurrency } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -241,6 +240,7 @@ class Bitcoin extends Bip39HDCurrency isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: @@ -256,6 +256,7 @@ class Bitcoin extends Bip39HDCurrency isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test4: @@ -271,6 +272,7 @@ class Bitcoin extends Bip39HDCurrency isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index d1f22227e..b24316b52 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import '../../../models/isar/models/blockchain_data/address.dart'; @@ -60,7 +61,7 @@ class BitcoinFrost extends FrostCurrency { bool get torSupport => true; @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -75,6 +76,7 @@ class BitcoinFrost extends FrostCurrency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: @@ -90,6 +92,7 @@ class BitcoinFrost extends FrostCurrency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test4: @@ -105,6 +108,7 @@ class BitcoinFrost extends FrostCurrency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -127,10 +131,8 @@ class BitcoinFrost extends FrostCurrency { } @override - Amount get dustLimit => Amount( - rawValue: BigInt.from(294), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(294), fractionDigits: fractionDigits); @override Uint8List addressToPubkey({required String address}) { @@ -222,7 +224,8 @@ class BitcoinFrost extends FrostCurrency { int get targetBlockTimeSeconds => 600; @override - DerivePathType get defaultDerivePathType => throw UnsupportedError( + DerivePathType get defaultDerivePathType => + throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); @@ -245,4 +248,21 @@ class BitcoinFrost extends FrostCurrency { // @override BigInt get defaultFeeRate => BigInt.from(1000); // https://github.com/bitcoin/bitcoin/blob/feab35189bc00bc4cf15e9dcb5cf6b34ff3a1e91/test/functional/mempool_limit.py#L259 + + @override + AddressType? getAddressType(String address) { + try { + final clAddress = cl.Address.fromString(address, networkParams); + + return switch (clAddress) { + cl.P2TRAddress() => AddressType.p2tr, + cl.P2PKHAddress() => AddressType.p2pkh, + cl.P2WSHAddress() => AddressType.p2sh, + cl.P2WPKHAddress() => AddressType.p2wpkh, + _ => null, + }; + } catch (_) { + return null; + } + } } diff --git a/lib/wallets/crypto_currency/coins/bitcoincash.dart b/lib/wallets/crypto_currency/coins/bitcoincash.dart index 6f6d5e4b5..d236b21a4 100644 --- a/lib/wallets/crypto_currency/coins/bitcoincash.dart +++ b/lib/wallets/crypto_currency/coins/bitcoincash.dart @@ -1,9 +1,8 @@ -import 'dart:typed_data'; - import 'package:bech32/bech32.dart'; import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; +import 'package:flutter/foundation.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/node_model.dart'; @@ -65,9 +64,9 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - DerivePathType.bip44, - if (network != CryptoCurrencyNetwork.test) DerivePathType.bch44, - ]; + DerivePathType.bip44, + if (network != CryptoCurrencyNetwork.test) DerivePathType.bch44, + ]; @override String get genesisHash { @@ -82,10 +81,8 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - Amount get dustLimit => Amount( - rawValue: BigInt.from(546), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(546), fractionDigits: fractionDigits); @override coinlib.Network get networkParams { @@ -233,6 +230,17 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface { // Do not validate "p" (P2SH) addresses. } + @override + AddressType? getAddressType(String address) { + final format = bitbox.Address.detectFormat(address); + + return super.getAddressType( + format == bitbox.Address.formatCashAddr + ? bitbox.Address.toLegacyAddress(address) + : address, + ); + } + @override DerivePathType addressType({required String address}) { Uint8List? decodeBase58; @@ -285,7 +293,7 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -300,6 +308,7 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: @@ -315,6 +324,7 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: diff --git a/lib/wallets/crypto_currency/coins/cardano.dart b/lib/wallets/crypto_currency/coins/cardano.dart index 71ea2c372..ce7268176 100644 --- a/lib/wallets/crypto_currency/coins/cardano.dart +++ b/lib/wallets/crypto_currency/coins/cardano.dart @@ -1,3 +1,6 @@ +import 'package:blockchain_utils/bip/address/ada/ada.dart'; +import 'package:on_chain/ada/ada.dart'; + import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/node_model.dart'; import '../../../utilities/default_nodes.dart'; @@ -52,7 +55,8 @@ class Cardano extends Bip39Currency { switch (network) { case CryptoCurrencyNetwork.main: return Uri.parse( - "https://explorer.cardano.org/en/transaction?id=$txid"); + "https://explorer.cardano.org/en/transaction?id=$txid", + ); default: throw Exception( "Unsupported network for defaultBlockExplorer(): $network", @@ -64,7 +68,7 @@ class Cardano extends Bip39Currency { DerivePathType get defaultDerivePathType => DerivePathType.cardanoShelley; @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -79,6 +83,7 @@ class Cardano extends Bip39Currency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -121,9 +126,28 @@ class Cardano extends Bip39Currency { bool validateAddress(String address) { switch (network) { case CryptoCurrencyNetwork.main: - return RegExp(r"^addr1[0-9a-zA-Z]{98}$").hasMatch(address); + try { + final adaAddress = ADAAddress.fromAddress( + address, + network: ADANetwork.mainnet, + ); + + return (adaAddress is ADABaseAddress || + adaAddress is ADAEnterpriseAddress); + } catch (_) { + return false; + } + default: throw Exception("Unsupported network: $network"); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.cardanoShelley; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/dash.dart b/lib/wallets/crypto_currency/coins/dash.dart index 1a63f1811..e2ad041ea 100644 --- a/lib/wallets/crypto_currency/coins/dash.dart +++ b/lib/wallets/crypto_currency/coins/dash.dart @@ -52,8 +52,8 @@ class Dash extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - DerivePathType.bip44, - ]; + DerivePathType.bip44, + ]; @override String constructDerivePath({ @@ -89,10 +89,8 @@ class Dash extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - Amount get dustLimit => Amount( - rawValue: BigInt.from(1000000), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(1000000), fractionDigits: fractionDigits); @override String get genesisHash { @@ -107,10 +105,7 @@ class Dash extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - ({ - coinlib.Address address, - AddressType addressType, - }) getAddressForPublicKey({ + ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({ required coinlib.ECPublicKey publicKey, required DerivePathType derivePathType, }) { @@ -176,7 +171,7 @@ class Dash extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -191,6 +186,7 @@ class Dash extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: diff --git a/lib/wallets/crypto_currency/coins/dogecoin.dart b/lib/wallets/crypto_currency/coins/dogecoin.dart index ce10bc0d7..1d281f5bb 100644 --- a/lib/wallets/crypto_currency/coins/dogecoin.dart +++ b/lib/wallets/crypto_currency/coins/dogecoin.dart @@ -52,8 +52,8 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - DerivePathType.bip44, - ]; + DerivePathType.bip44, + ]; @override String constructDerivePath({ @@ -89,10 +89,8 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - Amount get dustLimit => Amount( - rawValue: BigInt.from(1000000), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(1000000), fractionDigits: fractionDigits); @override String get genesisHash { @@ -107,10 +105,7 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - ({ - coinlib.Address address, - AddressType addressType, - }) getAddressForPublicKey({ + ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({ required coinlib.ECPublicKey publicKey, required DerivePathType derivePathType, }) { @@ -176,7 +171,7 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -191,6 +186,7 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: @@ -206,6 +202,7 @@ class Dogecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: diff --git a/lib/wallets/crypto_currency/coins/ecash.dart b/lib/wallets/crypto_currency/coins/ecash.dart index 4074b249d..c7d99b2d5 100644 --- a/lib/wallets/crypto_currency/coins/ecash.dart +++ b/lib/wallets/crypto_currency/coins/ecash.dart @@ -60,9 +60,9 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - DerivePathType.eCash44, - DerivePathType.bip44, - ]; + DerivePathType.eCash44, + DerivePathType.bip44, + ]; @override String get genesisHash { @@ -77,10 +77,8 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - Amount get dustLimit => Amount( - rawValue: BigInt.from(546), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(546), fractionDigits: fractionDigits); @override coinlib.Network get networkParams { @@ -224,6 +222,17 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface { // Do not validate "p" (P2SH) addresses. } + @override + AddressType? getAddressType(String address) { + final format = bitbox.Address.detectFormat(address); + + return super.getAddressType( + format == bitbox.Address.formatCashAddr + ? bitbox.Address.toLegacyAddress(address) + : address, + ); + } + @override DerivePathType addressType({required String address}) { Uint8List? decodeBase58; @@ -276,7 +285,7 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -291,6 +300,7 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: diff --git a/lib/wallets/crypto_currency/coins/epiccash.dart b/lib/wallets/crypto_currency/coins/epiccash.dart index 25fdd6c9c..b715f9623 100644 --- a/lib/wallets/crypto_currency/coins/epiccash.dart +++ b/lib/wallets/crypto_currency/coins/epiccash.dart @@ -67,7 +67,7 @@ class Epiccash extends Bip39Currency { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -82,6 +82,7 @@ class Epiccash extends Bip39Currency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -114,7 +115,8 @@ class Epiccash extends Bip39Currency { int get targetBlockTimeSeconds => 60; @override - DerivePathType get defaultDerivePathType => throw UnsupportedError( + DerivePathType get defaultDerivePathType => + throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); @@ -127,4 +129,12 @@ class Epiccash extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.mimbleWimble; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/ethereum.dart b/lib/wallets/crypto_currency/coins/ethereum.dart index 1d7bb53ed..448f4c9ff 100644 --- a/lib/wallets/crypto_currency/coins/ethereum.dart +++ b/lib/wallets/crypto_currency/coins/ethereum.dart @@ -4,6 +4,7 @@ import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/node_model.dart'; import '../../../utilities/default_nodes.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../utilities/eth_commons.dart'; import '../crypto_currency.dart'; import '../intermediate/bip39_currency.dart'; @@ -41,25 +42,26 @@ class Ethereum extends Bip39Currency { @override String get ticker => _ticker; - int get gasLimit => 21000; + int get gasLimit => kEthereumMinGasLimit; @override bool get hasTokenSupport => true; @override - NodeModel get defaultNode => NodeModel( - host: "https://eth.stackwallet.com", - port: 443, - name: DefaultNodes.defaultName, - id: DefaultNodes.buildId(this), - useSSL: true, - enabled: true, - coinName: identifier, - isFailover: true, - isDown: false, - torEnabled: true, - clearnetEnabled: true, - ); + NodeModel defaultNode({required bool isPrimary}) => NodeModel( + host: "https://eth2.stackwallet.com", + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + torEnabled: true, + clearnetEnabled: true, + isPrimary: isPrimary, + ); @override // Not used for eth @@ -111,4 +113,12 @@ class Ethereum extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.ethereum; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/fact0rn.dart b/lib/wallets/crypto_currency/coins/fact0rn.dart new file mode 100644 index 000000000..a168c9dec --- /dev/null +++ b/lib/wallets/crypto_currency/coins/fact0rn.dart @@ -0,0 +1,242 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/node_model.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/default_nodes.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../crypto_currency.dart'; +import '../interfaces/electrumx_currency_interface.dart'; +import '../intermediate/bip39_hd_currency.dart'; + +class Fact0rn extends Bip39HDCurrency with ElectrumXCurrencyInterface { + Fact0rn(super.network) { + _idMain = "fact0rn"; + _uriScheme = "fact0rn"; + switch (network) { + case CryptoCurrencyNetwork.main: + _id = _idMain; + _name = "FACT0RN"; + _ticker = "FACT"; + case CryptoCurrencyNetwork.test: + _id = "fact0rnTestNet"; + _name = "tFACT0RN"; + _ticker = "tFACT"; + default: + throw Exception("Unsupported network: $network"); + } + } + + late final String _id; + @override + String get identifier => _id; + + late final String _idMain; + @override + String get mainNetId => _idMain; + + late final String _name; + @override + String get prettyName => _name; + + late final String _uriScheme; + @override + String get uriScheme => _uriScheme; + + late final String _ticker; + @override + String get ticker => _ticker; + + @override + bool get torSupport => false; + + @override + List get supportedDerivationPathTypes => [ + DerivePathType.bip84, + ]; + + @override + String constructDerivePath({ + required DerivePathType derivePathType, + int account = 0, + required int chain, + required int index, + }) { + String coinType; + + switch (networkParams.wifPrefix) { + case 0x80: + coinType = "42069"; // fact0rn mainnet + break; + case 0xef: + coinType = "1"; // fact0rn testnet + break; + default: + throw Exception("Invalid Fact0rn network wif used!"); + } + + int purpose; + switch (derivePathType) { + case DerivePathType.bip84: + purpose = 84; + break; + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + + return "m/$purpose'/$coinType'/$account'/$chain/$index"; + } + + @override + Amount get dustLimit => + Amount(rawValue: BigInt.from(1000), fractionDigits: fractionDigits); + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "79cb40f8075b0e3dc2bc468c5ce2a7acbe0afd36c6c3d3a134ea692edac7de49"; + case CryptoCurrencyNetwork.test: + return "550bbf0a444d9f92189f067dd225f5b8a5d92587ebc2e8398d143236072580af"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey({ + required coinlib.ECPublicKey publicKey, + required DerivePathType derivePathType, + }) { + switch (derivePathType) { + 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 + int get minConfirms => 1; + + @override + coinlib.Network get networkParams { + switch (network) { + case CryptoCurrencyNetwork.main: + return coinlib.Network( + wifPrefix: 0x80, + p2pkhPrefix: 0x00, + p2shPrefix: 0x05, + privHDPrefix: 0x0488ade4, + pubHDPrefix: 0x0488b21e, + bech32Hrp: "fact", + messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // Not used in stack wallet currently + minOutput: dustLimit.raw, // Not used in stack wallet currently + feePerKb: BigInt.from(1), // Not used in stack wallet currently + ); + case CryptoCurrencyNetwork.test: + return coinlib.Network( + wifPrefix: 0xef, + p2pkhPrefix: 0x6f, + p2shPrefix: 0xc4, + privHDPrefix: 0x04358394, + pubHDPrefix: 0x043587cf, + bech32Hrp: "tfact", + messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // Not used in stack wallet currently + minOutput: dustLimit.raw, // Not used in stack wallet currently + feePerKb: BigInt.from(1), // Not used in stack wallet currently + ); + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + bool validateAddress(String address) { + try { + coinlib.Address.fromString(address, networkParams); + return true; + } catch (_) { + return false; + } + } + + @override + NodeModel defaultNode({required bool isPrimary}) { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "electrumx.fact0rn.io", + port: 50002, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + torEnabled: false, + clearnetEnabled: true, + isPrimary: isPrimary, + ); + + default: + throw UnimplementedError(); + } + } + + @override + int get defaultSeedPhraseLength => 12; + + @override + int get fractionDigits => 8; + + @override + bool get hasBuySupport => false; + + @override + bool get hasMnemonicPassphraseSupport => true; + + @override + List get possibleMnemonicLengths => [defaultSeedPhraseLength, 24]; + + @override + AddressType get defaultAddressType => defaultDerivePathType.getAddressType(); + + @override + BigInt get satsPerCoin => BigInt.from(100000000); + + @override + int get targetBlockTimeSeconds => 1800; + + @override + DerivePathType get defaultDerivePathType => DerivePathType.bip84; + + @override + Uri defaultBlockExplorer(String txid) { + switch (network) { + case CryptoCurrencyNetwork.main: + // "https://explorer.fact0rn.io/tx/$txid" doesn't show mempool transactions + return Uri.parse("https://factexplorer.io/tx/$txid"); + default: + throw Exception( + "Unsupported network for defaultBlockExplorer(): $network", + ); + } + } + + @override + int get transactionVersion => 2; + + @override + BigInt get defaultFeeRate => BigInt.from(1000); +} diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index 26957600d..f432bd77b 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -62,8 +62,8 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - DerivePathType.bip44, - ]; + DerivePathType.bip44, + ]; @override String get genesisHash { @@ -78,10 +78,8 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - Amount get dustLimit => Amount( - rawValue: BigInt.from(1000), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(1000), fractionDigits: fractionDigits); Uint8List get exAddressVersion { switch (network) { @@ -207,10 +205,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { bool isExchangeAddress(String address) { try { - EXP2PKHAddress.fromString( - address, - exAddressVersion, - ); + EXP2PKHAddress.fromString(address, exAddressVersion); return true; } catch (_) { return false; @@ -218,7 +213,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -233,25 +228,13 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: - // NodeModel( - // host: "firo-testnet.stackwallet.com", - // port: 50002, - // name: DefaultNodes.defaultName, - // id: _nodeId(Coin.firoTestNet), - // useSSL: true, - // enabled: true, - // coinName: Coin.firoTestNet.name, - // isFailover: true, - // isDown: false, - // ); - - // TODO revert to above eventually return NodeModel( - host: "95.179.164.13", - port: 51002, + host: "firo-testnet.stackwallet.com", + port: 50002, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(this), useSSL: true, @@ -261,6 +244,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index 91b444f73..1b830c4f9 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -56,10 +56,10 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - DerivePathType.bip44, - DerivePathType.bip49, - DerivePathType.bip84, - ]; + DerivePathType.bip44, + DerivePathType.bip49, + DerivePathType.bip84, + ]; @override String get genesisHash { @@ -74,15 +74,11 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - Amount get dustLimit => Amount( - rawValue: BigInt.from(294), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(294), fractionDigits: fractionDigits); - Amount get dustLimitP2PKH => Amount( - rawValue: BigInt.from(546), - fractionDigits: fractionDigits, - ); + Amount get dustLimitP2PKH => + Amount(rawValue: BigInt.from(546), fractionDigits: fractionDigits); @override coinlib.Network get networkParams { @@ -95,6 +91,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x0488ade4, pubHDPrefix: 0x0488b21e, bech32Hrp: "ltc", + mwebBech32Hrp: "ltcmweb", messagePrefix: '\x19Litecoin Signed Message:\n', minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently @@ -108,6 +105,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x04358394, pubHDPrefix: 0x043587cf, bech32Hrp: "tltc", + mwebBech32Hrp: "tmweb", messagePrefix: "\x19Litecoin Signed Message:\n", minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently @@ -171,10 +169,11 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { return (address: addr, addressType: AddressType.p2pkh); case DerivePathType.bip49: - final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( - publicKey, - hrp: networkParams.bech32Hrp, - ).program.script; + final p2wpkhScript = + coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ).program.script; final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, @@ -207,7 +206,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -222,6 +221,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: @@ -237,6 +237,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 4cbeeeb68..4d1535179 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -61,7 +61,7 @@ class Monero extends CryptonoteCurrency { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -77,6 +77,7 @@ class Monero extends CryptonoteCurrency { trusted: true, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -106,7 +107,8 @@ class Monero extends CryptonoteCurrency { int get targetBlockTimeSeconds => 120; @override - DerivePathType get defaultDerivePathType => throw UnsupportedError( + DerivePathType get defaultDerivePathType => + throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); diff --git a/lib/wallets/crypto_currency/coins/namecoin.dart b/lib/wallets/crypto_currency/coins/namecoin.dart index 77940784e..7945e8bca 100644 --- a/lib/wallets/crypto_currency/coins/namecoin.dart +++ b/lib/wallets/crypto_currency/coins/namecoin.dart @@ -89,7 +89,7 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -104,6 +104,7 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); // case CryptoCurrencyNetwork.test: // TODO: [prio=low] Add testnet support. @@ -114,10 +115,8 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override // See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L60 - Amount get dustLimit => Amount( - rawValue: BigInt.from(546), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(546), fractionDigits: fractionDigits); @override // See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L6 @@ -149,10 +148,11 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { return (address: addr, addressType: AddressType.p2pkh); case DerivePathType.bip49: - final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( - publicKey, - hrp: networkParams.bech32Hrp, - ).program.script; + final p2wpkhScript = + coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ).program.script; final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, @@ -200,11 +200,11 @@ class Namecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - // DerivePathType.bip16, - DerivePathType.bip44, - DerivePathType.bip49, - DerivePathType.bip84, - ]; + // DerivePathType.bip16, + DerivePathType.bip44, + DerivePathType.bip49, + DerivePathType.bip84, + ]; @override bool validateAddress(String address) { diff --git a/lib/wallets/crypto_currency/coins/nano.dart b/lib/wallets/crypto_currency/coins/nano.dart index 0909b8aa8..77fc9e3a7 100644 --- a/lib/wallets/crypto_currency/coins/nano.dart +++ b/lib/wallets/crypto_currency/coins/nano.dart @@ -45,9 +45,7 @@ class Nano extends NanoCurrency { int get fractionDigits => 30; @override - BigInt get satsPerCoin => BigInt.parse( - "1000000000000000000000000000000", - ); // 1*10^30 + BigInt get satsPerCoin => BigInt.parse("1000000000000000000000000000000"); // 1*10^30 @override int get minConfirms => 1; @@ -63,7 +61,7 @@ class Nano extends NanoCurrency { int get nanoAccountType => NanoAccountType.NANO; @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -79,6 +77,7 @@ class Nano extends NanoCurrency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -87,7 +86,8 @@ class Nano extends NanoCurrency { } @override - DerivePathType get defaultDerivePathType => throw UnsupportedError( + DerivePathType get defaultDerivePathType => + throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); @@ -95,11 +95,19 @@ class Nano extends NanoCurrency { Uri defaultBlockExplorer(String txid) { switch (network) { case CryptoCurrencyNetwork.main: - return Uri.parse("https://www.nanolooker.com/block/$txid"); + return Uri.parse("https://nanexplorer.com/nano/blocks/$txid"); default: throw Exception( "Unsupported network for defaultBlockExplorer(): $network", ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.nano; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/particl.dart b/lib/wallets/crypto_currency/coins/particl.dart index 8788b6114..2b07aad7e 100644 --- a/lib/wallets/crypto_currency/coins/particl.dart +++ b/lib/wallets/crypto_currency/coins/particl.dart @@ -84,7 +84,7 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -99,6 +99,7 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); // case CryptoCurrencyNetwork.test: // TODO: [prio=low] Add testnet. @@ -109,10 +110,8 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override // See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L58 - Amount get dustLimit => Amount( - rawValue: BigInt.from(294), - fractionDigits: fractionDigits, - ); + Amount get dustLimit => + Amount(rawValue: BigInt.from(294), fractionDigits: fractionDigits); @override // See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L63 @@ -180,9 +179,9 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - DerivePathType.bip44, - DerivePathType.bip84, - ]; + DerivePathType.bip44, + DerivePathType.bip84, + ]; @override bool validateAddress(String address) { diff --git a/lib/wallets/crypto_currency/coins/peercoin.dart b/lib/wallets/crypto_currency/coins/peercoin.dart index b67b0e1ab..0515beb4c 100644 --- a/lib/wallets/crypto_currency/coins/peercoin.dart +++ b/lib/wallets/crypto_currency/coins/peercoin.dart @@ -90,7 +90,7 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -105,6 +105,7 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: @@ -120,6 +121,7 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -129,10 +131,10 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override Amount get dustLimit => Amount( - // TODO should this be 10000 instead of 294 for peercoin? - rawValue: BigInt.from(294), - fractionDigits: fractionDigits, - ); + // TODO should this be 10000 instead of 294 for peercoin? + rawValue: BigInt.from(294), + fractionDigits: fractionDigits, + ); @override String get genesisHash { @@ -163,10 +165,11 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { return (address: addr, addressType: AddressType.p2pkh); case DerivePathType.bip49: - final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( - publicKey, - hrp: networkParams.bech32Hrp, - ).program.script; + final p2wpkhScript = + coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ).program.script; final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, @@ -202,9 +205,9 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override List get supportedDerivationPathTypes => [ - DerivePathType.bip44, - DerivePathType.bip84, - ]; + DerivePathType.bip44, + DerivePathType.bip84, + ]; @override bool validateAddress(String address) { diff --git a/lib/wallets/crypto_currency/coins/salvium.dart b/lib/wallets/crypto_currency/coins/salvium.dart new file mode 100644 index 000000000..db794dc53 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/salvium.dart @@ -0,0 +1,126 @@ +import 'package:cs_salvium/src/ffi_bindings/salvium_wallet_bindings.dart' + as sal_wallet_ffi; + +import '../../../models/node_model.dart'; +import '../../../utilities/default_nodes.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../crypto_currency.dart'; +import '../intermediate/cryptonote_currency.dart'; + +class Salvium extends CryptonoteCurrency { + Salvium(super.network) { + _idMain = "salvium"; + _uriScheme = "salvium"; + switch (network) { + case CryptoCurrencyNetwork.main: + _id = _idMain; + _name = "Salvium"; + _ticker = "SAL"; + default: + throw Exception("Unsupported network: $network"); + } + } + + late final String _id; + @override + String get identifier => _id; + + late final String _idMain; + @override + String get mainNetId => _idMain; + + late final String _name; + @override + String get prettyName => _name; + + late final String _uriScheme; + @override + String get uriScheme => _uriScheme; + + late final String _ticker; + @override + String get ticker => _ticker; + + @override + int get minConfirms => 10; + + @override + bool get torSupport => true; + + @override + bool validateAddress(String address) { + if (address.contains("111")) { + return false; + } + switch (network) { + case CryptoCurrencyNetwork.main: + return sal_wallet_ffi.validateAddress(address, 0); + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + NodeModel defaultNode({required bool isPrimary}) { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: "https://salvium.stackwallet.com", + port: 19081, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + trusted: true, + torEnabled: true, + clearnetEnabled: true, + isPrimary: isPrimary, + ); + + default: + throw UnimplementedError(); + } + } + + @override + int get defaultSeedPhraseLength => 25; + + @override + int get fractionDigits => 8; + + @override + bool get hasBuySupport => false; + + @override + bool get hasMnemonicPassphraseSupport => false; + + @override + List get possibleMnemonicLengths => [defaultSeedPhraseLength]; + + @override + BigInt get satsPerCoin => BigInt.from(100000000); + + @override + int get targetBlockTimeSeconds => 120; + + @override + DerivePathType get defaultDerivePathType => + throw UnsupportedError( + "$runtimeType does not use bitcoin style derivation paths", + ); + + @override + Uri defaultBlockExplorer(String txid) { + switch (network) { + case CryptoCurrencyNetwork.main: + return Uri.parse("https://explorer.salvium.io/tx/$txid"); + default: + throw Exception( + "Unsupported network for defaultBlockExplorer(): $network", + ); + } + } +} diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart index 1db985c76..03331a922 100644 --- a/lib/wallets/crypto_currency/coins/solana.dart +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -42,7 +42,7 @@ class Solana extends Bip39Currency { String get ticker => _ticker; @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -57,6 +57,7 @@ class Solana extends Bip39Currency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: throw Exception("Unsupported network: $network"); @@ -121,4 +122,12 @@ class Solana extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.solana; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/stellar.dart b/lib/wallets/crypto_currency/coins/stellar.dart index d799de31b..1aa701f87 100644 --- a/lib/wallets/crypto_currency/coins/stellar.dart +++ b/lib/wallets/crypto_currency/coins/stellar.dart @@ -50,12 +50,10 @@ class Stellar extends Bip39Currency { bool get torSupport => true; @override - String get genesisHash => throw UnimplementedError( - "Not used for stellar", - ); + String get genesisHash => throw UnimplementedError("Not used for stellar"); @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -70,6 +68,7 @@ class Stellar extends Bip39Currency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: @@ -85,6 +84,7 @@ class Stellar extends Bip39Currency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -115,15 +115,14 @@ class Stellar extends Bip39Currency { AddressType get defaultAddressType => AddressType.stellar; @override - BigInt get satsPerCoin => BigInt.from( - 10000000, - ); // https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/assets#amount-precision + BigInt get satsPerCoin => BigInt.from(10000000); // https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/assets#amount-precision @override int get targetBlockTimeSeconds => 5; @override - DerivePathType get defaultDerivePathType => throw UnsupportedError( + DerivePathType get defaultDerivePathType => + throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); @@ -140,4 +139,12 @@ class Stellar extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.stellar; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart index 0cb8cca0c..179ae2ce1 100644 --- a/lib/wallets/crypto_currency/coins/tezos.dart +++ b/lib/wallets/crypto_currency/coins/tezos.dart @@ -54,11 +54,11 @@ class Tezos extends Bip39Currency { DerivationPath()..value = "m/44'/1729'/0'/0'"; static List get possibleDerivationPaths => [ - standardDerivationPath, - DerivationPath()..value = "", - DerivationPath()..value = "m/44'/1729'/0'/0'/0'", - DerivationPath()..value = "m/44'/1729'/0'/0/0", - ]; + standardDerivationPath, + DerivationPath()..value = "", + DerivationPath()..value = "m/44'/1729'/0'/0'/0'", + DerivationPath()..value = "m/44'/1729'/0'/0/0", + ]; static Keystore mnemonicToKeyStore({ required String mnemonic, @@ -88,9 +88,8 @@ class Tezos extends Bip39Currency { // =========== Overrides ===================================================== @override - String get genesisHash => throw UnimplementedError( - "Not used in tezos at the moment", - ); + String get genesisHash => + throw UnimplementedError("Not used in tezos at the moment"); @override int get minConfirms => 1; @@ -104,7 +103,7 @@ class Tezos extends Bip39Currency { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -120,6 +119,7 @@ class Tezos extends Bip39Currency { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -220,4 +220,12 @@ class Tezos extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.tezos; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/wownero.dart b/lib/wallets/crypto_currency/coins/wownero.dart index 2aea90aa8..0c702af40 100644 --- a/lib/wallets/crypto_currency/coins/wownero.dart +++ b/lib/wallets/crypto_currency/coins/wownero.dart @@ -61,7 +61,7 @@ class Wownero extends CryptonoteCurrency { } @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -77,6 +77,7 @@ class Wownero extends CryptonoteCurrency { trusted: true, torEnabled: true, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -106,7 +107,8 @@ class Wownero extends CryptonoteCurrency { int get targetBlockTimeSeconds => 120; @override - DerivePathType get defaultDerivePathType => throw UnsupportedError( + DerivePathType get defaultDerivePathType => + throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); diff --git a/lib/wallets/crypto_currency/coins/xelis.dart b/lib/wallets/crypto_currency/coins/xelis.dart index b05fce216..62b33326b 100644 --- a/lib/wallets/crypto_currency/coins/xelis.dart +++ b/lib/wallets/crypto_currency/coins/xelis.dart @@ -1,3 +1,5 @@ +import 'package:xelis_flutter/src/api/utils.dart' as x_utils; + import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/node_model.dart'; import '../../../utilities/default_nodes.dart'; @@ -5,8 +7,6 @@ import '../../../utilities/enums/derive_path_type_enum.dart'; import '../crypto_currency.dart'; import '../intermediate/electrum_currency.dart'; -import 'package:xelis_flutter/src/api/utils.dart' as x_utils; - class Xelis extends ElectrumCurrency { Xelis(super.network) { _idMain = "xelis"; @@ -46,7 +46,7 @@ class Xelis extends ElectrumCurrency { String get ticker => _ticker; @override - NodeModel get defaultNode { + NodeModel defaultNode({required bool isPrimary}) { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( @@ -61,6 +61,7 @@ class Xelis extends ElectrumCurrency { isDown: false, torEnabled: false, clearnetEnabled: true, + isPrimary: isPrimary, ); case CryptoCurrencyNetwork.test: @@ -76,6 +77,7 @@ class Xelis extends ElectrumCurrency { isDown: false, torEnabled: false, clearnetEnabled: true, + isPrimary: isPrimary, ); default: @@ -139,4 +141,12 @@ class Xelis extends ElectrumCurrency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.xelis; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index d5553ceca..0c2f2cb83 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -12,6 +12,7 @@ export 'coins/dogecoin.dart'; export 'coins/ecash.dart'; export 'coins/epiccash.dart'; export 'coins/ethereum.dart'; +export 'coins/fact0rn.dart'; export 'coins/firo.dart'; export 'coins/litecoin.dart'; export 'coins/monero.dart'; @@ -19,6 +20,7 @@ export 'coins/namecoin.dart'; export 'coins/nano.dart'; export 'coins/particl.dart'; export 'coins/peercoin.dart'; +export 'coins/salvium.dart'; export 'coins/solana.dart'; export 'coins/stellar.dart'; export 'coins/tezos.dart'; @@ -67,8 +69,9 @@ abstract class CryptoCurrency { String get genesisHash; bool validateAddress(String address); + AddressType? getAddressType(String address); - NodeModel get defaultNode; + NodeModel defaultNode({required bool isPrimary}); int get defaultSeedPhraseLength; int get fractionDigits; diff --git a/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart b/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart index 387bf4454..70929af41 100644 --- a/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart +++ b/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart @@ -1,3 +1,8 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; +import 'package:flutter/foundation.dart'; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../utilities/logger.dart'; import '../intermediate/bip39_hd_currency.dart'; mixin ElectrumXCurrencyInterface on Bip39HDCurrency { @@ -5,4 +10,36 @@ mixin ElectrumXCurrencyInterface on Bip39HDCurrency { /// The default fee rate in satoshis per kilobyte. BigInt get defaultFeeRate; + + @override + AddressType? getAddressType(String address) { + try { + final clAddress = cl.Address.fromString(address, networkParams); + + Logging.instance.t( + "getAddressType($address) type is ${clAddress.runtimeType}", + ); + + return switch (clAddress) { + cl.P2PKHAddress() => AddressType.p2pkh, + cl.P2SHAddress() => AddressType.p2sh, + cl.P2WPKHAddress() => AddressType.p2wpkh, + cl.P2TRAddress() => AddressType.p2tr, + cl.MwebAddress() => AddressType.mweb, + _ => null, + }; + } catch (e, s) { + if (kDebugMode) { + Logging.instance.e( + "getAddressType($address) failed", + error: e, + stackTrace: s, + ); + } else { + Logging.instance.t("getAddressType($address) failed"); + } + + return null; + } + } } diff --git a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart index 319a501ac..a0069e09b 100644 --- a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart @@ -13,4 +13,12 @@ abstract class CryptonoteCurrency extends CryptoCurrency @override AddressType get defaultAddressType => AddressType.cryptonote; + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.cryptonote; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/intermediate/nano_currency.dart b/lib/wallets/crypto_currency/intermediate/nano_currency.dart index a04cd57a0..a553a6d6c 100644 --- a/lib/wallets/crypto_currency/intermediate/nano_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/nano_currency.dart @@ -1,4 +1,5 @@ import 'package:nanodart/nanodart.dart'; + import 'bip39_currency.dart'; abstract class NanoCurrency extends Bip39Currency { @@ -24,13 +25,10 @@ abstract class NanoCurrency extends Bip39Currency { List get possibleMnemonicLengths => [defaultSeedPhraseLength, 12]; @override - bool validateAddress(String address) => NanoAccounts.isValid( - nanoAccountType, - address, - ); + bool validateAddress(String address) => + NanoAccounts.isValid(nanoAccountType, address); @override - String get genesisHash => throw UnimplementedError( - "Not used in nano based coins", - ); + String get genesisHash => + throw UnimplementedError("Not used in nano based coins"); } diff --git a/lib/wallets/isar/models/spark_coin.dart b/lib/wallets/isar/models/spark_coin.dart index 9501bf06d..c16cee253 100644 --- a/lib/wallets/isar/models/spark_coin.dart +++ b/lib/wallets/isar/models/spark_coin.dart @@ -17,13 +17,7 @@ enum SparkCoinType { class SparkCoin { Id id = Isar.autoIncrement; - @Index( - unique: true, - replace: true, - composite: [ - CompositeIndex("lTagHash"), - ], - ) + @Index(unique: true, replace: true, composite: [CompositeIndex("lTagHash")]) final String walletId; @enumerated @@ -55,6 +49,10 @@ class SparkCoin { final String? serializedCoinB64; final String? contextB64; + // prefix name with zzz to ensure serialization order remains unchanged + @Name("zzzIsLocked") + final bool? isLocked; + @ignore BigInt get value => BigInt.parse(valueIntString); @@ -66,10 +64,7 @@ class SparkCoin { return max(0, currentChainHeight - (height! - 1)); } - bool isConfirmed( - int currentChainHeight, - int minimumConfirms, - ) { + bool isConfirmed(int currentChainHeight, int minimumConfirms) { final confirmations = getConfirmations(currentChainHeight); return confirmations >= minimumConfirms; } @@ -93,6 +88,7 @@ class SparkCoin { this.height, this.serializedCoinB64, this.contextB64, + this.isLocked, }); SparkCoin copyWith({ @@ -113,6 +109,7 @@ class SparkCoin { int? height, String? serializedCoinB64, String? contextB64, + bool? isLocked, }) { return SparkCoin( walletId: walletId, @@ -134,6 +131,7 @@ class SparkCoin { height: height ?? this.height, serializedCoinB64: serializedCoinB64 ?? this.serializedCoinB64, contextB64: contextB64 ?? this.contextB64, + isLocked: isLocked ?? this.isLocked, ); } @@ -158,6 +156,7 @@ class SparkCoin { ', height: $height' ', serializedCoinB64: $serializedCoinB64' ', contextB64: $contextB64' + ', isLocked: $isLocked' ')'; } } diff --git a/lib/wallets/isar/models/spark_coin.g.dart b/lib/wallets/isar/models/spark_coin.g.dart index 717d1a2c6..c84e0db40 100644 --- a/lib/wallets/isar/models/spark_coin.g.dart +++ b/lib/wallets/isar/models/spark_coin.g.dart @@ -107,6 +107,11 @@ const SparkCoinSchema = CollectionSchema( id: 17, name: r'walletId', type: IsarType.string, + ), + r'zzzIsLocked': PropertySchema( + id: 18, + name: r'zzzIsLocked', + type: IsarType.bool, ) }, estimateSize: _sparkCoinEstimateSize, @@ -229,6 +234,7 @@ void _sparkCoinSerialize( writer.writeByte(offsets[15], object.type.index); writer.writeString(offsets[16], object.valueIntString); writer.writeString(offsets[17], object.walletId); + writer.writeBool(offsets[18], object.isLocked); } SparkCoin _sparkCoinDeserialize( @@ -257,6 +263,7 @@ SparkCoin _sparkCoinDeserialize( SparkCoinType.mint, valueIntString: reader.readString(offsets[16]), walletId: reader.readString(offsets[17]), + isLocked: reader.readBoolOrNull(offsets[18]), ); object.id = id; return object; @@ -306,6 +313,8 @@ P _sparkCoinDeserializeProp

( return (reader.readString(offset)) as P; case 17: return (reader.readString(offset)) as P; + case 18: + return (reader.readBoolOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -2867,6 +2876,33 @@ extension SparkCoinQueryFilter )); }); } + + QueryBuilder isLockedIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'zzzIsLocked', + )); + }); + } + + QueryBuilder + isLockedIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'zzzIsLocked', + )); + }); + } + + QueryBuilder isLockedEqualTo( + bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'zzzIsLocked', + value: value, + )); + }); + } } extension SparkCoinQueryObject @@ -3034,6 +3070,18 @@ extension SparkCoinQuerySortBy on QueryBuilder { return query.addSortBy(r'walletId', Sort.desc); }); } + + QueryBuilder sortByIsLocked() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'zzzIsLocked', Sort.asc); + }); + } + + QueryBuilder sortByIsLockedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'zzzIsLocked', Sort.desc); + }); + } } extension SparkCoinQuerySortThenBy @@ -3208,6 +3256,18 @@ extension SparkCoinQuerySortThenBy return query.addSortBy(r'walletId', Sort.desc); }); } + + QueryBuilder thenByIsLocked() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'zzzIsLocked', Sort.asc); + }); + } + + QueryBuilder thenByIsLockedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'zzzIsLocked', Sort.desc); + }); + } } extension SparkCoinQueryWhereDistinct @@ -3332,6 +3392,12 @@ extension SparkCoinQueryWhereDistinct return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); }); } + + QueryBuilder distinctByIsLocked() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'zzzIsLocked'); + }); + } } extension SparkCoinQueryProperty @@ -3453,4 +3519,10 @@ extension SparkCoinQueryProperty return query.addPropertyName(r'walletId'); }); } + + QueryBuilder isLockedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'zzzIsLocked'); + }); + } } diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 779f1dd78..6df8dc456 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -114,9 +114,10 @@ class WalletInfo implements IsarId { } @ignore - Map get otherData => otherDataJsonString == null - ? {} - : Map.from(jsonDecode(otherDataJsonString!) as Map); + Map get otherData => + otherDataJsonString == null + ? {} + : Map.from(jsonDecode(otherDataJsonString!) as Map); @ignore bool get isViewOnly => @@ -134,9 +135,29 @@ class WalletInfo implements IsarId { ?.isMnemonicVerified == true; + @ignore + bool get isDuressVisible => + otherData[WalletInfoKeys.duressMarkedVisibleWalletKey] as bool? ?? false; + + @ignore + bool get isMwebEnabled => + otherData[WalletInfoKeys.mwebEnabled] as bool? ?? false; + //============================================================================ //============= Updaters ================================================ + Future updateDuressVisibilityStatus({ + required bool isDuressVisible, + required Isar isar, + }) async { + await updateOtherData( + newEntries: { + WalletInfoKeys.duressMarkedVisibleWalletKey: isDuressVisible, + }, + isar: isar, + ); + } + Future updateBalance({ required Balance newBalance, required Isar isar, @@ -151,9 +172,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedBalanceString: newEncoded, - ), + thisInfo.copyWith(cachedBalanceString: newEncoded), ); }); } @@ -173,9 +192,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedBalanceSecondaryString: newEncoded, - ), + thisInfo.copyWith(cachedBalanceSecondaryString: newEncoded), ); }); } @@ -195,9 +212,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedBalanceTertiaryString: newEncoded, - ), + thisInfo.copyWith(cachedBalanceTertiaryString: newEncoded), ); }); } @@ -215,9 +230,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedChainHeight: newHeight, - ), + thisInfo.copyWith(cachedChainHeight: newHeight), ); }); } @@ -235,11 +248,12 @@ class WalletInfo implements IsarId { if (customIndexOverride != null) { index = customIndexOverride; } else if (flag) { - final highest = await isar.walletInfo - .where() - .sortByFavouriteOrderIndexDesc() - .favouriteOrderIndexProperty() - .findFirst(); + final highest = + await isar.walletInfo + .where() + .sortByFavouriteOrderIndexDesc() + .favouriteOrderIndexProperty() + .findFirst(); index = (highest ?? 0) + 1; } else { index = -1; @@ -253,19 +267,14 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - favouriteOrderIndex: index, - ), + thisInfo.copyWith(favouriteOrderIndex: index), ); }); } } /// copies this with a new name and updates the db - Future updateName({ - required String newName, - required Isar isar, - }) async { + Future updateName({required String newName, required Isar isar}) async { // don't allow empty names if (newName.isEmpty) { throw Exception("Empty wallet name not allowed!"); @@ -278,11 +287,7 @@ class WalletInfo implements IsarId { if (thisInfo.name != newName) { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); - await isar.walletInfo.put( - thisInfo.copyWith( - name: newName, - ), - ); + await isar.walletInfo.put(thisInfo.copyWith(name: newName)); }); } } @@ -299,9 +304,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - cachedReceivingAddress: newAddress, - ), + thisInfo.copyWith(cachedReceivingAddress: newAddress), ); }); } @@ -325,37 +328,27 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - otherDataJsonString: encodedNew, - ), + thisInfo.copyWith(otherDataJsonString: encodedNew), ); }); } } /// Can be dangerous. Don't use unless you know the consequences - Future setMnemonicVerified({ - required Isar isar, - }) async { + Future setMnemonicVerified({required Isar isar}) async { final meta = await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst(); if (meta == null) { await isar.writeTxn(() async { await isar.walletInfoMeta.put( - WalletInfoMeta( - walletId: walletId, - isMnemonicVerified: true, - ), + WalletInfoMeta(walletId: walletId, isMnemonicVerified: true), ); }); } else if (meta.isMnemonicVerified == false) { await isar.writeTxn(() async { await isar.walletInfoMeta.deleteByWalletId(walletId); await isar.walletInfoMeta.put( - WalletInfoMeta( - walletId: walletId, - isMnemonicVerified: true, - ), + WalletInfoMeta(walletId: walletId, isMnemonicVerified: true), ); }); } else { @@ -384,9 +377,7 @@ class WalletInfo implements IsarId { await isar.writeTxn(() async { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( - thisInfo.copyWith( - restoreHeight: newRestoreHeight, - ), + thisInfo.copyWith(restoreHeight: newRestoreHeight), ); }); } @@ -405,6 +396,16 @@ class WalletInfo implements IsarId { ); } + Future setMwebEnabled({ + required bool newValue, + required Isar isar, + }) async { + await updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: newValue}, + isar: isar, + ); + } + //============================================================================ WalletInfo({ @@ -423,9 +424,7 @@ class WalletInfo implements IsarId { this.cachedBalanceSecondaryString, this.cachedBalanceTertiaryString, this.otherDataJsonString, - }) : assert( - AppConfig.coins.map((e) => e.identifier).contains(coinName), - ); + }) : assert(AppConfig.coins.map((e) => e.identifier).contains(coinName)); WalletInfo copyWith({ String? name, @@ -481,9 +480,7 @@ class WalletInfo implements IsarId { Map jsonObject, AddressType mainAddressType, ) { - final coin = AppConfig.getCryptoCurrencyFor( - jsonObject["coin"] as String, - )!; + final coin = AppConfig.getCryptoCurrencyFor(jsonObject["coin"] as String)!; return WalletInfo( coinName: coin.identifier, walletId: jsonObject["id"] as String, @@ -494,11 +491,7 @@ class WalletInfo implements IsarId { @Deprecated("Legacy support") Map toMap() { - return { - "name": name, - "id": walletId, - "coin": coin.identifier, - }; + return {"name": name, "id": walletId, "coin": coin.identifier}; } @Deprecated("Legacy support") @@ -518,13 +511,14 @@ abstract class WalletInfoKeys { static const String bananoMonkeyImageBytes = "monkeyImageBytesKey"; static const String tezosDerivationPath = "tezosDerivationPathKey"; static const String xelisDerivationPath = "xelisDerivationPathKey"; - static const String lelantusCoinIsarRescanRequired = - "lelantusCoinIsarRescanRequired"; - static const String enableLelantusScanning = "enableLelantusScanningKey"; static const String firoSparkCacheSetBlockHashCache = "firoSparkCacheSetBlockHashCacheKey"; static const String enableOptInRbf = "enableOptInRbfKey"; static const String reuseAddress = "reuseAddressKey"; static const String isViewOnlyKey = "isViewOnlyKey"; static const String viewOnlyTypeIndexKey = "viewOnlyTypeIndexKey"; + static const String duressMarkedVisibleWalletKey = + "duressMarkedVisibleWalletKey"; + static const String mwebEnabled = "mwebEnabledKey"; + static const String mwebScanHeight = "mwebScanHeightKey"; } diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index 5e93564c0..3dda37c77 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -270,6 +270,8 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'solana': 15, 'cardanoShelley': 16, 'xelis': 17, + 'fact0rn': 18, + 'mweb': 19, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -290,6 +292,8 @@ const _WalletInfomainAddressTypeValueEnumMap = { 15: AddressType.solana, 16: AddressType.cardanoShelley, 17: AddressType.xelis, + 18: AddressType.fact0rn, + 19: AddressType.mweb, }; Id _walletInfoGetId(WalletInfo object) { diff --git a/lib/wallets/isar/providers/all_wallets_info_provider.dart b/lib/wallets/isar/providers/all_wallets_info_provider.dart index 0acbae46c..2bdb932ca 100644 --- a/lib/wallets/isar/providers/all_wallets_info_provider.dart +++ b/lib/wallets/isar/providers/all_wallets_info_provider.dart @@ -3,20 +3,30 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import '../../../providers/db/main_db_provider.dart'; + import '../../../app_config.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../providers/global/duress_provider.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../models/wallet_info.dart'; final pAllWalletsInfo = Provider((ref) { - return ref.watch(_pAllWalletsInfo.select((value) => value.value)); + final duress = ref.watch(pDuress); + + final results = ref.watch(_pAllWalletsInfo.select((value) => value.value)); + + if (duress) { + results.retainWhere((e) => e.isDuressVisible); + } + + return results; }); final pAllWalletsInfoByCoin = Provider((ref) { final infos = ref.watch(pAllWalletsInfo); final Map wallets})> - map = {}; + map = {}; for (final info in infos) { if (map[info.coin] == null) { @@ -41,6 +51,7 @@ _WalletInfoWatcher? _globalInstance; final _pAllWalletsInfo = ChangeNotifierProvider((ref) { if (_globalInstance == null) { final isar = ref.watch(mainDBProvider).isar; + _globalInstance = _WalletInfoWatcher( isar.walletInfo .where() @@ -65,21 +76,22 @@ class _WalletInfoWatcher extends ChangeNotifier { List get value => _value; _WalletInfoWatcher(this._value, Isar isar) { - _streamSubscription = - isar.walletInfo.watchLazy(fireImmediately: true).listen((event) { - isar.walletInfo - .where() - .filter() - .anyOf( - AppConfig.coins.map((e) => e.identifier), - (q, element) => q.coinNameMatches(element), - ) - .findAll() - .then((value) { - _value = value; - notifyListeners(); - }); - }); + _streamSubscription = isar.walletInfo + .watchLazy(fireImmediately: true) + .listen((event) { + isar.walletInfo + .where() + .filter() + .anyOf( + AppConfig.coins.map((e) => e.identifier), + (q, element) => q.coinNameMatches(element), + ) + .findAll() + .then((value) { + _value = value; + notifyListeners(); + }); + }); } @override diff --git a/lib/wallets/isar/providers/favourite_wallets_provider.dart b/lib/wallets/isar/providers/favourite_wallets_provider.dart index 1f7eb798e..8a0b2c0ef 100644 --- a/lib/wallets/isar/providers/favourite_wallets_provider.dart +++ b/lib/wallets/isar/providers/favourite_wallets_provider.dart @@ -3,8 +3,10 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import '../../../providers/db/main_db_provider.dart'; + import '../../../app_config.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../providers/global/duress_provider.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../models/wallet_info.dart'; @@ -27,9 +29,9 @@ class _Watcher extends ChangeNotifier { .sortByFavouriteOrderIndex() .watch(fireImmediately: true) .listen((event) { - _value = event; - notifyListeners(); - }); + _value = event; + notifyListeners(); + }); } @override @@ -39,32 +41,42 @@ class _Watcher extends ChangeNotifier { } } -final _wiProvider = ChangeNotifierProvider.family<_Watcher, bool>( - (ref, isFavourite) { - final isar = ref.watch(mainDBProvider).isar; +final _wiProvider = ChangeNotifierProvider.family<_Watcher, bool>(( + ref, + isFavourite, +) { + final isar = ref.watch(mainDBProvider).isar; + + final watcher = _Watcher( + isar.walletInfo + .filter() + .anyOf( + AppConfig.coins.map((e) => e.identifier), + (q, element) => q.coinNameMatches(element), + ) + .isFavouriteEqualTo(isFavourite) + .sortByFavouriteOrderIndex() + .findAllSync(), + isFavourite, + isar, + ); + + ref.onDispose(() => watcher.dispose()); + + return watcher; +}); - final watcher = _Watcher( - isar.walletInfo - .filter() - .anyOf( - AppConfig.coins.map((e) => e.identifier), - (q, element) => q.coinNameMatches(element), - ) - .isFavouriteEqualTo(isFavourite) - .sortByFavouriteOrderIndex() - .findAllSync(), - isFavourite, - isar, - ); +final pFavouriteWalletInfos = Provider.family, bool>(( + ref, + isFavourite, +) { + final isDuress = ref.watch(pDuress); - ref.onDispose(() => watcher.dispose()); + final infos = ref.watch(_wiProvider(isFavourite)).value; - return watcher; - }, -); + if (isDuress) { + infos.retainWhere((e) => e.isDuressVisible); + } -final pFavouriteWalletInfos = Provider.family, bool>( - (ref, isFavourite) { - return ref.watch(_wiProvider(isFavourite)).value; - }, -); + return infos; +}); diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 94474e390..9a8dffd21 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -1,20 +1,38 @@ import 'package:cs_monero/cs_monero.dart' as lib_monero; +import 'package:cs_salvium/cs_salvium.dart' as lib_salvium; import 'package:tezart/tezart.dart' as tezart; import 'package:web3dart/web3dart.dart' as web3dart; +import '../../models/input.dart'; import '../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../widgets/eth_fee_form.dart'; import '../isar/models/spark_coin.dart'; import 'name_op_state.dart'; - -typedef TxRecipient = ({String address, Amount amount, bool isChange}); +import 'tx_recipient.dart'; + +export 'tx_recipient.dart'; + +enum TxType { + regular, + mweb, + mwebPegIn, + mwebPegOut; + + bool isMweb() => switch (this) { + TxType.mweb => true, + TxType.mwebPegIn => true, + TxType.mwebPegOut => true, + _ => false, + }; +} class TxData { final FeeRateType? feeRateType; - final int? feeRateAmount; + final BigInt? feeRateAmount; final int? satsPerVByte; final Amount? fee; @@ -31,8 +49,8 @@ class TxData { final String? memo; final List? recipients; - final Set? utxos; - final List? usedUTXOs; + final Set? utxos; + final List? usedUTXOs; final String? changeAddress; @@ -43,36 +61,32 @@ class TxData { // paynym specific final PaynymAccountLite? paynymAccountLite; - // eth token specific + // eth & token specific + final EthEIP1559Fee? ethEIP1559Fee; final web3dart.Transaction? web3dartTransaction; final int? nonce; final BigInt? chainId; - final BigInt? feeInWei; - // wownero and monero specific final lib_monero.PendingTransaction? pendingTransaction; - // firo lelantus specific - final int? jMintValue; - final List? spendCoinIndexes; - final int? height; - final TransactionType? txType; - final TransactionSubType? txSubType; - final List>? mintsMapLelantus; + // salvium + final lib_salvium.PendingTransaction? pendingSalviumTransaction; // tezos specific final tezart.OperationsList? tezosOperationsList; // firo spark specific - final List< - ({ - String address, - Amount amount, - String memo, - bool isChange, - })>? sparkRecipients; + final List<({String address, Amount amount, String memo, bool isChange})>? + sparkRecipients; final List? sparkMints; final List? usedSparkCoins; + final ({ + String additionalInfo, + String name, + Address sparkAddress, + int validBlocks, + })? + sparkNameInfo; // xelis specific final String? otherData; @@ -84,6 +98,8 @@ class TxData { // Namecoin Name related final NameOpState? opNameState; + final TxType type; + TxData({ this.feeRateType, this.feeRateAmount, @@ -103,17 +119,12 @@ class TxData { this.frostMSConfig, this.frostSigners, this.paynymAccountLite, + this.ethEIP1559Fee, this.web3dartTransaction, this.nonce, this.chainId, - this.feeInWei, this.pendingTransaction, - this.jMintValue, - this.spendCoinIndexes, - this.height, - this.txType, - this.txSubType, - this.mintsMapLelantus, + this.pendingSalviumTransaction, this.tezosOperationsList, this.sparkRecipients, this.otherData, @@ -122,6 +133,8 @@ class TxData { this.tempTx, this.ignoreCachedBalanceChecks = false, this.opNameState, + this.sparkNameInfo, + this.type = TxType.regular, }); Amount? get amount { @@ -139,6 +152,14 @@ class TxData { ); } } + if (pendingSalviumTransaction?.amount != null && fee != null) { + if (pendingSalviumTransaction!.amount + fee!.raw == sum.raw) { + return Amount( + rawValue: pendingSalviumTransaction!.amount, + fractionDigits: recipients!.first.amount.fractionDigits, + ); + } + } return sum; } @@ -175,6 +196,14 @@ class TxData { ); } } + if (pendingSalviumTransaction?.amount != null && fee != null) { + if (pendingSalviumTransaction!.amount + fee!.raw == sum.raw) { + return Amount( + rawValue: pendingSalviumTransaction!.amount, + fractionDigits: recipients!.first.amount.fractionDigits, + ); + } + } return sum; } @@ -201,13 +230,14 @@ class TxData { } } - int? get estimatedSatsPerVByte => fee != null && vSize != null - ? (fee!.raw ~/ BigInt.from(vSize!)).toInt() - : null; + int? get estimatedSatsPerVByte => + fee != null && vSize != null + ? (fee!.raw ~/ BigInt.from(vSize!)).toInt() + : null; TxData copyWith({ FeeRateType? feeRateType, - int? feeRateAmount, + BigInt? feeRateAmount, int? satsPerVByte, Amount? fee, int? vSize, @@ -218,18 +248,19 @@ class TxData { String? noteOnChain, String? memo, String? otherData, - Set? utxos, - List? usedUTXOs, + Set? utxos, + List? usedUTXOs, List? recipients, String? frostMSConfig, List? frostSigners, String? changeAddress, PaynymAccountLite? paynymAccountLite, + EthEIP1559Fee? ethEIP1559Fee, web3dart.Transaction? web3dartTransaction, int? nonce, BigInt? chainId, - BigInt? feeInWei, lib_monero.PendingTransaction? pendingTransaction, + lib_salvium.PendingTransaction? pendingSalviumTransaction, int? jMintValue, List? spendCoinIndexes, int? height, @@ -237,19 +268,21 @@ class TxData { TransactionSubType? txSubType, List>? mintsMapLelantus, tezart.OperationsList? tezosOperationsList, - List< - ({ - String address, - Amount amount, - String memo, - bool isChange, - })>? - sparkRecipients, + List<({String address, Amount amount, String memo, bool isChange})>? + sparkRecipients, List? sparkMints, List? usedSparkCoins, TransactionV2? tempTx, bool? ignoreCachedBalanceChecks, NameOpState? opNameState, + ({ + String additionalInfo, + String name, + Address sparkAddress, + int validBlocks, + })? + sparkNameInfo, + TxType? type, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -271,17 +304,13 @@ class TxData { frostSigners: frostSigners ?? this.frostSigners, changeAddress: changeAddress ?? this.changeAddress, paynymAccountLite: paynymAccountLite ?? this.paynymAccountLite, + ethEIP1559Fee: ethEIP1559Fee ?? this.ethEIP1559Fee, web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction, nonce: nonce ?? this.nonce, chainId: chainId ?? this.chainId, - feeInWei: feeInWei ?? this.feeInWei, pendingTransaction: pendingTransaction ?? this.pendingTransaction, - jMintValue: jMintValue ?? this.jMintValue, - spendCoinIndexes: spendCoinIndexes ?? this.spendCoinIndexes, - height: height ?? this.height, - txType: txType ?? this.txType, - txSubType: txSubType ?? this.txSubType, - mintsMapLelantus: mintsMapLelantus ?? this.mintsMapLelantus, + pendingSalviumTransaction: + pendingSalviumTransaction ?? this.pendingSalviumTransaction, tezosOperationsList: tezosOperationsList ?? this.tezosOperationsList, sparkRecipients: sparkRecipients ?? this.sparkRecipients, sparkMints: sparkMints ?? this.sparkMints, @@ -290,11 +319,14 @@ class TxData { ignoreCachedBalanceChecks: ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks, opNameState: opNameState ?? this.opNameState, + sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo, + type: type ?? this.type, ); } @override - String toString() => 'TxData{' + String toString() => + 'TxData{' 'feeRateType: $feeRateType, ' 'feeRateAmount: $feeRateAmount, ' 'satsPerVByte: $satsPerVByte, ' @@ -312,17 +344,12 @@ class TxData { 'frostSigners: $frostSigners, ' 'changeAddress: $changeAddress, ' 'paynymAccountLite: $paynymAccountLite, ' + 'ethEIP1559Fee: $ethEIP1559Fee, ' 'web3dartTransaction: $web3dartTransaction, ' 'nonce: $nonce, ' 'chainId: $chainId, ' - 'feeInWei: $feeInWei, ' 'pendingTransaction: $pendingTransaction, ' - 'jMintValue: $jMintValue, ' - 'spendCoinIndexes: $spendCoinIndexes, ' - 'height: $height, ' - 'txType: $txType, ' - 'txSubType: $txSubType, ' - 'mintsMapLelantus: $mintsMapLelantus, ' + 'pendingSalviumTransaction: $pendingSalviumTransaction, ' 'tezosOperationsList: $tezosOperationsList, ' 'sparkRecipients: $sparkRecipients, ' 'sparkMints: $sparkMints, ' @@ -331,5 +358,7 @@ class TxData { 'tempTx: $tempTx, ' 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' 'opNameState: $opNameState, ' + 'sparkNameInfo: $sparkNameInfo, ' + 'type: $type, ' '}'; } diff --git a/lib/wallets/models/tx_recipient.dart b/lib/wallets/models/tx_recipient.dart index 8c5e9a9d4..9173864d3 100644 --- a/lib/wallets/models/tx_recipient.dart +++ b/lib/wallets/models/tx_recipient.dart @@ -1,11 +1,52 @@ +import '../../models/isar/models/blockchain_data/address.dart'; import '../../utilities/amount/amount.dart'; class TxRecipient { final String address; final Amount amount; + final bool isChange; + final AddressType addressType; TxRecipient({ required this.address, required this.amount, + required this.isChange, + required this.addressType, }); + + TxRecipient copyWith({ + String? address, + Amount? amount, + bool? isChange, + AddressType? addressType, + }) { + return TxRecipient( + address: address ?? this.address, + amount: amount ?? this.amount, + isChange: isChange ?? this.isChange, + addressType: addressType ?? this.addressType, + ); + } + + @override + String toString() { + return "TxRecipient{" + "address: $address, " + "amount: $amount, " + "isChange: $isChange, " + "addressType: $addressType" + "}"; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TxRecipient && + address == other.address && + amount == other.amount && + isChange == other.isChange && + addressType == other.addressType; + + @override + int get hashCode => Object.hash(address, amount, isChange, addressType); } diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 3beaa0e8d..95f015c01 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -11,6 +11,7 @@ import 'package:isar/isar.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; @@ -37,12 +38,13 @@ const kFrostSecureStartingIndex = 1; class BitcoinFrostWallet extends Wallet with MultiAddressInterface { BitcoinFrostWallet(CryptoCurrencyNetwork network) - : super(BitcoinFrost(network) as T); + : super(BitcoinFrost(network) as T); - FrostWalletInfo get frostInfo => mainDB.isar.frostWalletInfo - .where() - .walletIdEqualTo(walletId) - .findFirstSync()!; + FrostWalletInfo get frostInfo => + mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .findFirstSync()!; late ElectrumXClient electrumXClient; late CachedElectrumXClient electrumXCachedClient; @@ -59,11 +61,7 @@ class BitcoinFrostWallet extends Wallet Logging.instance.i("Generating new FROST wallet."); try { - final salt = frost - .multisigSalt( - multisigConfig: multisigConfig, - ) - .toHex; + final salt = frost.multisigSalt(multisigConfig: multisigConfig).toHex; final FrostWalletInfo frostWalletInfo = FrostWalletInfo( walletId: info.walletId, @@ -127,22 +125,24 @@ class BitcoinFrostWallet extends Wallet .map((e) => e.amount) .reduce((value, e) => value += e); - final utxos = await mainDB - .getUTXOs(walletId) - .filter() - .isBlockedEqualTo(false) - .findAll(); + 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, - cryptoCurrency.minCoinbaseConfirms, - ), + (e) => + !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), ); if (utxos.isEmpty) { throw Exception("No confirmed UTXOs found"); @@ -174,32 +174,32 @@ class BitcoinFrostWallet extends Wallet } } - final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main - ? Network.Mainnet - : Network.Testnet; + final int network = + cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet; final List< - ({ - UTXO utxo, - Uint8List scriptPubKey, - ({int account, int index, bool change}) addressDerivationData - })> inputs = []; + ({ + UTXO utxo, + Uint8List scriptPubKey, + ({int account, int index, bool change, bool secure}) + addressDerivationData, + }) + > + inputs = []; for (final utxo in utxosToUse) { - final dData = await getDerivationData( - utxo.address, - ); + final dData = await getDerivationData(utxo.address); final publicKey = cryptoCurrency.addressToPubkey( address: utxo.address!, ); - inputs.add( - ( - utxo: utxo, - scriptPubKey: publicKey, - addressDerivationData: dData, - ), - ); + inputs.add(( + utxo: utxo, + scriptPubKey: publicKey, + addressDerivationData: dData, + )); } await checkChangeAddressForTransactions(); final changeAddress = await getCurrentChangeAddress(); @@ -221,34 +221,32 @@ class BitcoinFrostWallet extends Wallet utxosRemaining.isNotEmpty) { // add extra utxo final utxo = utxosRemaining.take(1).first; - final dData = await getDerivationData( - utxo.address, - ); + final dData = await getDerivationData(utxo.address); final publicKey = cryptoCurrency.addressToPubkey( address: utxo.address!, ); - inputs.add( - ( - utxo: utxo, - scriptPubKey: publicKey, - addressDerivationData: dData, - ), - ); + inputs.add(( + utxo: utxo, + scriptPubKey: publicKey, + addressDerivationData: dData, + )); } else { rethrow; } } } - return txData.copyWith(frostMSConfig: config, utxos: utxosToUse); + return txData.copyWith( + frostMSConfig: config, + utxos: utxosToUse.map((e) => StandardInput(e)).toSet(), + ); } catch (_) { rethrow; } } - Future<({int account, int index, bool change})> getDerivationData( - String? address, - ) async { + Future<({int account, int index, bool change, bool secure})> + getDerivationData(String? address) async { if (address == null) { throw Exception("Missing address required for FROST signing"); } @@ -269,15 +267,14 @@ class BitcoinFrostWallet extends Wallet ); } if (components[1] != 0 && components[1] != 1) { - throw Exception( - "${components[1]} must be 1 or 0 for change", - ); + throw Exception("${components[1]} must be 1 or 0 for change"); } return ( account: components[0], change: components[1] == 1, index: components[2], + secure: addr.zSafeFrost ?? false, ); } catch (_) { rethrow; @@ -285,15 +282,13 @@ class BitcoinFrostWallet extends Wallet } Future< - ({ - Pointer machinePtr, - String preprocess, - })> frostAttemptSignConfig({ - required String config, - }) async { - final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main - ? Network.Mainnet - : Network.Testnet; + ({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( @@ -312,17 +307,13 @@ class BitcoinFrostWallet extends Wallet await _saveMultisigConfig(multisigConfig); await _updateThreshold( - frost.getThresholdFromKeys( - serializedKeys: serializedKeys, - ), + frost.getThresholdFromKeys(serializedKeys: serializedKeys), ); final myNameIndex = frost.getParticipantIndexFromKeys( serializedKeys: serializedKeys, ); - final participants = Frost.getParticipants( - multisigConfig: multisigConfig, - ); + final participants = Frost.getParticipants(multisigConfig: multisigConfig); final myName = participants[myNameIndex]; await _updateParticipants(participants); @@ -337,7 +328,7 @@ class BitcoinFrostWallet extends Wallet } } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(BigInt feeRate) async { int available = 0; int inputCount = 0; final height = await chainHeight; @@ -367,11 +358,15 @@ class BitcoinFrostWallet extends Wallet // return vSize * (feeRatePerKB / 1000).ceil(); // } - Amount _roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount _roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -386,48 +381,26 @@ class BitcoinFrostWallet extends Wallet int get isarTransactionVersion => 2; @override - FilterOperation? get changeAddressFilterOperation => FilterGroup.and( - [ - FilterCondition.equalTo( - property: r"type", - value: info.mainAddressType, - ), - const FilterCondition.equalTo( - property: r"subType", - value: AddressSubType.change, - ), - const FilterCondition.equalTo( - property: r"zSafeFrost", - value: true, - ), - const FilterCondition.greaterThan( - property: r"derivationIndex", - value: 0, - ), - ], - ); + FilterOperation? get changeAddressFilterOperation => FilterGroup.and([ + FilterCondition.equalTo(property: r"type", value: info.mainAddressType), + const FilterCondition.equalTo( + property: r"subType", + value: AddressSubType.change, + ), + const FilterCondition.equalTo(property: r"zSafeFrost", value: true), + const FilterCondition.greaterThan(property: r"derivationIndex", value: 0), + ]); @override - FilterOperation? get receivingAddressFilterOperation => FilterGroup.and( - [ - FilterCondition.equalTo( - property: r"type", - value: info.mainAddressType, - ), - const FilterCondition.equalTo( - property: r"subType", - value: AddressSubType.receiving, - ), - const FilterCondition.equalTo( - property: r"zSafeFrost", - value: true, - ), - const FilterCondition.greaterThan( - property: r"derivationIndex", - value: 0, - ), - ], - ); + FilterOperation? get receivingAddressFilterOperation => FilterGroup.and([ + FilterCondition.equalTo(property: r"type", value: info.mainAddressType), + const FilterCondition.equalTo( + property: r"subType", + value: AddressSubType.receiving, + ), + const FilterCondition.equalTo(property: r"zSafeFrost", value: true), + const FilterCondition.greaterThan(property: r"derivationIndex", value: 0), + ]); @override Future updateTransactions() async { @@ -436,14 +409,16 @@ class BitcoinFrostWallet extends Wallet await _fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -451,18 +426,20 @@ class BitcoinFrostWallet extends Wallet final currentHeight = await chainHeight; // Fetch history from ElectrumX. - final List> allTxHashes = - await _fetchHistory(allAddressesSet); + final List> allTxHashes = await _fetchHistory( + allAddressesSet, + ); final List> allTransactions = []; for (final txHash in allTxHashes) { - final storedTx = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(txHash["tx_hash"] as String) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(txHash["tx_hash"] as String) + .findFirst(); if (storedTx == null || !storedTx.isConfirmed( @@ -590,8 +567,9 @@ class BitcoinFrostWallet extends Wallet TransactionSubType subType = TransactionSubType.none; if (outputs.length > 1 && inputs.isNotEmpty) { for (int i = 0; i < outputs.length; i++) { - final List? scriptChunks = - outputs[i].scriptPubKeyAsm?.split(" "); + final List? scriptChunks = outputs[i].scriptPubKeyAsm?.split( + " ", + ); if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { final blindedPaymentCode = scriptChunks![1]; final bytes = blindedPaymentCode.toUint8ListFromHex; @@ -635,7 +613,8 @@ class BitcoinFrostWallet extends Wallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -698,16 +677,16 @@ class BitcoinFrostWallet extends Wallet final hex = txData.raw!; final txHash = await electrumXClient.broadcastTransaction(rawTx: hex); - Logging.instance.d( - "Sent txHash: $txHash", - ); + Logging.instance.d("Sent txHash: $txHash"); // mark utxos as used - final usedUTXOs = txData.utxos!.map((e) => e.copyWith(used: true)); + final usedUTXOs = txData.utxos!.whereType().map( + (e) => e.utxo.copyWith(used: true), + ); await mainDB.putUTXOs(usedUTXOs.toList()); txData = txData.copyWith( - utxos: usedUTXOs.toSet(), + utxos: usedUTXOs.map((e) => StandardInput(e)).toSet(), txHash: txHash, txid: txHash, ); @@ -724,7 +703,7 @@ class BitcoinFrostWallet extends Wallet } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { final available = info.cachedBalance.spendable; if (available == amount) { @@ -787,18 +766,21 @@ class BitcoinFrostWallet extends Wallet 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(), + fast: + Amount.fromDecimal( + fast, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw, + medium: + Amount.fromDecimal( + medium, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw, + slow: + Amount.fromDecimal( + slow, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw, ); Logging.instance.i("fetched fees: $feeObject"); @@ -839,21 +821,14 @@ class BitcoinFrostWallet extends Wallet final coin = info.coin; GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), + WalletSyncStatusChangedEvent(WalletSyncStatus.syncing, walletId, coin), ); try { await refreshMutex.protect(() async { if (!isRescan) { - final salt = frost - .multisigSalt( - multisigConfig: multisigConfig!, - ) - .toHex; + final salt = + frost.multisigSalt(multisigConfig: multisigConfig!).toHex; final knownSalts = _getKnownSalts(); if (knownSalts.contains(salt)) { throw Exception("Known frost multisig salt found!"); @@ -875,20 +850,12 @@ class BitcoinFrostWallet extends Wallet const receiveChain = 0; const changeChain = 1; final List addresses})>> - receiveFutures = [ - _checkGapsLinearly( - serializedKeys, - receiveChain, - secure: true, - ), + receiveFutures = [ + _checkGapsLinearly(serializedKeys, receiveChain, secure: true), ]; final List addresses})>> - changeFutures = [ - _checkGapsLinearly( - serializedKeys, - changeChain, - secure: true, - ), + changeFutures = [ + _checkGapsLinearly(serializedKeys, changeChain, secure: true), ]; // io limitations may require running these linearly instead @@ -949,17 +916,16 @@ class BitcoinFrostWallet extends Wallet }); GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), + WalletSyncStatusChangedEvent(WalletSyncStatus.synced, walletId, coin), ); unawaited(refresh()); } catch (e, s) { - Logging.instance - .f("recoverFromSerializedKeys failed: ", error: e, stackTrace: s); + Logging.instance.f( + "recoverFromSerializedKeys failed: ", + error: e, + stackTrace: s, + ); GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.unableToSync, @@ -978,21 +944,11 @@ class BitcoinFrostWallet extends Wallet const changeChain = 1; final List addresses})>> receiveFutures = - [ - _checkGapsLinearly( - serializedKeys, - receiveChain, - secure: false, - ), - ]; + [_checkGapsLinearly(serializedKeys, receiveChain, secure: false)]; final List addresses})>> changeFutures = [ // for legacy support secure is set to false to see // funds received on insecure addresses - _checkGapsLinearly( - serializedKeys, - changeChain, - secure: false, - ), + _checkGapsLinearly(serializedKeys, changeChain, secure: false), ]; // io limitations may require running these linearly instead @@ -1111,10 +1067,7 @@ class BitcoinFrostWallet extends Wallet rethrow; } - await info.updateCachedChainHeight( - newHeight: height, - isar: mainDB.isar, - ); + await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } @override @@ -1153,9 +1106,7 @@ class BitcoinFrostWallet extends Wallet for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { - final utxo = await _parseUTXO( - jsonUTXO: fetchedUtxoList[i][j], - ); + final utxo = await _parseUTXO(jsonUTXO: fetchedUtxoList[i][j]); outputArray.add(utxo); } @@ -1163,8 +1114,11 @@ class BitcoinFrostWallet extends Wallet return await mainDB.updateUTXOs(walletId, outputArray); } catch (e, s) { - Logging.instance - .e("Output fetch unsuccessful: ", error: e, stackTrace: s); + Logging.instance.e( + "Output fetch unsuccessful: ", + error: e, + stackTrace: s, + ); return false; } } @@ -1172,13 +1126,9 @@ class BitcoinFrostWallet extends Wallet // =================== Secure storage ======================================== Future getSerializedKeys() async => - await secureStorageInterface.read( - key: "{$walletId}_serializedFROSTKeys", - ); + await secureStorageInterface.read(key: "{$walletId}_serializedFROSTKeys"); - Future _saveSerializedKeys( - String keys, - ) async { + Future _saveSerializedKeys(String keys) async { final current = await getSerializedKeys(); if (current == null) { @@ -1205,18 +1155,14 @@ class BitcoinFrostWallet extends Wallet ); Future getMultisigConfig() async => - await secureStorageInterface.read( - key: "{$walletId}_multisigConfig", - ); + await secureStorageInterface.read(key: "{$walletId}_multisigConfig"); Future getMultisigConfigPrevGen() async => await secureStorageInterface.read( key: "{$walletId}_multisigConfigPrevGen", ); - Future _saveMultisigConfig( - String multisigConfig, - ) async { + Future _saveMultisigConfig(String multisigConfig) async { final current = await getMultisigConfig(); if (current == null) { @@ -1248,21 +1194,16 @@ class BitcoinFrostWallet extends Wallet } } - Future _saveMultisigId( - Uint8List id, - ) async => + Future _saveMultisigId(Uint8List id) async => await secureStorageInterface.write( key: "{$walletId}_multisigIdFROST", value: id.toHex, ); - Future _recoveryString() async => await secureStorageInterface.read( - key: "{$walletId}_recoveryStringFROST", - ); + 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, @@ -1270,11 +1211,12 @@ class BitcoinFrostWallet extends Wallet // =================== DB ==================================================== - List _getKnownSalts() => mainDB.isar.frostWalletInfo - .where() - .walletIdEqualTo(walletId) - .knownSaltsProperty() - .findFirstSync()!; + List _getKnownSalts() => + mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .knownSaltsProperty() + .findFirstSync()!; Future _updateKnownSalts(List knownSalts) async { final info = frostInfo; @@ -1287,11 +1229,12 @@ class BitcoinFrostWallet extends Wallet }); } - List _getParticipants() => mainDB.isar.frostWalletInfo - .where() - .walletIdEqualTo(walletId) - .participantsProperty() - .findFirstSync()!; + List _getParticipants() => + mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .participantsProperty() + .findFirstSync()!; Future _updateParticipants(List participants) async { final info = frostInfo; @@ -1304,11 +1247,12 @@ class BitcoinFrostWallet extends Wallet }); } - int _getThreshold() => mainDB.isar.frostWalletInfo - .where() - .walletIdEqualTo(walletId) - .thresholdProperty() - .findFirstSync()!; + int _getThreshold() => + mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .thresholdProperty() + .findFirstSync()!; Future _updateThreshold(int threshold) async { final info = frostInfo; @@ -1321,20 +1265,19 @@ class BitcoinFrostWallet extends Wallet }); } - String _getMyName() => mainDB.isar.frostWalletInfo - .where() - .walletIdEqualTo(walletId) - .myNameProperty() - .findFirstSync()!; + String _getMyName() => + mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .myNameProperty() + .findFirstSync()!; Future _updateMyName(String myName) async { final info = frostInfo; await mainDB.isar.writeTxn(() async { await mainDB.isar.frostWalletInfo.delete(info.id); - await mainDB.isar.frostWalletInfo.put( - info.copyWith(myName: myName), - ); + await mainDB.isar.frostWalletInfo.put(info.copyWith(myName: myName)); }); } @@ -1356,20 +1299,21 @@ class BitcoinFrostWallet extends Wallet // TODO [prio=low]: Use ElectrumXInterface method. Future _updateElectrumX() async { - final failovers = nodeService - .failoverNodesFor(currency: cryptoCurrency) - .map( - (e) => ElectrumXNode( - address: e.host, - port: e.port, - name: e.name, - id: e.id, - useSSL: e.useSSL, - torEnabled: e.torEnabled, - clearnetEnabled: e.clearnetEnabled, - ), - ) - .toList(); + final failovers = + nodeService + .failoverNodesFor(currency: cryptoCurrency) + .map( + (e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + torEnabled: e.torEnabled, + clearnetEnabled: e.clearnetEnabled, + ), + ) + .toList(); final newNode = await _getCurrentElectrumXNode(); try { @@ -1409,9 +1353,7 @@ class BitcoinFrostWallet extends Wallet return false; } - Future _parseUTXO({ - required Map jsonUTXO, - }) async { + Future _parseUTXO({required Map jsonUTXO}) async { final txn = await electrumXCachedClient.getTransaction( txHash: jsonUTXO["tx_hash"] as String, verbose: true, @@ -1430,7 +1372,7 @@ class BitcoinFrostWallet extends Wallet // scriptPubKey = output["scriptPubKey"]?["hex"] as String?; utxoOwnerAddress = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; + output["scriptPubKey"]?["address"] as String?; } } @@ -1476,9 +1418,10 @@ class BitcoinFrostWallet extends Wallet await checkChangeAddressForTransactions(); } } catch (e, s) { - Logging.instance - .i("Exception rethrown from _checkChangeAddressForTransactions" - "($cryptoCurrency): $e\n$s"); + Logging.instance.i( + "Exception rethrown from _checkChangeAddressForTransactions" + "($cryptoCurrency): $e\n$s", + ); rethrow; } } @@ -1534,9 +1477,10 @@ class BitcoinFrostWallet extends Wallet @override Future generateNewChangeAddress() async { final current = await getCurrentChangeAddress(); - int index = current == null - ? kFrostSecureStartingIndex - : current.derivationIndex + 1; + int index = + current == null + ? kFrostSecureStartingIndex + : current.derivationIndex + 1; const chain = 1; // change address final serializedKeys = (await getSerializedKeys())!; @@ -1567,9 +1511,10 @@ class BitcoinFrostWallet extends Wallet @override Future generateNewReceivingAddress() async { final current = await getCurrentReceivingAddress(); - int index = current == null - ? kFrostSecureStartingIndex - : current.derivationIndex + 1; + int index = + current == null + ? kFrostSecureStartingIndex + : current.derivationIndex + 1; const chain = 0; // receiving address final serializedKeys = (await getSerializedKeys())!; @@ -1649,8 +1594,9 @@ class BitcoinFrostWallet extends Wallet } } - nextReceivingAddresses - .removeWhere((e) => e.derivationIndex > activeReceiveIndex); + nextReceivingAddresses.removeWhere( + (e) => e.derivationIndex > activeReceiveIndex, + ); if (nextReceivingAddresses.isNotEmpty) { await mainDB.updateOrPutAddresses(nextReceivingAddresses); await info.updateReceivingAddress( @@ -1658,8 +1604,9 @@ class BitcoinFrostWallet extends Wallet isar: mainDB.isar, ); } - nextChangeAddresses - .removeWhere((e) => e.derivationIndex > activeChangeIndex); + nextChangeAddresses.removeWhere( + (e) => e.derivationIndex > activeChangeIndex, + ); if (nextChangeAddresses.isNotEmpty) { await mainDB.updateOrPutAddresses(nextChangeAddresses); } @@ -1707,14 +1654,16 @@ class BitcoinFrostWallet extends Wallet account: account, change: change == 1, index: index, + secure: secure, ); final keys = frost.deserializeKeys(keys: serializedKeys); final addressString = frost.addressForKeys( - network: cryptoCurrency.network == CryptoCurrencyNetwork.main - ? Network.Mainnet - : Network.Testnet, + network: + cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet, keys: keys, addressDerivationData: addressDerivationData, secure: secure, @@ -1726,9 +1675,10 @@ class BitcoinFrostWallet extends Wallet publicKey: cryptoCurrency.addressToPubkey(address: addressString), derivationIndex: index, derivationPath: DerivationPath()..value = "$account/$change/$index", - subType: change == 0 - ? AddressSubType.receiving - : change == 1 + subType: + change == 0 + ? AddressSubType.receiving + : change == 1 ? AddressSubType.change : AddressSubType.unknown, type: AddressType.frostMS, @@ -1770,9 +1720,7 @@ class BitcoinFrostWallet extends Wallet } // get address tx count - final count = await _fetchTxCount( - address: address, - ); + final count = await _fetchTxCount(address: address); // check and add appropriate addresses if (count > 0) { @@ -1792,25 +1740,24 @@ class BitcoinFrostWallet extends Wallet Future _fetchTxCount({required Address address}) async { final transactions = await electrumXClient.getHistory( - scripthash: cryptoCurrency.addressToScriptHash( - address: address.value, - ), + scripthash: cryptoCurrency.addressToScriptHash(address: address.value), ); return transactions.length; } Future> _fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -1839,8 +1786,11 @@ class BitcoinFrostWallet extends Wallet return allTxHashes; } catch (e, s) { - Logging.instance - .e("$runtimeType._fetchHistory: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType._fetchHistory: ", + error: e, + stackTrace: s, + ); rethrow; } } diff --git a/lib/wallets/wallet/impl/bitcoin_wallet.dart b/lib/wallets/wallet/impl/bitcoin_wallet.dart index 5c7f3ed75..2ce942673 100644 --- a/lib/wallets/wallet/impl/bitcoin_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_wallet.dart @@ -54,19 +54,19 @@ class BitcoinWallet extends Bip39HDWallet // =========================================================================== @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate(int inputCount, int outputCount, BigInt feeRatePerKB) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } // // @override diff --git a/lib/wallets/wallet/impl/bitcoincash_wallet.dart b/lib/wallets/wallet/impl/bitcoincash_wallet.dart index 666d5fd6b..cee690219 100644 --- a/lib/wallets/wallet/impl/bitcoincash_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoincash_wallet.dart @@ -33,57 +33,54 @@ class BitcoincashWallet int get isarTransactionVersion => 2; BitcoincashWallet(CryptoCurrencyNetwork network) - : super(Bitcoincash(network) as T); + : super(Bitcoincash(network) as T); @override - FilterOperation? get changeAddressFilterOperation => FilterGroup.and( - [ - ...standardChangeAddressFilters, - FilterGroup.not( - const ObjectFilter( - property: "derivationPath", - filter: FilterCondition.startsWith( - property: "value", - value: "m/44'/0'", - ), - ), - ), - ], - ); + FilterOperation? get changeAddressFilterOperation => FilterGroup.and([ + ...standardChangeAddressFilters, + FilterGroup.not( + const ObjectFilter( + property: "derivationPath", + filter: FilterCondition.startsWith( + property: "value", + value: "m/44'/0'", + ), + ), + ), + ]); @override - FilterOperation? get receivingAddressFilterOperation => FilterGroup.and( - [ - ...standardReceivingAddressFilters, - FilterGroup.not( - const ObjectFilter( - property: "derivationPath", - filter: FilterCondition.startsWith( - property: "value", - value: "m/44'/0'", - ), - ), - ), - ], - ); + FilterOperation? get receivingAddressFilterOperation => FilterGroup.and([ + ...standardReceivingAddressFilters, + FilterGroup.not( + const ObjectFilter( + property: "derivationPath", + filter: FilterCondition.startsWith( + property: "value", + value: "m/44'/0'", + ), + ), + ), + ]); // =========================================================================== @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .typeEqualTo(AddressType.nonWallet) - .and() - .group( - (q) => q - .subTypeEqualTo(AddressSubType.receiving) - .or() - .subTypeEqualTo(AddressSubType.change), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .group( + (q) => q + .subTypeEqualTo(AddressSubType.receiving) + .or() + .subTypeEqualTo(AddressSubType.change), + ) + .findAll(); return allAddresses; } @@ -106,20 +103,23 @@ class BitcoincashWallet final List

allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); final List> allTransactions = []; @@ -139,8 +139,9 @@ class BitcoincashWallet ); // check for duplicates before adding to list - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -294,12 +295,8 @@ class BitcoincashWallet // only found outputs owned by this wallet type = TransactionType.incoming; } else { - Logging.instance.e( - "Unexpected tx found (ignoring it)", - ); - Logging.instance.d( - "Unexpected tx found (ignoring it): $txData", - ); + Logging.instance.e("Unexpected tx found (ignoring it)"); + Logging.instance.d("Unexpected tx found (ignoring it): $txData"); continue; } @@ -310,7 +307,8 @@ class BitcoincashWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -327,7 +325,7 @@ class BitcoincashWallet @override Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -339,8 +337,9 @@ class BitcoincashWallet if (scriptPubKeyHex != null) { // check for cash tokens try { - final ctOutput = - cash_tokens.unwrap_spk(scriptPubKeyHex.toUint8ListFromHex); + final ctOutput = cash_tokens.unwrap_spk( + scriptPubKeyHex.toUint8ListFromHex, + ); if (ctOutput.token_data != null) { // found a token! blocked = true; @@ -374,19 +373,23 @@ class BitcoincashWallet // TODO: correct formula for bch? @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: info.coin.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } @override diff --git a/lib/wallets/wallet/impl/cardano_wallet.dart b/lib/wallets/wallet/impl/cardano_wallet.dart index 3beafe5f5..5ae2d14a6 100644 --- a/lib/wallets/wallet/impl/cardano_wallet.dart +++ b/lib/wallets/wallet/impl/cardano_wallet.dart @@ -45,15 +45,16 @@ class CardanoWallet extends Bip39Wallet { final seed = CardanoIcarusSeedGenerator(mnemonic).generate(); final cip1852 = Cip1852.fromSeed(seed, Cip1852Coins.cardanoIcarus); final derivationAccount = cip1852.purpose.coin.account(0); - final shelley = CardanoShelley.fromCip1852Object(derivationAccount) - .change(Bip44Changes.chainExt) - .addressIndex(0); + final shelley = CardanoShelley.fromCip1852Object( + derivationAccount, + ).change(Bip44Changes.chainExt).addressIndex(0); final paymentPublicKey = shelley.bip44.publicKey.compressed; final stakePublicKey = shelley.bip44Sk.publicKey.compressed; - final addressStr = ADABaseAddress.fromPublicKey( - basePubkeyBytes: paymentPublicKey, - stakePubkeyBytes: stakePublicKey, - ).address; + final addressStr = + ADABaseAddress.fromPublicKey( + basePubkeyBytes: paymentPublicKey, + stakePubkeyBytes: stakePublicKey, + ).address; return Address( walletId: walletId, value: addressStr, @@ -76,7 +77,11 @@ class CardanoWallet extends Bip39Wallet { await mainDB.updateOrPutAddresses([address]); } } catch (e, s) { - Logging.instance.e("$runtimeType checkSaveInitialReceivingAddress() failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType checkSaveInitialReceivingAddress() failed: ", + error: e, + stackTrace: s, + ); } } @@ -91,13 +96,17 @@ class CardanoWallet extends Bip39Wallet { return Future.value(health); } catch (e, s) { - Logging.instance.e("Error ping checking in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error ping checking in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); return Future.value(false); } } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { await updateProvider(); if (info.cachedBalance.spendable.raw == BigInt.zero) { @@ -113,10 +122,7 @@ class CardanoWallet extends Bip39Wallet { final fee = params.calculateFee(284); - return Amount( - rawValue: fee, - fractionDigits: cryptoCurrency.fractionDigits, - ); + return Amount(rawValue: fee, fractionDigits: cryptoCurrency.fractionDigits); } @override @@ -129,7 +135,7 @@ class CardanoWallet extends Bip39Wallet { ); // 284 is the size of a basic transaction with one input and two outputs (change and recipient) - final fee = params.calculateFee(284).toInt(); + final fee = params.calculateFee(284); return FeeObject( numberOfBlocksFast: 2, @@ -140,7 +146,11 @@ class CardanoWallet extends Bip39Wallet { slow: fee, ); } catch (e, s) { - Logging.instance.e("Error getting fees in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error getting fees in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -181,51 +191,66 @@ class CardanoWallet extends Bip39Wallet { } final bip32 = CardanoIcarusBip32.fromSeed( - CardanoIcarusSeedGenerator(await getMnemonic()).generate()); + CardanoIcarusSeedGenerator(await getMnemonic()).generate(), + ); final spend = bip32.derivePath("1852'/1815'/0'/0/0"); final privateKey = AdaPrivateKey.fromBytes(spend.privateKey.raw); // Calculate fees with example tx final exampleFee = ADAHelper.toLovelaces("0.10"); final change = TransactionOutput( - address: ADABaseAddress((await getCurrentReceivingAddress())!.value), - amount: Value(coin: totalBalance - (txData.amount!.raw))); + address: ADABaseAddress((await getCurrentReceivingAddress())!.value), + amount: Value(coin: totalBalance - (txData.amount!.raw)), + ); + + final outputAddress = ADAAddress.fromAddress( + txData.recipients!.first.address, + ); + if (!(outputAddress is ADABaseAddress || + outputAddress is ADAEnterpriseAddress)) { + throw Exception( + "Address of type ${outputAddress.runtimeType} currently not supported.", + ); + } + final body = TransactionBody( - inputs: listOfUtxosToBeUsed - .map((e) => TransactionInput( - transactionId: TransactionHash.fromHex(e.txHash), - index: e.outputIndex)) - .toList(), + inputs: + listOfUtxosToBeUsed + .map( + (e) => TransactionInput( + transactionId: TransactionHash.fromHex(e.txHash), + index: e.outputIndex, + ), + ) + .toList(), outputs: [ change, TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), - amount: Value(coin: txData.amount!.raw - exampleFee)) + address: outputAddress, + amount: Value(coin: txData.amount!.raw - exampleFee), + ), ], fee: exampleFee, ); final exampleTx = ADATransaction( body: body, witnessSet: TransactionWitnessSet( - vKeys: [ - privateKey.createSignatureWitness(body.toHash().data), - ], + vKeys: [privateKey.createSignatureWitness(body.toHash().data)], ), ); - final params = await blockfrostProvider! - .request(BlockfrostRequestLatestEpochProtocolParameters()); + final params = await blockfrostProvider!.request( + BlockfrostRequestLatestEpochProtocolParameters(), + ); final fee = params.calculateFee(exampleTx.size); // Check if we are sending all balance, which means no change and only one output for recipient. if (totalBalance == txData.amount!.raw) { final List newRecipients = [ - ( - address: txData.recipients!.first.address, + txData.recipients!.first.copyWith( amount: Amount( rawValue: txData.amount!.raw - fee, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: txData.recipients!.first.isChange, ), ]; return txData.copyWith( @@ -244,7 +269,8 @@ class CardanoWallet extends Bip39Wallet { if (totalBalance - (txData.amount!.raw + fee) < ADAHelper.toLovelaces("1")) { throw Exception( - "Not enough balance for change. By network rules, please either send all balance or leave at least 1 ADA change."); + "Not enough balance for change. By network rules, please either send all balance or leave at least 1 ADA change.", + ); } return txData.copyWith( @@ -255,7 +281,11 @@ class CardanoWallet extends Bip39Wallet { ); } } catch (e, s) { - Logging.instance.e("$runtimeType Cardano prepareSend failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType Cardano prepareSend failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -293,44 +323,62 @@ class CardanoWallet extends Bip39Wallet { } final bip32 = CardanoIcarusBip32.fromSeed( - CardanoIcarusSeedGenerator(await getMnemonic()).generate()); + CardanoIcarusSeedGenerator(await getMnemonic()).generate(), + ); final spend = bip32.derivePath("1852'/1815'/0'/0/0"); final privateKey = AdaPrivateKey.fromBytes(spend.privateKey.raw); final change = TransactionOutput( - address: ADABaseAddress((await getCurrentReceivingAddress())!.value), - amount: Value( - coin: totalUtxoAmount - (txData.amount!.raw + txData.fee!.raw))); + address: ADABaseAddress((await getCurrentReceivingAddress())!.value), + amount: Value( + coin: totalUtxoAmount - (txData.amount!.raw + txData.fee!.raw), + ), + ); + + final outputAddress = ADAAddress.fromAddress( + txData.recipients!.first.address, + ); + if (!(outputAddress is ADABaseAddress || + outputAddress is ADAEnterpriseAddress)) { + throw Exception( + "Address of type ${outputAddress.runtimeType} currently not supported.", + ); + } + List outputs = []; if (totalBalance == (txData.amount!.raw + txData.fee!.raw)) { outputs = [ TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), - amount: Value(coin: txData.amount!.raw)) + address: outputAddress, + amount: Value(coin: txData.amount!.raw), + ), ]; } else { outputs = [ change, TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), - amount: Value(coin: txData.amount!.raw)) + address: outputAddress, + amount: Value(coin: txData.amount!.raw), + ), ]; } final body = TransactionBody( - inputs: listOfUtxosToBeUsed - .map((e) => TransactionInput( - transactionId: TransactionHash.fromHex(e.txHash), - index: e.outputIndex)) - .toList(), + inputs: + listOfUtxosToBeUsed + .map( + (e) => TransactionInput( + transactionId: TransactionHash.fromHex(e.txHash), + index: e.outputIndex, + ), + ) + .toList(), outputs: outputs, fee: txData.fee!.raw, ); final tx = ADATransaction( body: body, witnessSet: TransactionWitnessSet( - vKeys: [ - privateKey.createSignatureWitness(body.toHash().data), - ], + vKeys: [privateKey.createSignatureWitness(body.toHash().data)], ), ); @@ -339,11 +387,13 @@ class CardanoWallet extends Bip39Wallet { transactionCborBytes: tx.serialize(), ), ); - return txData.copyWith( - txid: sentTx, - ); + return txData.copyWith(txid: sentTx); } catch (e, s) { - Logging.instance.e("$runtimeType Cardano confirmSend failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType Cardano confirmSend failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -410,7 +460,11 @@ class CardanoWallet extends Bip39Wallet { await info.updateBalance(newBalance: balance, isar: mainDB.isar); } catch (e, s) { - Logging.instance.e("Error getting balance in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error getting balance in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); } } @@ -428,7 +482,11 @@ class CardanoWallet extends Bip39Wallet { isar: mainDB.isar, ); } catch (e, s) { - Logging.instance.e("Error updating transactions in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error updating transactions in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); } } @@ -446,14 +504,13 @@ class CardanoWallet extends Bip39Wallet { final txsList = await blockfrostProvider!.request( BlockfrostRequestAddressTransactions( - ADAAddress.fromAddress( - currentAddr, - ), + ADAAddress.fromAddress(currentAddr), ), ); - final parsedTxsList = - List>.empty(growable: true); + final parsedTxsList = List>.empty( + growable: true, + ); for (final tx in txsList) { final txInfo = await blockfrostProvider!.request( @@ -525,10 +582,11 @@ class CardanoWallet extends Bip39Wallet { type: txType, subType: isar.TransactionSubType.none, amount: amount, - amountString: Amount( - rawValue: BigInt.from(amount), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + amountString: + Amount( + rawValue: BigInt.from(amount), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), fee: int.parse(txInfo.fees), height: txInfo.blockHeight, isCancelled: false, @@ -548,9 +606,10 @@ class CardanoWallet extends Bip39Wallet { derivationIndex: 0, derivationPath: DerivationPath()..value = _addressDerivationPath, type: AddressType.cardanoShelley, - subType: txType == isar.TransactionType.outgoing - ? AddressSubType.unknown - : AddressSubType.receiving, + subType: + txType == isar.TransactionType.outgoing + ? AddressSubType.unknown + : AddressSubType.receiving, ); parsedTxsList.add(Tuple2(transaction, txAddress)); @@ -560,7 +619,11 @@ class CardanoWallet extends Bip39Wallet { } on NodeTorMismatchConfigException { rethrow; } catch (e, s) { - Logging.instance.e("Error updating transactions in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error updating transactions in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); } } @@ -576,10 +639,7 @@ class CardanoWallet extends Bip39Wallet { final client = HttpClient(); if (prefs.useTor) { final proxyInfo = TorService.sharedInstance.getProxyInfo(); - final proxySettings = ProxySettings( - proxyInfo.host, - proxyInfo.port, - ); + final proxySettings = ProxySettings(proxyInfo.host, proxyInfo.port); SocksTCPClient.assignToHttpClient(client, [proxySettings]); } blockfrostProvider = CustomBlockForestProvider( diff --git a/lib/wallets/wallet/impl/dash_wallet.dart b/lib/wallets/wallet/impl/dash_wallet.dart index 59fdaa037..f9a90d5b8 100644 --- a/lib/wallets/wallet/impl/dash_wallet.dart +++ b/lib/wallets/wallet/impl/dash_wallet.dart @@ -36,17 +36,18 @@ class DashWallet extends Bip39HDWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -59,30 +60,34 @@ class DashWallet extends Bip39HDWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; // Fetch history from ElectrumX. - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -95,8 +100,9 @@ class DashWallet extends Bip39HDWallet ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -246,7 +252,8 @@ class DashWallet extends Bip39HDWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -263,7 +270,7 @@ class DashWallet extends Bip39HDWallet @override Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -296,18 +303,22 @@ class DashWallet extends Bip39HDWallet } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } } diff --git a/lib/wallets/wallet/impl/dogecoin_wallet.dart b/lib/wallets/wallet/impl/dogecoin_wallet.dart index e9046f959..da24c5a9d 100644 --- a/lib/wallets/wallet/impl/dogecoin_wallet.dart +++ b/lib/wallets/wallet/impl/dogecoin_wallet.dart @@ -38,17 +38,18 @@ class DogecoinWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -61,30 +62,34 @@ class DogecoinWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; // Fetch history from ElectrumX. - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -97,8 +102,9 @@ class DogecoinWallet ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -249,7 +255,8 @@ class DogecoinWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -266,7 +273,7 @@ class DogecoinWallet @override Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -287,7 +294,8 @@ class DogecoinWallet // https://en.bitcoin.it/wiki/BIP_0047#Sending if (bytes.length == 80 && bytes.first == 1) { blocked = true; - blockedReason = "Paynym notification output. Incautious " + blockedReason = + "Paynym notification output. Incautious " "handling of outputs from notification transactions " "may cause unintended loss of privacy."; break; @@ -299,18 +307,22 @@ class DogecoinWallet } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } } diff --git a/lib/wallets/wallet/impl/ecash_wallet.dart b/lib/wallets/wallet/impl/ecash_wallet.dart index a8741ba21..aacdc4f31 100644 --- a/lib/wallets/wallet/impl/ecash_wallet.dart +++ b/lib/wallets/wallet/impl/ecash_wallet.dart @@ -34,46 +34,37 @@ class EcashWallet extends Bip39HDWallet EcashWallet(CryptoCurrencyNetwork network) : super(Ecash(network) as T); @override - FilterOperation? get changeAddressFilterOperation => FilterGroup.and( - [ - ...standardChangeAddressFilters, - const ObjectFilter( - property: "derivationPath", - filter: FilterCondition.startsWith( - property: "value", - value: "m/44'/899", - ), - ), - ], - ); + FilterOperation? get changeAddressFilterOperation => FilterGroup.and([ + ...standardChangeAddressFilters, + const ObjectFilter( + property: "derivationPath", + filter: FilterCondition.startsWith(property: "value", value: "m/44'/899"), + ), + ]); @override - FilterOperation? get receivingAddressFilterOperation => FilterGroup.and( - [ - ...standardReceivingAddressFilters, - const ObjectFilter( - property: "derivationPath", - filter: FilterCondition.startsWith( - property: "value", - value: "m/44'/899", - ), - ), - ], - ); + FilterOperation? get receivingAddressFilterOperation => FilterGroup.and([ + ...standardReceivingAddressFilters, + const ObjectFilter( + property: "derivationPath", + filter: FilterCondition.startsWith(property: "value", value: "m/44'/899"), + ), + ]); // =========================================================================== @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .typeEqualTo(AddressType.nonWallet) - .and() - .not() - .subTypeEqualTo(AddressSubType.nonWallet) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .not() + .subTypeEqualTo(AddressSubType.nonWallet) + .findAll(); return allAddresses; } @@ -96,28 +87,32 @@ class EcashWallet extends Bip39HDWallet final List
allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); final List> allTransactions = []; for (final txHash in allTxHashes) { - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -129,8 +124,9 @@ class EcashWallet extends Bip39HDWallet ); // check for duplicates before adding to list - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -288,7 +284,8 @@ class EcashWallet extends Bip39HDWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -304,12 +301,8 @@ class EcashWallet 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, @@ -321,8 +314,9 @@ class EcashWallet extends Bip39HDWallet if (scriptPubKeyHex != null) { // check for cash tokens try { - final ctOutput = - cash_tokens.unwrap_spk(scriptPubKeyHex.toUint8ListFromHex); + final ctOutput = cash_tokens.unwrap_spk( + scriptPubKeyHex.toUint8ListFromHex, + ); if (ctOutput.token_data != null) { // found a token! blocked = true; @@ -350,19 +344,23 @@ class EcashWallet extends Bip39HDWallet // TODO: correct formula for ecash? @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: info.coin.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } @override diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 10238cef6..ea6654b12 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -53,15 +53,17 @@ class EpiccashWallet extends Bip39Wallet { final int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; final _chainHeight = await chainHeight; final double restorePercent = lastScannedBlock / _chainHeight; - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(highestPercent, walletId)); + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercent, walletId), + ); if (restorePercent > highestPercent) { highestPercent = restorePercent; } final int blocksRemaining = _chainHeight - lastScannedBlock; - GlobalEventBus.instance - .fire(BlocksRemainingEvent(blocksRemaining, walletId)); + GlobalEventBus.instance.fire( + BlocksRemainingEvent(blocksRemaining, walletId), + ); return restorePercent < 0 ? 0.0 : restorePercent; } @@ -84,17 +86,14 @@ class EpiccashWallet extends Bip39Wallet { Future cancelPendingTransactionAndPost(String txSlateId) async { try { _hackedCheckTorNodePrefs(); - final String wallet = (await secureStorageInterface.read( - key: '${walletId}_wallet', - ))!; + final String wallet = + (await secureStorageInterface.read(key: '${walletId}_wallet'))!; final result = await epiccash.LibEpiccash.cancelTransaction( wallet: wallet, transactionId: txSlateId, ); - Logging.instance.d( - "cancel $txSlateId result: $result", - ); + Logging.instance.d("cancel $txSlateId result: $result"); return result; } catch (e, s) { Logging.instance.e("", error: e, stackTrace: s); @@ -154,8 +153,10 @@ class EpiccashWallet extends Bip39Wallet { config["chain"] = "mainnet"; config["account"] = "default"; config["api_listen_port"] = port; - config["api_listen_interface"] = - nodeApiAddress.replaceFirst(uri.scheme, ""); + config["api_listen_interface"] = nodeApiAddress.replaceFirst( + uri.scheme, + "", + ); final String stringConfig = jsonEncode(config); return stringConfig; } @@ -219,12 +220,14 @@ class EpiccashWallet extends Bip39Wallet { } Future< - ({ - double awaitingFinalization, - double pending, - double spendable, - double total - })> _allWalletBalances() async { + ({ + double awaitingFinalization, + double pending, + double spendable, + double total, + }) + > + _allWalletBalances() async { _hackedCheckTorNodePrefs(); final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); const refreshFromNode = 0; @@ -243,9 +246,7 @@ class EpiccashWallet extends Bip39Wallet { try { final uri = Uri.parse('wss://$host:$port'); - channel = WebSocketChannel.connect( - uri, - ); + channel = WebSocketChannel.connect(uri); await channel.ready; @@ -280,9 +281,7 @@ class EpiccashWallet extends Bip39Wallet { "to": to, }; await info.updateExtraEpiccashWalletInfo( - epicData: info.epicData!.copyWith( - slatesToCommits: slatesToCommits, - ), + epicData: info.epicData!.copyWith(slatesToCommits: slatesToCommits), isar: mainDB.isar, ); return true; @@ -305,9 +304,7 @@ class EpiccashWallet extends Bip39Wallet { } /// Only index 0 is currently used in stack wallet. - Future
_generateAndStoreReceivingAddressForIndex( - int index, - ) async { + Future
_generateAndStoreReceivingAddressForIndex(int index) async { // Since only 0 is a valid index in stack wallet at this time, lets just // throw is not zero if (index != 0) { @@ -338,9 +335,7 @@ class EpiccashWallet extends Bip39Wallet { epicboxConfig: epicboxConfig.toString(), ); - Logging.instance.d( - "WALLET_ADDRESS_IS $walletAddress", - ); + Logging.instance.d("WALLET_ADDRESS_IS $walletAddress"); final address = Address( walletId: walletId, @@ -359,8 +354,9 @@ class EpiccashWallet extends Bip39Wallet { try { //First stop the current listener epiccash.LibEpiccash.stopEpicboxListener(); - final wallet = - await secureStorageInterface.read(key: '${walletId}_wallet'); + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); // max number of blocks to scan per loop iteration const scanChunkSize = 10000; @@ -387,9 +383,7 @@ class EpiccashWallet extends Bip39Wallet { // update local cache await info.updateExtraEpiccashWalletInfo( - epicData: info.epicData!.copyWith( - lastScannedBlock: nextScannedBlock, - ), + epicData: info.epicData!.copyWith(lastScannedBlock: nextScannedBlock), isar: mainDB.isar, ); @@ -470,8 +464,9 @@ class EpiccashWallet extends Bip39Wallet { @override Future init({bool? isRestore}) async { if (isRestore != true) { - String? encodedWallet = - await secureStorageInterface.read(key: "${walletId}_wallet"); + String? encodedWallet = await secureStorageInterface.read( + key: "${walletId}_wallet", + ); // check if should create a new wallet if (encodedWallet == null) { @@ -543,8 +538,9 @@ class EpiccashWallet extends Bip39Wallet { ); final config = await _getRealConfig(); - final password = - await secureStorageInterface.read(key: '${walletId}_password'); + final password = await secureStorageInterface.read( + key: '${walletId}_password', + ); final walletOpen = await epiccash.LibEpiccash.openWallet( config: config, @@ -558,8 +554,11 @@ class EpiccashWallet extends Bip39Wallet { await updateNode(); } catch (e, s) { // do nothing, still allow user into wallet - Logging.instance - .w("$runtimeType init() failed: ", error: e, stackTrace: s); + Logging.instance.w( + "$runtimeType init() failed: ", + error: e, + stackTrace: s, + ); } } } @@ -571,8 +570,9 @@ class EpiccashWallet extends Bip39Wallet { Future confirmSend({required TxData txData}) async { try { _hackedCheckTorNodePrefs(); - final wallet = - await secureStorageInterface.read(key: '${walletId}_wallet'); + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); // TODO determine whether it is worth sending change to a change address. @@ -581,9 +581,7 @@ class EpiccashWallet extends Bip39Wallet { if (!receiverAddress.startsWith("http://") || !receiverAddress.startsWith("https://")) { - final bool isEpicboxConnected = await _testEpicboxServer( - epicboxConfig, - ); + final bool isEpicboxConnected = await _testEpicboxServer(epicboxConfig); if (!isEpicboxConnected) { throw Exception("Failed to send TX : Unable to reach epicbox server"); } @@ -618,9 +616,7 @@ class EpiccashWallet extends Bip39Wallet { txAddressInfo['to'] = txData.recipients!.first.address; await _putSendToAddresses(transaction, txAddressInfo); - return txData.copyWith( - txid: transaction.slateId, - ); + return txData.copyWith(txid: transaction.slateId); } catch (e, s) { Logging.instance.e("Epic cash confirmSend: ", error: e, stackTrace: s); rethrow; @@ -635,8 +631,7 @@ class EpiccashWallet extends Bip39Wallet { throw Exception("Epic cash prepare send requires a single recipient!"); } - ({String address, Amount amount, bool isChange}) recipient = - txData.recipients!.first; + TxRecipient recipient = txData.recipients!.first; final int realFee = await _nativeFee(recipient.amount.raw.toInt()); final feeAmount = Amount( @@ -651,17 +646,10 @@ class EpiccashWallet extends Bip39Wallet { } if (info.cachedBalance.spendable == recipient.amount) { - recipient = ( - address: recipient.address, - amount: recipient.amount - feeAmount, - isChange: recipient.isChange, - ); + recipient = recipient.copyWith(amount: recipient.amount - feeAmount); } - return txData.copyWith( - recipients: [recipient], - fee: feeAmount, - ); + return txData.copyWith(recipients: [recipient], fee: feeAmount); } catch (e, s) { Logging.instance.e("Epic cash prepareSend", error: e, stackTrace: s); rethrow; @@ -898,10 +886,7 @@ class EpiccashWallet extends Bip39Wallet { ), ); - await info.updateBalance( - newBalance: balance, - isar: mainDB.isar, - ); + await info.updateBalance(newBalance: balance, isar: mainDB.isar); } catch (e, s) { Logging.instance.w( "Epic cash wallet failed to update balance: ", @@ -915,20 +900,22 @@ class EpiccashWallet extends Bip39Wallet { Future updateTransactions() async { try { _hackedCheckTorNodePrefs(); - final wallet = - await secureStorageInterface.read(key: '${walletId}_wallet'); + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); const refreshFromNode = 1; - final myAddresses = await mainDB - .getAddresses(walletId) - .filter() - .typeEqualTo(AddressType.mimbleWimble) - .and() - .subTypeEqualTo(AddressSubType.receiving) - .and() - .valueIsNotEmpty() - .valueProperty() - .findAll(); + final myAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .typeEqualTo(AddressType.mimbleWimble) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .and() + .valueIsNotEmpty() + .valueProperty() + .findAll(); final myAddressesSet = myAddresses.toSet(); final transactions = await epiccash.LibEpiccash.getTransactions( @@ -943,7 +930,7 @@ class EpiccashWallet extends Bip39Wallet { for (final tx in transactions) { final isIncoming = tx.txType == epic_models.TransactionType.TxReceived || - tx.txType == epic_models.TransactionType.TxReceivedCancelled; + tx.txType == epic_models.TransactionType.TxReceivedCancelled; final slateId = tx.txSlateId; final commitId = slatesToCommits[slateId]?['commitId'] as String?; final numberOfMessages = tx.messages?.messages.length; @@ -964,9 +951,7 @@ class EpiccashWallet extends Bip39Wallet { OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: credit.toString(), - addresses: [ - if (addressFrom != null) addressFrom, - ], + addresses: [if (addressFrom != null) addressFrom], walletOwns: true, ); final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -1010,11 +995,12 @@ class EpiccashWallet extends Bip39Wallet { "onChainNote": onChainNote, "isCancelled": tx.txType == epic_models.TransactionType.TxSentCancelled || - tx.txType == epic_models.TransactionType.TxReceivedCancelled, - "overrideFee": Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + tx.txType == epic_models.TransactionType.TxReceivedCancelled, + "overrideFee": + Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }; final txn = TransactionV2( @@ -1092,11 +1078,7 @@ class EpiccashWallet extends Bip39Wallet { ) != null; } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + Logging.instance.e("", error: e, stackTrace: s); return false; } } @@ -1105,8 +1087,9 @@ class EpiccashWallet extends Bip39Wallet { Future updateChainHeight() async { _hackedCheckTorNodePrefs(); final config = await _getRealConfig(); - final latestHeight = - await epiccash.LibEpiccash.getChainHeight(config: config); + final latestHeight = await epiccash.LibEpiccash.getChainHeight( + config: config, + ); await info.updateCachedChainHeight( newHeight: latestHeight, isar: mainDB.isar, @@ -1114,7 +1097,7 @@ class EpiccashWallet extends Bip39Wallet { } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { _hackedCheckTorNodePrefs(); // setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function????? final int currentFee = await _nativeFee( @@ -1135,9 +1118,9 @@ class EpiccashWallet extends Bip39Wallet { numberOfBlocksFast: 10, numberOfBlocksAverage: 10, numberOfBlocksSlow: 10, - fast: 1, - medium: 1, - slow: 1, + fast: BigInt.one, + medium: BigInt.one, + slow: BigInt.one, ); } @@ -1202,10 +1185,7 @@ Future deleteEpicWallet({ return "Tried to delete non existent epic wallet file with walletId=$walletId"; } else { try { - return epiccash.LibEpiccash.deleteWallet( - wallet: wallet, - config: config!, - ); + return epiccash.LibEpiccash.deleteWallet(wallet: wallet, config: config!); } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); return "deleteEpicWallet($walletId) failed..."; diff --git a/lib/wallets/wallet/impl/ethereum_wallet.dart b/lib/wallets/wallet/impl/ethereum_wallet.dart index e61da1df6..efb19bcbc 100644 --- a/lib/wallets/wallet/impl/ethereum_wallet.dart +++ b/lib/wallets/wallet/impl/ethereum_wallet.dart @@ -5,8 +5,10 @@ import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; +import 'package:web3dart/json_rpc.dart' show RPCError; import 'package:web3dart/web3dart.dart' as web3; +import '../../../dto/ethereum/eth_tx_dto.dart'; import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; @@ -57,9 +59,10 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { return web3.Web3Client(node.host, client); } - Amount estimateEthFee(int feeRate, int gasLimit, int decimals) { + Amount estimateEthFee(BigInt feeRate, int gasLimit, int decimals) { final gweiAmount = feeRate.toDecimal() / (Decimal.ten.pow(9).toDecimal()); - final fee = gasLimit.toDecimal() * + final fee = + gasLimit.toDecimal() * gweiAmount.toDecimal( scaleOnInfinitePrecision: cryptoCurrency.fractionDigits, ); @@ -95,9 +98,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), - addresses: [ - addressTo, - ], + addresses: [addressTo], walletOwns: addressTo == myAddress, ); final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -132,16 +133,15 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: -1, - type: addressTo == myAddress - ? TransactionType.sentToSelf - : TransactionType.outgoing, + type: + addressTo == myAddress + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.none, otherData: jsonEncode(otherData), ); - return txData.copyWith( - tempTx: txn, - ); + return txData.copyWith(tempTx: txn); } // ==================== Overrides ============================================ @@ -151,11 +151,11 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { @override FilterOperation? get transactionFilterOperation => FilterGroup.not( - const FilterCondition.equalTo( - property: r"subType", - value: TransactionSubType.ethToken, - ), - ); + const FilterCondition.equalTo( + property: r"subType", + value: TransactionSubType.ethToken, + ), + ); @override FilterOperation? get changeAddressFilterOperation => @@ -189,7 +189,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { return estimateEthFee( feeRate, (cryptoCurrency as Ethereum).gasLimit, @@ -198,7 +198,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { } @override - Future get fees => EthereumAPI.getFees(); + Future get fees => EthereumAPI.getFees(); @override Future pingCheck() async { @@ -235,10 +235,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { fractionDigits: cryptoCurrency.fractionDigits, ), ); - await info.updateBalance( - newBalance: balance, - isar: mainDB.isar, - ); + await info.updateBalance(newBalance: balance, isar: mainDB.isar); } catch (e, s) { Logging.instance.w( "$runtimeType wallet failed to update balance: ", @@ -254,10 +251,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { final client = getEthClient(); final height = await client.getBlockNumber(); - await info.updateCachedChainHeight( - newHeight: height, - isar: mainDB.isar, - ); + await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } catch (e, s) { Logging.instance.w( "$runtimeType Exception caught in chainHeight: ", @@ -279,7 +273,8 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { int firstBlock = 0; if (!isRescan) { - firstBlock = await mainDB.isar.transactionV2s + firstBlock = + await mainDB.isar.transactionV2s .where() .walletIdEqualTo(walletId) .heightProperty() @@ -300,8 +295,8 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { if (response.value == null) { Logging.instance.w( - "Failed to refresh transactions for ${cryptoCurrency.prettyName} ${info.name} " - "$walletId: ${response.exception}", + "Failed to refresh transactions for ${cryptoCurrency.prettyName}" + " ${info.name} $walletId: ${response.exception}", ); return; } @@ -311,108 +306,121 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { return; } - final txsResponse = - await EthereumAPI.getEthTransactionNonces(response.value!); + web3.Web3Client? client; + final List allTxs = []; + for (final dto in response.value!) { + if (dto.nonce == null) { + client ??= getEthClient(); + final txInfo = await client.getTransactionByHash(dto.hash); + if (txInfo == null) { + // Something strange is happening + Logging.instance.w( + "Could not find transaction via RPC that was found use TrueBlocks " + "API.\nOffending tx: $dto", + ); + } else { + final updated = dto.copyWith(nonce: txInfo.nonce); + allTxs.add(updated); + } + } else { + allTxs.add(dto); + } + } + + final List txns = []; + for (final element in allTxs) { + if (element.hasToken && !element.isError) { + continue; + } - if (txsResponse.value != null) { - final allTxs = txsResponse.value!; - final List txns = []; - for (final tuple in allTxs) { - final element = tuple.item1; + //Calculate fees (GasLimit * gasPrice) + // int txFee = element.gasPrice * element.gasUsed; + final Amount txFee = element.gasCost; + final transactionAmount = element.value; + final addressFrom = checksumEthereumAddress(element.from); + final String addressTo; + try { + addressTo = checksumEthereumAddress(element.to); + } catch (e, s) { + Logging.instance.w("Ignoring eth transaction:\n$e\n$s"); + // temp "fix" + continue; + } - if (element.hasToken && !element.isError) { - continue; + bool isIncoming; + bool txFailed = false; + if (addressFrom == thisAddress) { + if (element.isError) { + txFailed = true; } + isIncoming = false; + } else if (addressTo == thisAddress) { + isIncoming = true; + } else { + continue; + } - //Calculate fees (GasLimit * gasPrice) - // int txFee = element.gasPrice * element.gasUsed; - final Amount txFee = element.gasCost; - final transactionAmount = element.value; - final addressFrom = checksumEthereumAddress(element.from); - final addressTo = checksumEthereumAddress(element.to); - - bool isIncoming; - bool txFailed = false; - if (addressFrom == thisAddress) { - if (element.isError) { - txFailed = true; - } - isIncoming = false; - } else if (addressTo == thisAddress) { - isIncoming = true; - } else { - continue; - } + // hack eth tx data into inputs and outputs + final List outputs = []; + final List inputs = []; - // hack eth tx data into inputs and outputs - final List outputs = []; - final List inputs = []; - - final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( - scriptPubKeyHex: "00", - valueStringSats: transactionAmount.raw.toString(), - addresses: [ - addressTo, - ], - walletOwns: addressTo == thisAddress, - ); - final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( - scriptSigHex: null, - scriptSigAsm: null, - sequence: null, - outpoint: null, - addresses: [addressFrom], - valueStringSats: transactionAmount.raw.toString(), - witness: null, - innerRedeemScriptAsm: null, - coinbase: null, - walletOwns: addressFrom == thisAddress, - ); + final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "00", + valueStringSats: transactionAmount.raw.toString(), + addresses: [addressTo], + walletOwns: addressTo == thisAddress, + ); + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: null, + scriptSigAsm: null, + sequence: null, + outpoint: null, + addresses: [addressFrom], + valueStringSats: transactionAmount.raw.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: addressFrom == thisAddress, + ); - final TransactionType txType; - if (isIncoming) { - if (addressFrom == addressTo) { - txType = TransactionType.sentToSelf; - } else { - txType = TransactionType.incoming; - } + final TransactionType txType; + if (isIncoming) { + if (addressFrom == addressTo) { + txType = TransactionType.sentToSelf; } else { - txType = TransactionType.outgoing; + txType = TransactionType.incoming; } + } else { + txType = TransactionType.outgoing; + } - outputs.add(output); - inputs.add(input); - - final otherData = { - "nonce": tuple.item2, - "isCancelled": txFailed, - "overrideFee": txFee.toJsonString(), - }; - - final txn = TransactionV2( - walletId: walletId, - blockHash: element.blockHash, - hash: element.hash, - txid: element.hash, - timestamp: element.timestamp, - height: element.blockNumber, - inputs: List.unmodifiable(inputs), - outputs: List.unmodifiable(outputs), - version: -1, - type: txType, - subType: TransactionSubType.none, - otherData: jsonEncode(otherData), - ); + outputs.add(output); + inputs.add(input); - txns.add(txn); - } - await mainDB.updateOrPutTransactionV2s(txns); - } else { - Logging.instance.w( - "Failed to refresh transactions with nonces for ${cryptoCurrency.prettyName} " - "${info.name} $walletId: ${txsResponse.exception}", + final otherData = { + "nonce": element.nonce, + "isCancelled": txFailed, + "overrideFee": txFee.toJsonString(), + }; + + final txn = TransactionV2( + walletId: walletId, + blockHash: element.blockHash, + hash: element.hash, + txid: element.hash, + timestamp: element.timestamp, + height: element.blockNumber, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + version: -1, + type: txType, + subType: TransactionSubType.none, + otherData: jsonEncode(otherData), ); + + txns.add(txn); } + await mainDB.updateOrPutTransactionV2s(txns); } @override @@ -421,88 +429,128 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { return false; } - @override - Future prepareSend({required TxData txData}) async { - final int - rate; // TODO: use BigInt for feeObject whenever FeeObject gets redone + Future getMyWeb3Address() async { + final myAddress = (await getCurrentReceivingAddress())!.value; + final myWeb3Address = web3.EthereumAddress.fromHex(myAddress); + return myWeb3Address; + } + + Future< + ({ + int nonce, + BigInt chainId, + BigInt baseFee, + BigInt maxBaseFee, + BigInt priorityFee, + }) + > + internalSharedPrepareSend({ + required TxData txData, + required web3.EthereumAddress myWeb3Address, + }) async { + if (txData.feeRateType == null) throw Exception("Missing fee rate type."); + if (txData.feeRateType == FeeRateType.custom && + txData.ethEIP1559Fee == null) { + throw Exception("Missing custom EIP-1559 values."); + } + + await updateBalance(); + + final client = getEthClient(); + final chainId = await client.getChainId(); + final nonce = + txData.nonce ?? + await client.getTransactionCount( + myWeb3Address, + atBlock: const web3.BlockNum.pending(), + ); + final feeObject = await fees; + final baseFee = feeObject.suggestBaseFee; + BigInt maxBaseFee = baseFee; + BigInt priorityFee; + switch (txData.feeRateType!) { case FeeRateType.fast: - rate = feeObject.fast; + priorityFee = feeObject.fast - baseFee; + if (priorityFee.isNegative) priorityFee = BigInt.zero; break; + case FeeRateType.average: - rate = feeObject.medium; + priorityFee = feeObject.medium - baseFee; + if (priorityFee.isNegative) priorityFee = BigInt.zero; break; + case FeeRateType.slow: - rate = feeObject.slow; + priorityFee = feeObject.slow - baseFee; + if (priorityFee.isNegative) priorityFee = BigInt.zero; break; + case FeeRateType.custom: - throw UnimplementedError("custom eth fees"); + priorityFee = txData.ethEIP1559Fee!.priorityFeeWei; + maxBaseFee = txData.ethEIP1559Fee!.maxBaseFeeWei; + break; } - final feeEstimate = await estimateFeeFor(Amount.zero, rate); - - // bool isSendAll = false; - // final availableBalance = balance.spendable; - // if (satoshiAmount == availableBalance) { - // isSendAll = true; - // } - // - // if (isSendAll) { - // //Subtract fee amount from send amount - // satoshiAmount -= feeEstimate; - // } - - final client = getEthClient(); + if (baseFee > maxBaseFee) { + throw Exception("Base cannot be greater than max base fee"); + } + if (priorityFee > maxBaseFee) { + throw Exception("Priority fee cannot be greater than max base fee"); + } - final myAddress = (await getCurrentReceivingAddress())!.value; - final myWeb3Address = web3.EthereumAddress.fromHex(myAddress); + return ( + nonce: nonce, + chainId: chainId, + baseFee: baseFee, + maxBaseFee: maxBaseFee, + priorityFee: priorityFee, + ); + } + @override + Future prepareSend({required TxData txData}) async { final amount = txData.recipients!.first.amount; final address = txData.recipients!.first.address; - // final est = await client.estimateGas( - // sender: myWeb3Address, - // to: web3.EthereumAddress.fromHex(address), - // gasPrice: web3.EtherAmount.fromUnitAndValue( - // web3.EtherUnit.wei, - // rate, - // ), - // amountOfGas: BigInt.from((cryptoCurrency as Ethereum).gasLimit), - // value: web3.EtherAmount.inWei(amount.raw), - // ); - - final nonce = txData.nonce ?? - await client.getTransactionCount( - myWeb3Address, - atBlock: const web3.BlockNum.pending(), - ); + final myWeb3Address = await getMyWeb3Address(); - // final nResponse = await EthereumAPI.getAddressNonce(address: myAddress); - // print("=============================================================="); - // print("ETH client.estimateGas: $est"); - // print("ETH estimateFeeFor : $feeEstimate"); - // print("ETH nonce custom response: $nResponse"); - // print("ETH actual nonce : $nonce"); - // print("=============================================================="); + final prep = await internalSharedPrepareSend( + txData: txData, + myWeb3Address: myWeb3Address, + ); + + // double check balance after internalSharedPrepareSend call to ensure + // balance is up to date + if (amount > info.cachedBalance.spendable) { + throw Exception("Insufficient balance"); + } final tx = web3.Transaction( to: web3.EthereumAddress.fromHex(address), - gasPrice: web3.EtherAmount.fromUnitAndValue( + maxGas: txData.ethEIP1559Fee?.gasLimit ?? kEthereumMinGasLimit, + value: web3.EtherAmount.inWei(amount.raw), + nonce: prep.nonce, + maxFeePerGas: web3.EtherAmount.fromBigInt( web3.EtherUnit.wei, - rate, + prep.maxBaseFee, ), - maxGas: (cryptoCurrency as Ethereum).gasLimit, - value: web3.EtherAmount.inWei(amount.raw), - nonce: nonce, + maxPriorityFeePerGas: web3.EtherAmount.fromBigInt( + web3.EtherUnit.wei, + prep.priorityFee, + ), + ); + + final feeEstimate = await estimateFeeFor( + Amount.zero, + prep.maxBaseFee + prep.priorityFee, ); return txData.copyWith( nonce: tx.nonce, web3dartTransaction: tx, fee: feeEstimate, - feeInWei: BigInt.from(rate), - chainId: (await client.getChainId()), + chainId: prep.chainId, ); } @@ -516,21 +564,24 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { await _initCredentials(); } - final txid = await client.sendTransaction( - _credentials!, - txData.web3dartTransaction!, - chainId: txData.chainId!.toInt(), - ); + try { + final txid = await client.sendTransaction( + _credentials!, + txData.web3dartTransaction!, + chainId: txData.chainId!.toInt(), + ); - final data = (prepareTempTx ?? _prepareTempTx)( - txData.copyWith( - txid: txid, - txHash: txid, - ), - (await getCurrentReceivingAddress())!.value, - ); + final data = (prepareTempTx ?? _prepareTempTx)( + txData.copyWith(txid: txid, txHash: txid), + (await getCurrentReceivingAddress())!.value, + ); - return await updateSentCachedTxData(txData: data); + return await updateSentCachedTxData(txData: data); + } on RPCError catch (e) { + final message = + "${e.toString()}${e.data == null ? "" : e.data.toString()}"; + throw Exception(message); + } } @override diff --git a/lib/wallets/wallet/impl/fact0rn_wallet.dart b/lib/wallets/wallet/impl/fact0rn_wallet.dart new file mode 100644 index 000000000..14a9d4be8 --- /dev/null +++ b/lib/wallets/wallet/impl/fact0rn_wallet.dart @@ -0,0 +1,326 @@ +import 'package:isar/isar.dart'; + +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/extensions/extensions.dart'; +import '../../../utilities/logger.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../intermediate/bip39_hd_wallet.dart'; +import '../wallet_mixin_interfaces/coin_control_interface.dart'; +import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/extended_keys_interface.dart'; + +class Fact0rnWallet + extends Bip39HDWallet + with ElectrumXInterface, ExtendedKeysInterface, CoinControlInterface { + Fact0rnWallet(CryptoCurrencyNetwork network) : super(Fact0rn(network) as T); + + @override + int get isarTransactionVersion => 2; + + @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 + Future updateTransactions() async { + // Get all addresses. + final List
allAddressesOld = + await fetchAddressesForElectrumXScan(); + + // Separate receiving and change addresses. + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); + + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + // Fetch history from ElectrumX. + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); + + // Only parse new txs (not in db yet). + final List> allTransactions = []; + for (final txHash in allTxHashes) { + // Check for duplicates by searching for tx by tx_hash in db. + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); + + if (storedTx == null || + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { + // Tx not in db yet. + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + cryptoCurrency: cryptoCurrency, + ); + + // 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, + cryptoCurrency: cryptoCurrency, + ); + + 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; + final 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. + } + + // Fact0rn has special outputs like deadpool bounties + announcements, but they're unsupported. + // This is where we would check for them. + // TODO: [prio=none] Check for special Fact0rn outputs. + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.e("Unexpected tx found (ignoring it)"); + Logging.instance.d("Unexpected tx found (ignoring it): $txData"); + 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<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( + Map jsonUTXO, + String? scriptPubKeyHex, + Map jsonTX, + String? utxoOwnerAddress, + ) async { + bool blocked = false; + String? blockedReason; + + // check for bip47 notification + final outputs = jsonTX["vout"] as List; + for (final output in outputs) { + final 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); + } + + // Typical SegWit estimation + @override + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB.toInt() / 1000).ceil(), + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); + } +} diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index aed72a33a..0bb1b1fed 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -23,7 +23,6 @@ import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; import '../wallet_mixin_interfaces/extended_keys_interface.dart'; -import '../wallet_mixin_interfaces/lelantus_interface.dart'; import '../wallet_mixin_interfaces/spark_interface.dart'; const sparkStartBlock = 819300; // (approx 18 Jan 2024) @@ -32,11 +31,9 @@ class FiroWallet extends Bip39HDWallet with ElectrumXInterface, ExtendedKeysInterface, - LelantusInterface, SparkInterface, CoinControlInterface { // IMPORTANT: The order of the above mixins matters. - // SparkInterface MUST come after LelantusInterface. FiroWallet(CryptoCurrencyNetwork network) : super(Firo(network) as T); @@ -58,11 +55,23 @@ class FiroWallet extends Bip39HDWallet @override Future updateSentCachedTxData({required TxData txData}) async { if (txData.tempTx != null) { + final otherDataString = txData.tempTx!.otherData; + final Map map; + if (otherDataString == null) { + map = {}; + } else { + map = jsonDecode(otherDataString) as Map? ?? {}; + } + + map[TxV2OdKeys.isInstantLock] = true; + + txData = txData.copyWith( + tempTx: txData.tempTx!.copyWith(otherData: jsonEncode(map)), + ); + await mainDB.updateOrPutTransactionV2s([txData.tempTx!]); _unconfirmedTxids.add(txData.tempTx!.txid); - Logging.instance.d( - "Added firo unconfirmed: ${txData.tempTx!.txid}", - ); + Logging.instance.d("Added firo unconfirmed: ${txData.tempTx!.txid}"); } return txData; } @@ -72,36 +81,41 @@ class FiroWallet extends Bip39HDWallet final List
allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); - final sparkCoins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .findAll(); + final sparkCoins = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .findAll(); final List> allTransactions = []; // some lelantus transactions aren't fetched via wallet addresses so they // will never show as confirmed in the gui. - final unconfirmedTransactions = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .heightIsNull() - .findAll(); + final unconfirmedTransactions = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightIsNull() + .findAll(); for (final tx in unconfirmedTransactions) { final txn = await electrumXCachedClient.getTransaction( txHash: tx.txid, @@ -113,10 +127,7 @@ class FiroWallet extends Bip39HDWallet if (height != null) { // tx was mined // add to allTxHashes - final info = { - "tx_hash": tx.txid, - "height": height, - }; + final info = {"tx_hash": tx.txid, "height": height}; allTxHashes.add(info); } } @@ -126,10 +137,7 @@ class FiroWallet extends Bip39HDWallet sparkTxids.add(coin.txHash); // check for duplicates before adding to list if (allTxHashes.indexWhere((e) => e["tx_hash"] == coin.txHash) == -1) { - final info = { - "tx_hash": coin.txHash, - "height": coin.height, - }; + final info = {"tx_hash": coin.txHash, "height": coin.height}; allTxHashes.add(info); } } @@ -138,9 +146,7 @@ class FiroWallet extends Bip39HDWallet for (final txid in missing.map((e) => e.txid).toSet()) { // check for duplicates before adding to list if (allTxHashes.indexWhere((e) => e["tx_hash"] == txid) == -1) { - final info = { - "tx_hash": txid, - }; + final info = {"tx_hash": txid}; allTxHashes.add(info); } } @@ -148,12 +154,13 @@ class FiroWallet extends Bip39HDWallet final currentHeight = await chainHeight; for (final txHash in allTxHashes) { - final storedTx = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(txHash["tx_hash"] as String) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(txHash["tx_hash"] as String) + .findFirst(); if (storedTx?.isConfirmed( currentHeight, @@ -180,8 +187,9 @@ class FiroWallet extends Bip39HDWallet } // check for duplicates before adding to list - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] ??= txHash["height"]; allTransactions.add(tx); @@ -290,17 +298,19 @@ class FiroWallet extends Bip39HDWallet if (output.addresses.isEmpty && output.scriptPubKeyHex.length >= 488) { // likely spark related - final opByte = output.scriptPubKeyHex - .substring(0, 2) - .toUint8ListFromHex - .first; + final opByte = + output.scriptPubKeyHex + .substring(0, 2) + .toUint8ListFromHex + .first; if (opByte == OP_SPARKMINT || opByte == OP_SPARKSMINT) { final serCoin = base64Encode( output.scriptPubKeyHex.substring(2, 488).toUint8ListFromHex, ); - final coin = sparkCoinsInvolvedReceived - .where((e) => e.serializedCoinB64!.startsWith(serCoin)) - .firstOrNull; + final coin = + sparkCoinsInvolvedReceived + .where((e) => e.serializedCoinB64!.startsWith(serCoin)) + .firstOrNull; if (coin == null) { // not ours @@ -308,9 +318,7 @@ class FiroWallet extends Bip39HDWallet output = output.copyWith( walletOwns: true, valueStringSats: coin.value.toString(), - addresses: [ - coin.address, - ], + addresses: [coin.address], ); } } @@ -395,11 +403,10 @@ class FiroWallet extends Bip39HDWallet txid: txData["txid"] as String, network: cryptoCurrency.network, ); - spentSparkCoins = sparkCoinsInvolvedSpent - .where( - (e) => tags.contains(e.lTagHash), - ) - .toList(); + spentSparkCoins = + sparkCoinsInvolvedSpent + .where((e) => tags.contains(e.lTagHash)) + .toList(); } else if (isSparkSpend) { parseAnonFees(); } else if (isSparkMint) { @@ -483,10 +490,11 @@ class FiroWallet extends Bip39HDWallet if (usedCoins.isNotEmpty) { input = input.copyWith( addresses: usedCoins.map((e) => e.address).toList(), - valueStringSats: usedCoins - .map((e) => e.value) - .reduce((value, element) => value += element) - .toString(), + valueStringSats: + usedCoins + .map((e) => e.value) + .reduce((value, element) => value += element) + .toString(), walletOwns: true, ); wasSentFromThisWallet = true; @@ -497,10 +505,11 @@ class FiroWallet extends Bip39HDWallet spentSparkCoins.isNotEmpty) { input = input.copyWith( addresses: spentSparkCoins.map((e) => e.address).toList(), - valueStringSats: spentSparkCoins - .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + e) - .toString(), + valueStringSats: + spentSparkCoins + .map((e) => e.value) + .fold(BigInt.zero, (p, e) => p + e) + .toString(), walletOwns: true, ); wasSentFromThisWallet = true; @@ -568,13 +577,14 @@ class FiroWallet extends Bip39HDWallet continue; } - String? otherData; + final isInstantLock = txData["instantlock"] as bool? ?? false; + + final otherData = { + TxV2OdKeys.isInstantLock: isInstantLock, + }; + if (anonFees != null) { - otherData = jsonEncode( - { - "overrideFee": anonFees!.toJsonString(), - }, - ); + otherData[TxV2OdKeys.overrideFee] = anonFees!.toJsonString(); } final tx = TransactionV2( @@ -584,13 +594,14 @@ class FiroWallet extends Bip39HDWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), type: type, subType: subType, - otherData: otherData, + otherData: jsonEncode(otherData), ); if (_unconfirmedTxids.contains(tx.txid)) { @@ -599,26 +610,18 @@ class FiroWallet extends Bip39HDWallet cryptoCurrency.minConfirms, cryptoCurrency.minCoinbaseConfirms, )) { - txns.add(tx); _unconfirmedTxids.removeWhere((e) => e == tx.txid); - } else { - // don't update in db until confirmed } - } else { - txns.add(tx); } + txns.add(tx); } await mainDB.updateOrPutTransactionV2s(txns); } @override - Future< - ({ - String? blockedReason, - bool blocked, - String? utxoLabel, - })> checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map? jsonTX, @@ -631,7 +634,8 @@ class FiroWallet extends Bip39HDWallet if (jsonUTXO["value"] is int) { // TODO: [prio=high] use special electrumx call to verify the 1000 Firo output is masternode // electrumx call should exist now. Unsure if it works though - blocked = Amount.fromDecimal( + blocked = + Amount.fromDecimal( Decimal.fromInt( 1000, // 1000 firo output is a possible master node ), @@ -654,7 +658,8 @@ class FiroWallet extends Bip39HDWallet } if (blocked) { - blockedReason = "Possible masternode collateral. " + blockedReason = + "Possible masternode collateral. " "Unlock and spend at your own risk."; label = "Possible masternode collateral"; } @@ -702,22 +707,6 @@ class FiroWallet extends Bip39HDWallet await mainDB.deleteWalletBlockchainData(walletId); } - // lelantus - int? latestSetId; - final List> lelantusFutures = []; - final enableLelantusScanning = - info.otherData[WalletInfoKeys.enableLelantusScanning] as bool? ?? - false; - if (enableLelantusScanning) { - latestSetId = await electrumXClient.getLelantusLatestCoinId(); - lelantusFutures.add( - electrumXCachedClient.getUsedCoinSerials( - cryptoCurrency: info.coin, - ), - ); - lelantusFutures.add(getSetDataMap(latestSetId)); - } - // spark final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); final List> sparkAnonSetFutures = []; @@ -733,9 +722,9 @@ class FiroWallet extends Bip39HDWallet } final sparkUsedCoinTagsFuture = FiroCacheCoordinator.runFetchAndUpdateSparkUsedCoinTags( - electrumXClient, - cryptoCurrency.network, - ); + electrumXClient, + cryptoCurrency.network, + ); // receiving addresses Logging.instance.i("checking receiving addresses..."); @@ -745,17 +734,8 @@ class FiroWallet extends Bip39HDWallet for (final type in cryptoCurrency.supportedDerivationPathTypes) { receiveFutures.add( canBatch - ? checkGapsBatched( - txCountBatchSize, - root, - type, - receiveChain, - ) - : checkGapsLinearly( - root, - type, - receiveChain, - ), + ? checkGapsBatched(txCountBatchSize, root, type, receiveChain) + : checkGapsLinearly(root, type, receiveChain), ); } @@ -764,17 +744,8 @@ class FiroWallet extends Bip39HDWallet for (final type in cryptoCurrency.supportedDerivationPathTypes) { changeFutures.add( canBatch - ? checkGapsBatched( - txCountBatchSize, - root, - type, - changeChain, - ) - : checkGapsLinearly( - root, - type, - changeChain, - ), + ? checkGapsBatched(txCountBatchSize, root, type, changeChain) + : checkGapsLinearly(root, type, changeChain), ); } @@ -834,53 +805,11 @@ class FiroWallet extends Bip39HDWallet await mainDB.updateOrPutAddresses(addressesToStore); - await Future.wait([ - updateTransactions(), - updateUTXOs(), - ]); - - final List> futures = []; - if (enableLelantusScanning) { - futures.add(lelantusFutures[0]); - futures.add(lelantusFutures[1]); - } - futures.add(sparkUsedCoinTagsFuture); - futures.addAll(sparkAnonSetFutures); + await Future.wait([updateTransactions(), updateUTXOs()]); - final futureResults = await Future.wait(futures); + await Future.wait([sparkUsedCoinTagsFuture, ...sparkAnonSetFutures]); - // lelantus - Set? usedSerialsSet; - Map? setDataMap; - if (enableLelantusScanning) { - usedSerialsSet = (futureResults[0] as List).toSet(); - setDataMap = futureResults[1] as Map; - } - - if (Util.isDesktop) { - await Future.wait([ - if (enableLelantusScanning) - recoverLelantusWallet( - latestSetId: latestSetId!, - usedSerialNumbers: usedSerialsSet!, - setDataMap: setDataMap!, - ), - recoverSparkWallet( - latestSparkCoinId: latestSparkCoinId, - ), - ]); - } else { - if (enableLelantusScanning) { - await recoverLelantusWallet( - latestSetId: latestSetId!, - usedSerialNumbers: usedSerialsSet!, - setDataMap: setDataMap!, - ); - } - await recoverSparkWallet( - latestSparkCoinId: latestSparkCoinId, - ); - } + await recoverSparkWallet(latestSparkCoinId: latestSparkCoinId); }); unawaited(refresh()); @@ -900,37 +829,22 @@ class FiroWallet extends Bip39HDWallet } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); - } - - // =========================================================================== - - bool get lelantusCoinIsarRescanRequired => - info.otherData[WalletInfoKeys.lelantusCoinIsarRescanRequired] as bool? ?? - true; - - Future firoRescanRecovery() async { - try { - await recover(isRescan: true); - await info.updateOtherData( - newEntries: {WalletInfoKeys.lelantusCoinIsarRescanRequired: false}, - isar: mainDB.isar, - ); - return true; - } catch (_) { - return false; - } + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } } diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index 034056817..76205c252 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:isar/isar.dart'; +import '../../../db/drift/database.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; @@ -13,9 +14,11 @@ import '../../../utilities/logger.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../intermediate/bip39_hd_wallet.dart'; +import '../intermediate/external_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; import '../wallet_mixin_interfaces/extended_keys_interface.dart'; +import '../wallet_mixin_interfaces/mweb_interface.dart'; import '../wallet_mixin_interfaces/ordinals_interface.dart'; import '../wallet_mixin_interfaces/rbf_interface.dart'; @@ -26,7 +29,9 @@ class LitecoinWallet ExtendedKeysInterface, CoinControlInterface, RbfInterface, - OrdinalsInterface { + OrdinalsInterface, + MwebInterface + implements ExternalWallet { @override int get isarTransactionVersion => 2; @@ -44,17 +49,20 @@ class LitecoinWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.mweb) + .or() + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -67,14 +75,16 @@ class LitecoinWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -84,17 +94,19 @@ class LitecoinWallet ); // Fetch history from ElectrumX. - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -107,8 +119,9 @@ class LitecoinWallet ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -195,30 +208,52 @@ class LitecoinWallet // 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, - ); + try { + 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); - } + // 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); + outputs.add(output); + } catch (_) { + if (outputJson["ismweb"] == true) { + final outputId = outputJson["output_id"] as String; + + final db = Drift.get(walletId); + + final mwebUtxo = + await (db.select( + db.mwebUtxos, + )..where((e) => e.outputId.equals(outputId))).getSingleOrNull(); + + final output = OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "mweb", + scriptPubKeyAsm: null, + valueStringSats: mwebUtxo?.value.toString() ?? "0", + addresses: [outputId], + walletOwns: mwebUtxo != null, + ); + outputs.add(output); + } + } } final totalOut = outputs @@ -248,14 +283,19 @@ class LitecoinWallet // Check for special Litecoin outputs like ordinals. if (outputs.isNotEmpty) { // may not catch every case but it is much quicker - final hasOrdinal = await mainDB.isar.ordinals - .where() - .filter() - .walletIdEqualTo(walletId) - .utxoTXIDEqualTo(txData["txid"] as String) - .isNotEmpty(); + final hasOrdinal = + await mainDB.isar.ordinals + .where() + .filter() + .walletIdEqualTo(walletId) + .utxoTXIDEqualTo(txData["txid"] as String) + .isNotEmpty(); if (hasOrdinal) { subType = TransactionSubType.ordinal; + } else { + if (outputs.any((e) => e.scriptPubKeyHex == "mweb")) { + subType = TransactionSubType.mweb; + } } // making API calls for every output in every transaction is too expensive @@ -307,7 +347,8 @@ class LitecoinWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -323,79 +364,84 @@ class LitecoinWallet } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } -// -// @override -// Future coinSelection({required TxData txData}) async { -// final isCoinControl = txData.utxos != null; -// final isSendAll = txData.amount == info.cachedBalance.spendable; -// -// final utxos = -// txData.utxos?.toList() ?? await mainDB.getUTXOs(walletId).findAll(); -// -// final currentChainHeight = await chainHeight; -// final List spendableOutputs = []; -// int spendableSatoshiValue = 0; -// -// // Build list of spendable outputs and totaling their satoshi amount -// for (final utxo in utxos) { -// if (utxo.isBlocked == false && -// utxo.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms) && -// utxo.used != true) { -// spendableOutputs.add(utxo); -// spendableSatoshiValue += utxo.value; -// } -// } -// -// if (isCoinControl && spendableOutputs.length < utxos.length) { -// throw ArgumentError("Attempted to use an unavailable utxo"); -// } -// -// if (spendableSatoshiValue < txData.amount!.raw.toInt()) { -// throw Exception("Insufficient balance"); -// } else if (spendableSatoshiValue == txData.amount!.raw.toInt() && -// !isSendAll) { -// throw Exception("Insufficient balance to pay transaction fee"); -// } -// -// if (isCoinControl) { -// } else { -// final selection = cs.coinSelection( -// spendableOutputs -// .map((e) => cs.InputModel( -// i: e.vout, -// txid: e.txid, -// value: e.value, -// address: e.address, -// )) -// .toList(), -// txData.recipients! -// .map((e) => cs.OutputModel( -// address: e.address, -// value: e.amount.raw.toInt(), -// )) -// .toList(), -// txData.feeRateAmount!, -// 10, // TODO: ??????????????????????????????? -// ); -// -// // .inputs and .outputs will be null if no solution was found -// if (selection.inputs!.isEmpty || selection.outputs!.isEmpty) { -// throw Exception("coin selection failed"); -// } -// } -// } + + // + // @override + // Future coinSelection({required TxData txData}) async { + // final isCoinControl = txData.utxos != null; + // final isSendAll = txData.amount == info.cachedBalance.spendable; + // + // final utxos = + // txData.utxos?.toList() ?? await mainDB.getUTXOs(walletId).findAll(); + // + // final currentChainHeight = await chainHeight; + // final List spendableOutputs = []; + // int spendableSatoshiValue = 0; + // + // // Build list of spendable outputs and totaling their satoshi amount + // for (final utxo in utxos) { + // if (utxo.isBlocked == false && + // utxo.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms) && + // utxo.used != true) { + // spendableOutputs.add(utxo); + // spendableSatoshiValue += utxo.value; + // } + // } + // + // if (isCoinControl && spendableOutputs.length < utxos.length) { + // throw ArgumentError("Attempted to use an unavailable utxo"); + // } + // + // if (spendableSatoshiValue < txData.amount!.raw.toInt()) { + // throw Exception("Insufficient balance"); + // } else if (spendableSatoshiValue == txData.amount!.raw.toInt() && + // !isSendAll) { + // throw Exception("Insufficient balance to pay transaction fee"); + // } + // + // if (isCoinControl) { + // } else { + // final selection = cs.coinSelection( + // spendableOutputs + // .map((e) => cs.InputModel( + // i: e.vout, + // txid: e.txid, + // value: e.value, + // address: e.address, + // )) + // .toList(), + // txData.recipients! + // .map((e) => cs.OutputModel( + // address: e.address, + // value: e.amount.raw.toInt(), + // )) + // .toList(), + // txData.feeRateAmount!, + // 10, // TODO: ??????????????????????????????? + // ); + // + // // .inputs and .outputs will be null if no solution was found + // if (selection.inputs!.isEmpty || selection.outputs!.isEmpty) { + // throw Exception("coin selection failed"); + // } + // } + // } } diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index f1de036aa..03bbfd5c3 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -9,22 +9,17 @@ import '../intermediate/lib_monero_wallet.dart'; class MoneroWallet extends LibMoneroWallet { MoneroWallet(CryptoCurrencyNetwork network) - : super( - Monero(network), - lib_monero_compat.WalletType.monero, - ); + : super(Monero(network), lib_monero_compat.WalletType.monero); @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { if (libMoneroWallet == null || syncStatus is! lib_monero_compat.SyncedSyncStatus) { - return Amount.zeroWith( - fractionDigits: cryptoCurrency.fractionDigits, - ); + return Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits); } lib_monero.TransactionPriority priority; - switch (feeRate) { + switch (feeRate.toInt()) { case 1: priority = lib_monero.TransactionPriority.low; break; @@ -76,6 +71,7 @@ class MoneroWallet extends LibMoneroWallet { required String path, required String password, required int wordCount, + required String seedOffset, }) async { final lib_monero.MoneroSeedType type; switch (wordCount) { @@ -95,6 +91,7 @@ class MoneroWallet extends LibMoneroWallet { path: path, password: password, seedType: type, + seedOffset: seedOffset, ); } @@ -103,14 +100,15 @@ class MoneroWallet extends LibMoneroWallet { required String path, required String password, required String mnemonic, + required String seedOffset, int height = 0, - }) async => - await lib_monero.MoneroWallet.restoreWalletFromSeed( - path: path, - password: password, - seed: mnemonic, - restoreHeight: height, - ); + }) async => await lib_monero.MoneroWallet.restoreWalletFromSeed( + path: path, + password: password, + seed: mnemonic, + restoreHeight: height, + seedOffset: seedOffset, + ); @override Future getRestoredFromViewKeyWallet({ @@ -119,14 +117,13 @@ class MoneroWallet extends LibMoneroWallet { required String address, required String privateViewKey, int height = 0, - }) async => - lib_monero.MoneroWallet.createViewOnlyWallet( - path: path, - password: password, - address: address, - viewKey: privateViewKey, - restoreHeight: height, - ); + }) async => lib_monero.MoneroWallet.createViewOnlyWallet( + path: path, + password: password, + address: address, + viewKey: privateViewKey, + restoreHeight: height, + ); @override void invalidSeedLengthCheck(int length) { diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index bf6f43e41..abf598c6f 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -5,11 +5,11 @@ import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; import 'package:namecoin/namecoin.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; @@ -200,17 +200,21 @@ class NamecoinWallet } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } // TODO: Check if this is the correct formula for namecoin. @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -574,8 +578,10 @@ class NamecoinWallet noteName += ".bit"; } + final receivingAddress = (await getCurrentReceivingAddress())!; + TxData txData = TxData( - utxos: {utxo}, + utxos: {StandardInput(utxo)}, opNameState: NameOpState( name: data.name, saltHex: data.salt, @@ -589,13 +595,14 @@ class NamecoinWallet note: "Purchase $noteName", feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( - address: (await getCurrentReceivingAddress())!.value, + TxRecipient( + address: receivingAddress.value, isChange: false, amount: Amount( rawValue: BigInt.from(kNameAmountSats), fractionDigits: cryptoCurrency.fractionDigits, ), + addressType: receivingAddress.type, ), ], ); @@ -623,7 +630,7 @@ class NamecoinWallet /// Builds and signs a transaction Future _createNameTx({ required TxData txData, - required List utxoSigningData, + required List inputsWithKeys, required bool isForFeeCalcPurposesOnly, }) async { Logging.instance.d("Starting _createNameTx ----------"); @@ -663,19 +670,19 @@ class NamecoinWallet : 0xffffffff - 1; // Add transaction inputs - for (int i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (int i = 0; i < inputsWithKeys.length; i++) { + final txid = inputsWithKeys[i].utxo.txid; final hash = Uint8List.fromList( txid.toUint8ListFromHex.reversed.toList(), ); - final prevOutpoint = coinlib.OutPoint(hash, utxoSigningData[i].utxo.vout); + final prevOutpoint = coinlib.OutPoint(hash, inputsWithKeys[i].utxo.vout); final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), + BigInt.from(inputsWithKeys[i].utxo.value), coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, + inputsWithKeys[i].utxo.address!, cryptoCurrency.networkParams, ), ); @@ -684,11 +691,11 @@ class NamecoinWallet final coinlib.Input input; - switch (utxoSigningData[i].derivePathType) { + switch (inputsWithKeys[i].derivePathType) { case DerivePathType.bip44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: sequence, ); @@ -706,7 +713,7 @@ class NamecoinWallet case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: sequence, ); @@ -715,7 +722,7 @@ class NamecoinWallet default: throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + "Unknown derivation path type found: ${inputsWithKeys[i].derivePathType}", ); } @@ -727,14 +734,14 @@ class NamecoinWallet scriptSigAsm: null, sequence: sequence, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: inputsWithKeys[i].utxo.txid, + vout: inputsWithKeys[i].utxo.vout, ), addresses: - utxoSigningData[i].utxo.address == null + inputsWithKeys[i].utxo.address == null ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + : [inputsWithKeys[i].utxo.address!], + valueStringSats: inputsWithKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -804,13 +811,13 @@ class NamecoinWallet try { // Sign the transaction accordingly - for (int i = 0; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; + for (int i = 0; i < inputsWithKeys.length; i++) { + final value = BigInt.from(inputsWithKeys[i].utxo.value); + final key = inputsWithKeys[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, + internalKey: inputsWithKeys[i].key!.publicKey, ); clTx = clTx.signTaproot( @@ -897,8 +904,8 @@ class NamecoinWallet if (customSatsPerVByte != null) { final result = await coinSelectionName( - txData: txData.copyWith(feeRateAmount: -1), - utxos: utxos?.toList(), + txData: txData.copyWith(feeRateAmount: BigInt.from(-1)), + utxos: utxos?.whereType().map((e) => e.utxo).toList(), coinControl: coinControl, ); @@ -911,10 +918,10 @@ class NamecoinWallet } return result; - } else if (feeRateType is FeeRateType || feeRateAmount is int) { - late final int rate; + } else if (feeRateType is FeeRateType || feeRateAmount is BigInt) { + late final BigInt rate; if (feeRateType is FeeRateType) { - int fee = 0; + BigInt fee = BigInt.zero; final feeObject = await fees; switch (feeRateType) { case FeeRateType.fast: @@ -931,12 +938,12 @@ class NamecoinWallet } rate = fee; } else { - rate = feeRateAmount as int; + rate = feeRateAmount!; } final result = await coinSelectionName( txData: txData.copyWith(feeRateAmount: rate), - utxos: utxos?.toList(), + utxos: utxos?.whereType().map((e) => e.utxo).toList(), coinControl: coinControl, ); @@ -1111,13 +1118,16 @@ class NamecoinWallet final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final inputsWithKeys = + (await addSigningKeys( + utxoObjectsToUse.map((e) => StandardInput(e)).toList(), + )).whereType().toList(); final int vSizeForOneOutput; try { vSizeForOneOutput = (await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: true, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1138,7 +1148,7 @@ class NamecoinWallet try { vSizeForTwoOutPuts = (await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: true, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1190,7 +1200,7 @@ class NamecoinWallet ); final txnData = await _createNameTx( isForFeeCalcPurposesOnly: false, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, @@ -1203,7 +1213,7 @@ class NamecoinWallet rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } @@ -1252,7 +1262,7 @@ class NamecoinWallet ); TxData txnData = await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: false, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1278,7 +1288,7 @@ class NamecoinWallet ); txnData = await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: false, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1294,7 +1304,7 @@ class NamecoinWallet rawValue: feeBeingPaid, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 80db6e2ef..4725110af 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -3,12 +3,12 @@ import 'dart:typed_data'; import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:isar/isar.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/extensions/impl/uint8_list.dart'; @@ -45,29 +45,26 @@ class ParticlWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } -// =========================================================================== + // =========================================================================== @override - Future< - ({ - bool blocked, - String? blockedReason, - String? utxoLabel, - })> checkBlockUTXO( + Future<({bool blocked, String? blockedReason, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -98,8 +95,9 @@ class ParticlWallet utxoLabel = "Unsupported output type."; } else if (output['scriptPubKey'] != null) { if (output['scriptPubKey']?['asm'] is String && - (output['scriptPubKey']['asm'] as String) - .contains("OP_ISCOINSTAKE")) { + (output['scriptPubKey']['asm'] as String).contains( + "OP_ISCOINSTAKE", + )) { blocked = true; blockedReason = "Spending staking"; utxoLabel = "Unsupported output type."; @@ -111,21 +109,25 @@ class ParticlWallet return ( blocked: blocked, blockedReason: blockedReason, - utxoLabel: utxoLabel + utxoLabel: utxoLabel, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -138,30 +140,34 @@ class ParticlWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; // Fetch history from ElectrumX. - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -174,8 +180,9 @@ class ParticlWallet ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -325,7 +332,8 @@ class ParticlWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -344,8 +352,10 @@ class ParticlWallet @override Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required List inputsWithKeys, }) async { + final insAndKeys = inputsWithKeys.cast(); + Logging.instance.d("Starting Particl buildTransaction ----------"); // TODO: use coinlib (For this we need coinlib to support particl) @@ -363,41 +373,40 @@ class ParticlWallet ); final List<({Uint8List? output, Uint8List? redeem})> extraData = []; - for (int i = 0; i < utxoSigningData.length; i++) { - final sd = utxoSigningData[i]; + for (int i = 0; i < insAndKeys.length; i++) { + final sd = insAndKeys[i]; - final pubKey = sd.keyPair!.publicKey.data; + final pubKey = sd.key!.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; + 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; + 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; + data = + bitcoindart + .P2SH( + data: bitcoindart.PaymentData(redeem: p2wpkh), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip84: @@ -405,14 +414,13 @@ class ParticlWallet // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), // publicKey: keys.publicKey, // ); - data = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; + data = + bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip86: @@ -432,9 +440,7 @@ class ParticlWallet extraData.add((output: output, redeem: redeem)); } - final txb = bitcoindart.TransactionBuilder( - network: convertedNetwork, - ); + final txb = bitcoindart.TransactionBuilder(network: convertedNetwork); const version = 160; // buildTransaction overridden for Particl to set this. // TODO: [prio=low] refactor overridden buildTransaction to use eg. cryptocurrency.networkParams.txVersion. txb.setVersion(version); @@ -444,11 +450,11 @@ class ParticlWallet final List tempOutputs = []; // Add inputs. - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < insAndKeys.length; i++) { + final txid = insAndKeys[i].utxo.txid; txb.addInput( txid, - utxoSigningData[i].utxo.vout, + insAndKeys[i].utxo.vout, null, extraData[i].output!, cryptoCurrency.networkParams.bech32Hrp, @@ -460,13 +466,14 @@ class ParticlWallet scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: insAndKeys[i].utxo.txid, + vout: insAndKeys[i].utxo.vout, ), - addresses: utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + addresses: + insAndKeys[i].utxo.address == null + ? [] + : [insAndKeys[i].utxo.address!], + valueStringSats: insAndKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -487,10 +494,9 @@ class ParticlWallet OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", valueStringSats: txData.recipients![i].amount.raw.toString(), - addresses: [ - txData.recipients![i].address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [txData.recipients![i].address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -504,22 +510,25 @@ class ParticlWallet // Sign. try { - for (var i = 0; i < utxoSigningData.length; i++) { + for (var i = 0; i < insAndKeys.length; i++) { txb.sign( vin: i, keyPair: bitcoindart.ECPair.fromPrivateKey( - utxoSigningData[i].keyPair!.privateKey.data, + insAndKeys[i].key!.privateKey!.data, network: convertedNetwork, - compressed: utxoSigningData[i].keyPair!.privateKey.compressed, + compressed: insAndKeys[i].key!.privateKey!.compressed, ), - witnessValue: utxoSigningData[i].utxo.value, + witnessValue: insAndKeys[i].utxo.value, redeemScript: extraData[i].redeem, overridePrefix: cryptoCurrency.networkParams.bech32Hrp, ); } } catch (e, s) { - Logging.instance.e("Caught exception while signing transaction: ", - error: e, stackTrace: s); + Logging.instance.e( + "Caught exception while signing transaction: ", + error: e, + stackTrace: s, + ); rethrow; } diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart index 153131415..09c69e2cc 100644 --- a/lib/wallets/wallet/impl/peercoin_wallet.dart +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -54,13 +54,13 @@ class PeercoinWallet // =========================================================================== @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate(int inputCount, int outputCount, BigInt feeRatePerKB) { // TODO: actually do this properly for peercoin // this is probably wrong for peercoin return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -68,8 +68,8 @@ class PeercoinWallet /// we can just pretend vSize is size for peercoin @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } // =========================================================================== diff --git a/lib/wallets/wallet/impl/salvium_wallet.dart b/lib/wallets/wallet/impl/salvium_wallet.dart new file mode 100644 index 000000000..f340776be --- /dev/null +++ b/lib/wallets/wallet/impl/salvium_wallet.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:compat/compat.dart' as lib_monero_compat; +import 'package:cs_salvium/cs_salvium.dart' as lib_salvium; + +import '../../../utilities/amount/amount.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../intermediate/lib_salvium_wallet.dart'; + +class SalviumWallet extends LibSalviumWallet { + SalviumWallet(CryptoCurrencyNetwork network) + : super(Salvium(network)); + + @override + Future estimateFeeFor(Amount amount, BigInt feeRate) async { + if (libSalviumWallet == null /*|| + syncStatus is! lib_monero_compat.SyncedSyncStatus*/) { + return Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits); + } + + lib_salvium.TransactionPriority priority; + switch (feeRate.toInt()) { + case 1: + priority = lib_salvium.TransactionPriority.low; + break; + case 2: + priority = lib_salvium.TransactionPriority.medium; + break; + case 3: + priority = lib_salvium.TransactionPriority.high; + break; + case 4: + priority = lib_salvium.TransactionPriority.last; + break; + case 0: + default: + priority = lib_salvium.TransactionPriority.normal; + break; + } + + int approximateFee = 0; + await estimateFeeMutex.protect(() async { + approximateFee = await libSalviumWallet!.estimateFee( + priority, + amount.raw.toInt(), + ); + }); + + return Amount( + rawValue: BigInt.from(approximateFee), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + bool walletExists(String path) => lib_salvium.SalviumWallet.isWalletExist(path); + + @override + Future loadWallet({ + required String path, + required String password, + }) async { + libSalviumWallet = await lib_salvium.SalviumWallet.loadWallet( + path: path, + password: password, + ); + } + + @override + Future getCreatedWallet({ + required String path, + required String password, + required int wordCount, + required String seedOffset, + }) async { + final lib_salvium.SalviumSeedType type; + switch (wordCount) { + case 25: + type = lib_salvium.SalviumSeedType.twentyFive; + break; + + default: + throw Exception("Invalid mnemonic word count: $wordCount"); + } + + return await lib_salvium.SalviumWallet.create( + path: path, + password: password, + seedType: type, + seedOffset: seedOffset, + ); + } + + @override + Future getRestoredWallet({ + required String path, + required String password, + required String mnemonic, + required String seedOffset, + int height = 0, + }) async => await lib_salvium.SalviumWallet.restoreWalletFromSeed( + path: path, + password: password, + seed: mnemonic, + restoreHeight: height, + seedOffset: seedOffset, + ); + + @override + Future getRestoredFromViewKeyWallet({ + required String path, + required String password, + required String address, + required String privateViewKey, + int height = 0, + }) async => lib_salvium.SalviumWallet.createViewOnlyWallet( + path: path, + password: password, + address: address, + viewKey: privateViewKey, + restoreHeight: height, + ); + + @override + void invalidSeedLengthCheck(int length) { + if (length != 25 && length != 16) { + throw Exception("Invalid salvium mnemonic length found: $length"); + } + } +} diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 8ae0db348..5a5ef7c57 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -55,13 +55,13 @@ class SolanaWallet extends Bip39Wallet { return addressStruct; } - Future _getCurrentBalanceInLamports() async { + Future _getCurrentBalanceInLamports() async { _checkClient(); final balance = await _rpcClient?.getBalance((await _getKeyPair()).address); - return balance!.value; + return BigInt.from(balance!.value); } - Future _getEstimatedNetworkFee(Amount transferAmount) async { + Future _getEstimatedNetworkFee(Amount transferAmount) async { _checkClient(); final latestBlockhash = await _rpcClient?.getLatestBlockhash(); final pubKey = (await _getKeyPair()).publicKey; @@ -79,9 +79,13 @@ class SolanaWallet extends Bip39Wallet { feePayer: pubKey, ); - return await _rpcClient?.getFeeForMessage( + final estimate = await _rpcClient?.getFeeForMessage( base64Encode(compiledMessage.toByteArray().toList()), ); + + if (estimate == null) return null; + + return BigInt.from(estimate); } @override @@ -99,7 +103,11 @@ class SolanaWallet extends Bip39Wallet { await mainDB.updateOrPutAddresses([address]); } } catch (e, s) { - Logging.instance.e("$runtimeType checkSaveInitialReceivingAddress() failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType checkSaveInitialReceivingAddress() failed: ", + error: e, + stackTrace: s, + ); } } @@ -133,28 +141,33 @@ class SolanaWallet extends Bip39Wallet { throw Exception("Account does not appear to exist"); } - final int minimumRent = - await _rpcClient!.getMinimumBalanceForRentExemption( - accInfo.value!.data.toString().length, + final BigInt minimumRent = BigInt.from( + await _rpcClient!.getMinimumBalanceForRentExemption( + accInfo.value!.data.toString().length, + ), ); if (minimumRent > ((await _getCurrentBalanceInLamports()) - - txData.amount!.raw.toInt() - + txData.amount!.raw - feeAmount)) { throw Exception( "Insufficient remaining balance for rent exemption, minimum rent: " - "${minimumRent / pow(10, cryptoCurrency.fractionDigits)}", + "${minimumRent.toInt() / pow(10, cryptoCurrency.fractionDigits)}", ); } return txData.copyWith( fee: Amount( - rawValue: BigInt.from(feeAmount), + rawValue: feeAmount, fractionDigits: cryptoCurrency.fractionDigits, ), ); } catch (e, s) { - Logging.instance.e("$runtimeType Solana prepareSend failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType Solana prepareSend failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -166,8 +179,9 @@ class SolanaWallet extends Bip39Wallet { final keyPair = await _getKeyPair(); final recipientAccount = txData.recipients!.first; - final recipientPubKey = - Ed25519HDPublicKey.fromBase58(recipientAccount.address); + final recipientPubKey = Ed25519HDPublicKey.fromBase58( + recipientAccount.address, + ); final message = Message( instructions: [ SystemInstruction.transfer( @@ -187,17 +201,19 @@ class SolanaWallet extends Bip39Wallet { ); final txid = await _rpcClient?.signAndSendTransaction(message, [keyPair]); - return txData.copyWith( - txid: txid, - ); + return txData.copyWith(txid: txid); } catch (e, s) { - Logging.instance.e("$runtimeType Solana confirmSend failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType Solana confirmSend failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { _checkClient(); if (info.cachedBalance.spendable.raw == BigInt.zero) { @@ -212,10 +228,7 @@ class SolanaWallet extends Bip39Wallet { throw Exception("Failed to get fees, please check your node connection."); } - return Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ); + return Amount(rawValue: fee, fractionDigits: cryptoCurrency.fractionDigits); } @override @@ -250,7 +263,9 @@ class SolanaWallet extends Bip39Wallet { health = await _rpcClient?.getHealth(); return health != null; } catch (e, s) { - Logging.instance.e("$runtimeType Solana pingCheck failed \"health response=$health\": $e\n$s"); + Logging.instance.e( + "$runtimeType Solana pingCheck failed \"health response=$health\": $e\n$s", + ); return Future.value(false); } } @@ -295,10 +310,10 @@ class SolanaWallet extends Bip39Wallet { throw Exception("Account does not appear to exist"); } - final int minimumRent = - await _rpcClient!.getMinimumBalanceForRentExemption( - accInfo.value!.data.toString().length, - ); + final int minimumRent = await _rpcClient! + .getMinimumBalanceForRentExemption( + accInfo.value!.data.toString().length, + ); final spendableBalance = balance!.value - minimumRent; final newBalance = Balance( @@ -322,7 +337,11 @@ class SolanaWallet extends Bip39Wallet { await info.updateBalance(newBalance: newBalance, isar: mainDB.isar); } catch (e, s) { - Logging.instance.e("Error getting balance in solana_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error getting balance in solana_wallet.dart: ", + error: e, + stackTrace: s, + ); } } @@ -339,24 +358,30 @@ class SolanaWallet extends Bip39Wallet { isar: mainDB.isar, ); } catch (e, s) { - Logging.instance.e("Error occurred in solana_wallet.dart while getting" - " chain height for solana: $e\n$s"); + Logging.instance.e( + "Error occurred in solana_wallet.dart while getting" + " chain height for solana: $e\n$s", + ); } } @override Future updateNode() async { - _solNode = NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? - info.coin.defaultNode; + _solNode = + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? + info.coin.defaultNode(isPrimary: true); await refresh(); } @override NodeModel getCurrentNode() { - _solNode ??= NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? - info.coin.defaultNode; + _solNode ??= + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? + info.coin.defaultNode(isPrimary: true); return _solNode!; } @@ -370,8 +395,9 @@ class SolanaWallet extends Bip39Wallet { (await _getKeyPair()).publicKey, encoding: Encoding.jsonParsed, ); - final txsList = - List>.empty(growable: true); + final txsList = List>.empty( + growable: true, + ); final myAddress = (await getCurrentReceivingAddress())!; @@ -384,8 +410,9 @@ class SolanaWallet extends Bip39Wallet { (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; var txType = isar.TransactionType.unknown; final txAmount = Amount( - rawValue: - BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]), + rawValue: BigInt.from( + tx.meta!.postBalances[1] - tx.meta!.preBalances[1], + ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -429,9 +456,10 @@ class SolanaWallet extends Bip39Wallet { derivationIndex: 0, derivationPath: DerivationPath()..value = _addressDerivationPath, 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)); @@ -440,8 +468,10 @@ class SolanaWallet extends Bip39Wallet { } on NodeTorMismatchConfigException { rethrow; } catch (e, s) { - Logging.instance.e("Error occurred in solana_wallet.dart while getting" - " transactions for solana: $e\n$s"); + Logging.instance.e( + "Error occurred in solana_wallet.dart while getting" + " transactions for solana: $e\n$s", + ); } } diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart index 811858546..91af9758f 100644 --- a/lib/wallets/wallet/impl/stellar_wallet.dart +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -33,34 +33,34 @@ class StellarWallet extends Bip39Wallet { final bus = GlobalEventBus.instance; // 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; + _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; - } - }, - ); + 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; - }, - ); + _torPreferenceListener = bus.on().listen(( + event, + ) async { + _stellarSdk?.httpClient.close(); + _stellarSdk = null; + }); } void _hackedCheck() { @@ -116,12 +116,10 @@ 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(); - }, - ); + final _ = Finalizer((p0) { + p0._torPreferenceListener?.cancel(); + p0._torStatusListener?.cancel(); + }); StreamSubscription? _torStatusListener; StreamSubscription? _torPreferenceListener; @@ -131,9 +129,9 @@ class StellarWallet extends Bip39Wallet { stellar.StellarSDK? _stellarSdk; - Future _getBaseFee() async { + Future _getBaseFee() async { final fees = await (await stellarSdk).feeStats.execute(); - return int.parse(fees.lastLedgerBaseFee); + return BigInt.parse(fees.lastLedgerBaseFee); } stellar.StellarSDK _getFreshSdk() { @@ -145,15 +143,9 @@ class StellarWallet extends Bip39Wallet { TorService.sharedInstance.getProxyInfo(); _httpClient = HttpClient(); - SocksTCPClient.assignToHttpClient( - _httpClient, - [ - ProxySettings( - proxyInfo.host, - proxyInfo.port, - ), - ], - ); + SocksTCPClient.assignToHttpClient(_httpClient, [ + ProxySettings(proxyInfo.host, proxyInfo.port), + ]); } return stellar.StellarSDK( @@ -166,16 +158,18 @@ class StellarWallet extends Bip39Wallet { bool exists = false; try { - final receiverAccount = - await (await stellarSdk).accounts.account(accountId); + final receiverAccount = await (await stellarSdk).accounts.account( + accountId, + ); if (receiverAccount.accountId != "") { exists = true; } } catch (e, s) { Logging.instance.e( - "Error getting account ${e.toString()} - ${s.toString()}", - error: e, - stackTrace: s); + "Error getting account ${e.toString()} - ${s.toString()}", + error: e, + stackTrace: s, + ); } return exists; } @@ -225,15 +219,17 @@ class StellarWallet extends Bip39Wallet { try { final address = await getCurrentReceivingAddress(); if (address == null) { - await mainDB - .updateOrPutAddresses([await _fetchStellarAddress(index: 0)]); + await mainDB.updateOrPutAddresses([ + await _fetchStellarAddress(index: 0), + ]); } } catch (e, s) { // do nothing, still allow user into wallet Logging.instance.e( - "$runtimeType checkSaveInitialReceivingAddress() failed: ", - error: e, - stackTrace: s); + "$runtimeType checkSaveInitialReceivingAddress() failed: ", + error: e, + stackTrace: s, + ); } } @@ -245,7 +241,7 @@ class StellarWallet extends Bip39Wallet { } final feeRate = txData.feeRateType; - var fee = 1000; + BigInt fee = BigInt.from(1000); if (feeRate is FeeRateType) { final theFees = await fees; switch (feeRate) { @@ -261,13 +257,16 @@ class StellarWallet extends Bip39Wallet { return txData.copyWith( fee: Amount( - rawValue: BigInt.from(fee), + rawValue: fee, fractionDigits: cryptoCurrency.fractionDigits, ), ); } catch (e, s) { - Logging.instance - .e("$runtimeType prepareSend() failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType prepareSend() failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -275,8 +274,9 @@ class StellarWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { final senderKeyPair = await _getSenderKeyPair(index: 0); - final sender = - await (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; @@ -293,9 +293,9 @@ class StellarWallet extends Bip39Wallet { address, amountToSend.decimal.toString(), ); - transactionBuilder = stellar.TransactionBuilder(sender).addOperation( - createAccBuilder.build(), - ); + transactionBuilder = stellar.TransactionBuilder( + sender, + ).addOperation(createAccBuilder.build()); } else { transactionBuilder = stellar.TransactionBuilder(sender).addOperation( stellar.PaymentOperationBuilder( @@ -316,14 +316,13 @@ class StellarWallet extends Bip39Wallet { try { final response = await (await stellarSdk).submitTransaction(transaction); if (!response.success) { - throw Exception("${response.extras?.resultCodes?.transactionResultCode}" - " ::: ${response.extras?.resultCodes?.operationsResultCodes}"); + throw Exception( + "${response.extras?.resultCodes?.transactionResultCode}" + " ::: ${response.extras?.resultCodes?.operationsResultCodes}", + ); } - return txData.copyWith( - txHash: response.hash!, - txid: response.hash!, - ); + return txData.copyWith(txHash: response.hash!, txid: response.hash!); } catch (e, s) { Logging.instance.e("Error sending TX $e - $s", error: e, stackTrace: s); rethrow; @@ -331,17 +330,17 @@ class StellarWallet extends Bip39Wallet { } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { final baseFee = await _getBaseFee(); return Amount( - rawValue: BigInt.from(baseFee), + rawValue: baseFee, fractionDigits: cryptoCurrency.fractionDigits, ); } @override Future get fees async { - final int fee = await _getBaseFee(); + final fee = await _getBaseFee(); return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 1, @@ -379,16 +378,17 @@ class StellarWallet extends Bip39Wallet { stellar.AccountResponse accountResponse; try { - accountResponse = await (await stellarSdk) - .accounts + accountResponse = await (await stellarSdk).accounts .account((await getCurrentReceivingAddress())!.value) .onError((error, stackTrace) => throw error!); } catch (e) { if (e is stellar.ErrorResponse && - e.body.contains("The resource at the url requested was not found. " - "This usually occurs for one of two reasons: " - "The url requested is not valid, or no data in our database " - "could be found with the parameters provided.")) { + e.body.contains( + "The resource at the url requested was not found. " + "This usually occurs for one of two reasons: " + "The url requested is not valid, or no data in our database " + "could be found with the parameters provided.", + )) { // probably just doesn't have any history yet or whatever stellar needs return; } else { @@ -438,16 +438,18 @@ class StellarWallet extends Bip39Wallet { @override Future updateChainHeight() async { try { - final height = await (await stellarSdk) - .ledgers + final height = await (await stellarSdk).ledgers .order(stellar.RequestBuilderOrder.DESC) .limit(1) .execute() .then((value) => value.records!.first.sequence); await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } catch (e, s) { - Logging.instance.e("$runtimeType updateChainHeight() failed: ", - error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType updateChainHeight() failed: ", + error: e, + stackTrace: s, + ); rethrow; } @@ -467,17 +469,19 @@ class StellarWallet extends Bip39Wallet { final List transactionList = []; stellar.Page payments; try { - payments = await (await stellarSdk) - .payments - .forAccount(myAddress.value) - .order(stellar.RequestBuilderOrder.DESC) - .execute(); + payments = + await (await stellarSdk).payments + .forAccount(myAddress.value) + .order(stellar.RequestBuilderOrder.DESC) + .execute(); } catch (e) { if (e is stellar.ErrorResponse && - e.body.contains("The resource at the url requested was not found. " - "This usually occurs for one of two reasons: " - "The url requested is not valid, or no data in our database " - "could be found with the parameters provided.")) { + e.body.contains( + "The resource at the url requested was not found. " + "This usually occurs for one of two reasons: " + "The url requested is not valid, or no data in our database " + "could be found with the parameters provided.", + )) { // probably just doesn't have any history yet or whatever stellar needs return; } else { @@ -521,13 +525,11 @@ class StellarWallet extends Bip39Wallet { final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( - scriptPubKeyHex: "00", - valueStringSats: amount.raw.toString(), - addresses: [ - addressTo, - ], - walletOwns: addressTo == myAddress.value, - ); + scriptPubKeyHex: "00", + valueStringSats: amount.raw.toString(), + addresses: [addressTo], + walletOwns: addressTo == myAddress.value, + ); final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, @@ -558,10 +560,11 @@ class StellarWallet extends Bip39Wallet { } final otherData = { - "overrideFee": Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + "overrideFee": + Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }; final theTransaction = TransactionV2( @@ -603,8 +606,8 @@ class StellarWallet extends Bip39Wallet { final List outputs = []; final List inputs = []; - final OutputV2 output = - OutputV2.isarCantDoRequiredInDefaultConstructor( + final OutputV2 + output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), addresses: [ @@ -634,19 +637,20 @@ class StellarWallet extends Bip39Wallet { int fee = 0; int height = 0; - final tx = await (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; } final otherData = { - "overrideFee": Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + "overrideFee": + Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }; final theTransaction = TransactionV2( @@ -671,8 +675,11 @@ class StellarWallet extends Bip39Wallet { await mainDB.updateOrPutTransactionV2s(transactionList); } catch (e, s) { - Logging.instance.e("Exception rethrown from updateTransactions(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception rethrown from updateTransactions(): ", + error: e, + stackTrace: s, + ); rethrow; } } 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 845b0282b..d32ae3db8 100644 --- a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart +++ b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart @@ -5,7 +5,6 @@ import 'package:isar/isar.dart'; import 'package:web3dart/web3dart.dart' as web3dart; import '../../../../dto/ethereum/eth_token_tx_dto.dart'; -import '../../../../dto/ethereum/eth_token_tx_extra_dto.dart'; import '../../../../models/balance.dart'; import '../../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../../models/isar/models/blockchain_data/v2/input_v2.dart'; @@ -15,7 +14,6 @@ import '../../../../models/isar/models/ethereum/eth_contract.dart'; import '../../../../models/paymint/fee_object_model.dart'; import '../../../../services/ethereum/ethereum_api.dart'; import '../../../../utilities/amount/amount.dart'; -import '../../../../utilities/enums/fee_rate_type_enum.dart'; import '../../../../utilities/eth_commons.dart'; import '../../../../utilities/extensions/extensions.dart'; import '../../../../utilities/logger.dart'; @@ -29,7 +27,7 @@ class EthTokenWallet extends Wallet { int get isarTransactionVersion => 2; EthTokenWallet(this.ethWallet, this._tokenContract) - : super(ethWallet.cryptoCurrency); + : super(ethWallet.cryptoCurrency); final EthereumWallet ethWallet; @@ -39,8 +37,6 @@ class EthTokenWallet extends Wallet { late web3dart.DeployedContract _deployedContract; late web3dart.ContractFunction _sendFunction; - static const _gasLimit = 65000; - // =========================================================================== // =========================================================================== @@ -85,9 +81,7 @@ class EthTokenWallet extends Wallet { final output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), - addresses: [ - addressTo, - ], + addresses: [addressTo], walletOwns: addressTo == myAddress, ); final input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -116,16 +110,15 @@ class EthTokenWallet extends Wallet { inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: -1, - type: addressTo == myAddress - ? TransactionType.sentToSelf - : TransactionType.outgoing, + type: + addressTo == myAddress + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.ethToken, otherData: jsonEncode(otherData), ); - return txData.copyWith( - tempTx: tempTx, - ); + return txData.copyWith(tempTx: tempTx); } // =========================================================================== @@ -143,8 +136,9 @@ class EthTokenWallet extends Wallet { try { await super.init(); - final contractAddress = - web3dart.EthereumAddress.fromHex(tokenContract.address); + final contractAddress = web3dart.EthereumAddress.fromHex( + tokenContract.address, + ); // first try to update the abi regardless just in case something has changed try { @@ -153,8 +147,11 @@ class EthTokenWallet extends Wallet { usingContractAddress: contractAddress.hex, ); } catch (e, s) { - Logging.instance - .w("$runtimeType _updateTokenABI(): ", error: e, stackTrace: s); + Logging.instance.w( + "$runtimeType _updateTokenABI(): ", + error: e, + stackTrace: s, + ); } try { @@ -176,8 +173,8 @@ class EthTokenWallet extends Wallet { // Some failure, try for proxy contract final contractAddressResponse = await EthereumAPI.getProxyTokenImplementationAddress( - contractAddress.hex, - ); + contractAddress.hex, + ); if (contractAddressResponse.value != null) { _tokenContract = await _updateTokenABI( @@ -198,55 +195,36 @@ class EthTokenWallet extends Wallet { _sendFunction = _deployedContract.function('transfer'); } catch (e, s) { - Logging.instance - .w("$runtimeType wallet failed init(): ", error: e, stackTrace: s); + Logging.instance.w( + "$runtimeType wallet failed init(): ", + error: e, + stackTrace: s, + ); } } @override Future prepareSend({required TxData txData}) async { - final feeRateType = txData.feeRateType!; - int fee = 0; - final feeObject = await fees; - switch (feeRateType) { - case FeeRateType.fast: - fee = feeObject.fast; - break; - case FeeRateType.average: - fee = feeObject.medium; - break; - case FeeRateType.slow: - fee = feeObject.slow; - break; - case FeeRateType.custom: - throw UnimplementedError("custom eth token fees"); - } - - final feeEstimate = await estimateFeeFor(Amount.zero, fee); - - final client = ethWallet.getEthClient(); - - final myAddress = (await getCurrentReceivingAddress())!.value; - final myWeb3Address = web3dart.EthereumAddress.fromHex(myAddress); - - final nonce = txData.nonce ?? - await client.getTransactionCount( - myWeb3Address, - atBlock: const web3dart.BlockNum.pending(), - ); - final amount = txData.recipients!.first.amount; final address = txData.recipients!.first.address; - await updateBalance(); - final info = await mainDB.isar.tokenWalletInfo - .where() - .walletIdTokenAddressEqualTo(walletId, tokenContract.address) - .findFirst(); - final availableBalance = info?.getCachedBalance().spendable ?? - Amount.zeroWith( - fractionDigits: tokenContract.decimals, - ); + final myWeb3Address = await ethWallet.getMyWeb3Address(); + + final prep = await ethWallet.internalSharedPrepareSend( + txData: txData, + myWeb3Address: myWeb3Address, + ); + + // double check balance after internalSharedPrepareSend call to ensure + // balance is up to date + final info = + await mainDB.isar.tokenWalletInfo + .where() + .walletIdTokenAddressEqualTo(walletId, tokenContract.address) + .findFirst(); + final availableBalance = + info?.getCachedBalance().spendable ?? + Amount.zeroWith(fractionDigits: tokenContract.decimals); if (amount > availableBalance) { throw Exception("Insufficient balance"); } @@ -255,19 +233,26 @@ class EthTokenWallet extends Wallet { contract: _deployedContract, function: _sendFunction, parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw], - maxGas: _gasLimit, - gasPrice: web3dart.EtherAmount.fromUnitAndValue( + maxGas: txData.ethEIP1559Fee?.gasLimit ?? kEthereumTokenMinGasLimit, + nonce: prep.nonce, + maxFeePerGas: web3dart.EtherAmount.fromBigInt( web3dart.EtherUnit.wei, - fee, + prep.maxBaseFee, + ), + maxPriorityFeePerGas: web3dart.EtherAmount.fromBigInt( + web3dart.EtherUnit.wei, + prep.priorityFee, ), - nonce: nonce, ); + final feeEstimate = await estimateFeeFor( + Amount.zero, + prep.maxBaseFee + prep.priorityFee, + ); return txData.copyWith( fee: feeEstimate, - feeInWei: BigInt.from(fee), web3dartTransaction: tx, - chainId: await client.getChainId(), + chainId: prep.chainId, nonce: tx.nonce, ); } @@ -286,16 +271,16 @@ class EthTokenWallet extends Wallet { } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { return ethWallet.estimateEthFee( feeRate, - _gasLimit, + kEthereumTokenMinGasLimit, cryptoCurrency.fractionDigits, ); } @override - Future get fees => EthereumAPI.getFees(); + Future get fees => EthereumAPI.getFees(); @override Future pingCheck() async { @@ -317,10 +302,11 @@ class EthTokenWallet extends Wallet { @override Future updateBalance() async { try { - final info = await mainDB.isar.tokenWalletInfo - .where() - .walletIdTokenAddressEqualTo(walletId, tokenContract.address) - .findFirst(); + final info = + await mainDB.isar.tokenWalletInfo + .where() + .walletIdTokenAddressEqualTo(walletId, tokenContract.address) + .findFirst(); final response = await EthereumAPI.getWalletTokenBalance( address: (await getCurrentReceivingAddress())!.value, contractAddress: tokenContract.address, @@ -364,8 +350,9 @@ class EthTokenWallet extends Wallet { @override Future updateTransactions() async { try { - final String addressString = - checksumEthereumAddress((await getCurrentReceivingAddress())!.value); + final String addressString = checksumEthereumAddress( + (await getCurrentReceivingAddress())!.value, + ); final response = await EthereumAPI.getTokenTransactions( address: addressString, @@ -374,8 +361,9 @@ class EthTokenWallet extends Wallet { if (response.value == null) { if (response.exception != null && - response.exception!.message - .contains("response is empty but status code is 200")) { + response.exception!.message.contains( + "response is empty but status code is 200", + )) { Logging.instance.d( "No ${tokenContract.name} transfers found for $addressString", ); @@ -390,50 +378,38 @@ class EthTokenWallet extends Wallet { return; } - final response2 = await EthereumAPI.getEthTokenTransactionsByTxids( - response.value!.map((e) => e.transactionHash).toSet().toList(), - ); - - if (response2.value == null) { - throw response2.exception ?? - Exception("Failed to fetch token transactions"); - } - final List<({EthTokenTxDto tx, EthTokenTxExtraDTO extra})> data = []; - for (final tokenDto in response.value!) { - try { - final txExtra = response2.value!.firstWhere( - (e) => e.hash == tokenDto.transactionHash, - ); - data.add( - ( - tx: tokenDto, - extra: txExtra, - ), - ); - } catch (e, s) { - // Server indexing failed for some reason. Instead of hard crashing or - // showing no transactions we just skip it here. Not ideal but better - // than nothing showing up - Logging.instance.e( - "Server error: Transaction hash not found.", - error: e, - stackTrace: s, - ); - Logging.instance.d( - "Server error: Transaction ${tokenDto.transactionHash} not found.", - error: e, - stackTrace: s, - ); + web3dart.Web3Client? client; + final List allTxs = []; + for (final dto in response.value!) { + if (dto.nonce == null) { + client ??= ethWallet.getEthClient(); + final txInfo = await client.getTransactionByHash(dto.transactionHash); + if (txInfo == null) { + // Something strange is happening + Logging.instance.w( + "Could not find token transaction via RPC that was found use " + "TrueBlocks API.\nOffending tx: $dto", + ); + } else { + final updated = dto.copyWith( + nonce: txInfo.nonce, + gasPrice: txInfo.gasPrice.getInWei, + gasUsed: txInfo.gas, + ); + allTxs.add(updated); + } + } else { + allTxs.add(dto); } } final List txns = []; - for (final tuple in data) { + for (final tx in allTxs) { // ignore all non Transfer events (for now) - if (tuple.tx.topics[0] == kTransferEventSignature) { + if (tx.topics[0] == kTransferEventSignature) { final amount = Amount( - rawValue: tuple.tx.data.toBigIntFromHex, + rawValue: tx.data.toBigIntFromHex, fractionDigits: tokenContract.decimals, ); @@ -442,13 +418,12 @@ class EthTokenWallet extends Wallet { continue; } - final Amount txFee = tuple.extra.gasUsed * tuple.extra.gasPrice; - final addressFrom = _addressFromTopic( - tuple.tx.topics[1], - ); - final addressTo = _addressFromTopic( - tuple.tx.topics[2], + final txFee = Amount( + rawValue: BigInt.from(tx.gasUsed!) * tx.gasPrice!, + fractionDigits: cryptoCurrency.fractionDigits, ); + final addressFrom = _addressFromTopic(tx.topics[1]); + final addressTo = _addressFromTopic(tx.topics[2]); final TransactionType txType; if (addressTo == addressString) { @@ -470,10 +445,10 @@ class EthTokenWallet extends Wallet { } final otherData = { - "nonce": tuple.extra.nonce, + "nonce": tx.nonce, "isCancelled": false, "overrideFee": txFee.toJsonString(), - "contractAddress": tuple.tx.address, + "contractAddress": tx.address, }; // hack eth tx data into inputs and outputs @@ -483,9 +458,7 @@ class EthTokenWallet extends Wallet { final output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), - addresses: [ - addressTo, - ], + addresses: [addressTo], walletOwns: addressTo == addressString, ); final input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -506,11 +479,11 @@ class EthTokenWallet extends Wallet { final txn = TransactionV2( walletId: walletId, - blockHash: tuple.extra.blockHash, - hash: tuple.tx.transactionHash, - txid: tuple.tx.transactionHash, - timestamp: tuple.extra.timestamp, - height: tuple.tx.blockNumber, + blockHash: tx.blockHash, + hash: tx.transactionHash, + txid: tx.transactionHash, + timestamp: tx.timestamp, + height: tx.blockNumber, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: -1, @@ -544,15 +517,15 @@ class EthTokenWallet extends Wallet { @override FilterOperation? get transactionFilterOperation => FilterGroup.and([ - FilterCondition.equalTo( - property: r"contractAddress", - value: tokenContract.address, - ), - const FilterCondition.equalTo( - property: r"subType", - value: TransactionSubType.ethToken, - ), - ]); + FilterCondition.equalTo( + property: r"contractAddress", + value: tokenContract.address, + ), + const FilterCondition.equalTo( + property: r"subType", + value: TransactionSubType.ethToken, + ), + ]); @override Future checkSaveInitialReceivingAddress() async { diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index 992a900ae..54302a027 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:isar/isar.dart'; @@ -115,9 +116,10 @@ class TezosWallet extends Bip39Wallet { prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null; final tezartClient = tezart.TezartClient( server, - proxy: proxyInfo != null - ? "socks5://${proxyInfo.host}:${proxyInfo.port};" - : null, + proxy: + proxyInfo != null + ? "socks5://${proxyInfo.host}:${proxyInfo.port};" + : null, ); final opList = await tezartClient.transferOperation( @@ -127,6 +129,7 @@ class TezosWallet extends Bip39Wallet { // customFee: customFee?.raw.toInt(), // customGasLimit: customGasLimit, // reveal: false, + customGasLimit: 10600, ); // if (reveal) { @@ -197,9 +200,7 @@ class TezosWallet extends Bip39Wallet { } final myAddress = (await getCurrentReceivingAddress())!; - final account = await TezosAPI.getAccount( - myAddress.value, - ); + final account = await TezosAPI.getAccount(myAddress.value); // final bool isSendAll = sendAmount == info.cachedBalance.spendable; // @@ -252,23 +253,12 @@ class TezosWallet extends Bip39Wallet { await opList.simulate(); return txData.copyWith( - recipients: [ - ( - amount: sendAmount, - address: txData.recipients!.first.address, - isChange: txData.recipients!.first.isChange, - ), - ], + recipients: [txData.recipients!.first.copyWith(amount: sendAmount)], // fee: fee, fee: Amount( rawValue: opList.operations - .map( - (e) => BigInt.from(e.fee), - ) - .fold( - BigInt.zero, - (p, e) => p + e, - ), + .map((e) => BigInt.from(e.fee)) + .fold(BigInt.zero, (p, e) => p + e), fractionDigits: cryptoCurrency.fractionDigits, ), tezosOperationsList: opList, @@ -280,14 +270,14 @@ class TezosWallet extends Bip39Wallet { stackTrace: s, ); - if (e - .toString() - .contains("(_operationResult['errors']): Must not be null")) { + if (e.toString().contains( + "(_operationResult['errors']): Must not be null", + )) { throw Exception("Probably insufficient balance"); } else if (e.toString().contains( - "The simulation of the operation: \"transaction\" failed with error(s) :" - " contract.balance_too_low, tez.subtraction_underflow.", - )) { + "The simulation of the operation: \"transaction\" failed with error(s) :" + " contract.balance_too_low, tez.subtraction_underflow.", + )) { throw Exception("Insufficient balance to pay fees"); } @@ -298,11 +288,39 @@ class TezosWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { _hackedCheckTorNodePrefs(); + + final completer = Completer(); + final sub = mainDB.isar.transactions + .where() + .walletIdEqualTo(walletId) + .watch(fireImmediately: true) + .listen((event) { + if (!completer.isCompleted && + event + .where((e) => e.txid == txData.tezosOperationsList!.result.id) + .isNotEmpty) { + completer.complete(); + } + }); + await txData.tezosOperationsList!.inject(); - await txData.tezosOperationsList!.monitor(); - return txData.copyWith( - txid: txData.tezosOperationsList!.result.id, + + final completerFuture = completer.future.timeout( + const Duration(minutes: 2), + onTimeout: () { + throw Exception("Tezos confirm send timeout"); + }, ); + + while (!completer.isCompleted) { + await updateTransactions(); + await Future.delayed(const Duration(seconds: 3)); + } + + await completerFuture; + + unawaited(sub.cancel()); + return txData.copyWith(txid: txData.tezosOperationsList!.result.id); } int _estCount = 0; @@ -350,13 +368,8 @@ class TezosWallet extends Bip39Wallet { rethrow; } else { _estCount++; - Logging.instance.e( - "_estimate() retry _estCount=$_estCount", - ); - return await _estimate( - account, - recipientAddress, - ); + Logging.instance.e("_estimate() retry _estCount=$_estCount"); + return await _estimate(account, recipientAddress); } } } @@ -364,7 +377,7 @@ class TezosWallet extends Bip39Wallet { @override Future estimateFeeFor( Amount amount, - int feeRate, { + BigInt feeRate, { String recipientAddress = "tz1MXvDCyXSqBqXPNDcsdmVZKfoxL9FTHmp2", }) async { _hackedCheckTorNodePrefs(); @@ -376,9 +389,7 @@ class TezosWallet extends Bip39Wallet { } final myAddress = (await getCurrentReceivingAddress())!; - final account = await TezosAPI.getAccount( - myAddress.value, - ); + final account = await TezosAPI.getAccount(myAddress.value); try { final fees = await _estimate(account, recipientAddress); @@ -402,7 +413,7 @@ class TezosWallet extends Bip39Wallet { /// Not really used (yet) @override Future get fees async { - const feePerTx = 1; + final feePerTx = BigInt.one; return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, @@ -418,10 +429,7 @@ class TezosWallet extends Bip39Wallet { _hackedCheckTorNodePrefs(); final currentNode = getCurrentNode(); return await TezosRpcAPI.testNetworkConnection( - nodeInfo: ( - host: currentNode.host, - port: currentNode.port, - ), + nodeInfo: (host: currentNode.host, port: currentNode.port), ); } @@ -518,16 +526,10 @@ class TezosWallet extends Bip39Wallet { _hackedCheckTorNodePrefs(); final currentNode = _xtzNode ?? getCurrentNode(); final height = await TezosRpcAPI.getChainHeight( - nodeInfo: ( - host: currentNode.host, - port: currentNode.port, - ), + nodeInfo: (host: currentNode.host, port: currentNode.port), ); - await info.updateCachedChainHeight( - newHeight: height!, - isar: mainDB.isar, - ); + await info.updateCachedChainHeight(newHeight: height!, isar: mainDB.isar); } catch (e, s) { Logging.instance.e( "Error occurred in tezos_wallet.dart while getting" @@ -540,9 +542,11 @@ class TezosWallet extends Bip39Wallet { @override Future updateNode() async { - _xtzNode = NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? - info.coin.defaultNode; + _xtzNode = + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? + info.coin.defaultNode(isPrimary: true); await refresh(); } @@ -550,9 +554,10 @@ class TezosWallet extends Bip39Wallet { @override NodeModel getCurrentNode() { return _xtzNode ??= - NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? - info.coin.defaultNode; + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? + info.coin.defaultNode(isPrimary: true); } @override @@ -590,10 +595,11 @@ class TezosWallet extends Bip39Wallet { type: txType, subType: TransactionSubType.none, amount: theTx.amountInMicroTez, - amountString: Amount( - rawValue: BigInt.from(theTx.amountInMicroTez), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + amountString: + Amount( + rawValue: BigInt.from(theTx.amountInMicroTez), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), fee: theTx.feeInMicroTez, height: theTx.height, isCancelled: false, diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index 81407f837..a8d5e1c65 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:compat/compat.dart' as lib_monero_compat; import 'package:cs_monero/cs_monero.dart' as lib_monero; +import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; import '../../crypto_currency/crypto_currency.dart'; @@ -11,23 +12,18 @@ import '../intermediate/lib_monero_wallet.dart'; class WowneroWallet extends LibMoneroWallet { WowneroWallet(CryptoCurrencyNetwork network) - : super( - Wownero(network), - lib_monero_compat.WalletType.wownero, - ); + : super(Wownero(network), lib_monero_compat.WalletType.wownero); @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { if (libMoneroWallet == null || syncStatus is! lib_monero_compat.SyncedSyncStatus) { - return Amount.zeroWith( - fractionDigits: cryptoCurrency.fractionDigits, - ); + return Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits); } lib_monero.TransactionPriority priority; FeeRateType feeRateType = FeeRateType.slow; - switch (feeRate) { + switch (feeRate.toInt()) { case 1: priority = lib_monero.TransactionPriority.low; feeRateType = FeeRateType.average; @@ -59,11 +55,12 @@ class WowneroWallet extends LibMoneroWallet { txData: TxData( recipients: [ // This address is only used for getting an approximate fee, never for sending - ( + TxRecipient( address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", amount: amount, isChange: false, + addressType: AddressType.cryptonote, ), ], feeRateType: feeRateType, @@ -112,6 +109,7 @@ class WowneroWallet extends LibMoneroWallet { required String path, required String password, required int wordCount, + required String seedOffset, }) async { final lib_monero.WowneroSeedType type; switch (wordCount) { @@ -132,6 +130,7 @@ class WowneroWallet extends LibMoneroWallet { password: password, seedType: type, overrideDeprecated14WordSeedException: true, + seedOffset: seedOffset, ); } @@ -140,14 +139,15 @@ class WowneroWallet extends LibMoneroWallet { required String path, required String password, required String mnemonic, + required String seedOffset, int height = 0, - }) async => - await lib_monero.WowneroWallet.restoreWalletFromSeed( - path: path, - password: password, - seed: mnemonic, - restoreHeight: height, - ); + }) async => await lib_monero.WowneroWallet.restoreWalletFromSeed( + path: path, + password: password, + seed: mnemonic, + restoreHeight: height, + seedOffset: seedOffset, + ); @override Future getRestoredFromViewKeyWallet({ @@ -156,14 +156,13 @@ class WowneroWallet extends LibMoneroWallet { required String address, required String privateViewKey, int height = 0, - }) async => - lib_monero.WowneroWallet.createViewOnlyWallet( - path: path, - password: password, - address: address, - viewKey: privateViewKey, - restoreHeight: height, - ); + }) async => lib_monero.WowneroWallet.createViewOnlyWallet( + path: path, + password: password, + address: address, + viewKey: privateViewKey, + restoreHeight: height, + ); @override void invalidSeedLengthCheck(int length) { diff --git a/lib/wallets/wallet/impl/xelis_wallet.dart b/lib/wallets/wallet/impl/xelis_wallet.dart index 352b076f7..eea19dbdb 100644 --- a/lib/wallets/wallet/impl/xelis_wallet.dart +++ b/lib/wallets/wallet/impl/xelis_wallet.dart @@ -416,7 +416,10 @@ class XelisWallet extends LibXelisWallet { asset: xelis_sdk.xelisAsset, ); - fee = Amount(rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); + fee = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); outputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( @@ -477,7 +480,10 @@ class XelisWallet extends LibXelisWallet { asset: transfer.asset, ); - fee = Amount(rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); + fee = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); outputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( @@ -622,9 +628,9 @@ class XelisWallet extends LibXelisWallet { numberOfBlocksFast: 10, numberOfBlocksAverage: 10, numberOfBlocksSlow: 10, - fast: 1, - medium: 1, - slow: 1, + fast: BigInt.one, + medium: BigInt.one, + slow: BigInt.one, ); } @@ -661,7 +667,7 @@ class XelisWallet extends LibXelisWallet { // Estimate fee using the shared method final boostedFee = await estimateFeeFor( totalSendAmount, - 1, + BigInt.one, feeMultiplier: 1.0, recipients: recipients, assetId: asset, @@ -701,7 +707,7 @@ class XelisWallet extends LibXelisWallet { @override Future estimateFeeFor( Amount amount, - int feeRate, { + BigInt feeRate, { double? feeMultiplier, List recipients = const [], String? assetId, @@ -719,11 +725,12 @@ class XelisWallet extends LibXelisWallet { recipients.isNotEmpty ? recipients : [ - ( + TxRecipient( address: 'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m', amount: amount, isChange: false, + addressType: AddressType.xelis, ), ]; diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index b6b30de34..d29e97976 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -11,6 +11,7 @@ import 'package:stack_wallet_backup/generate_password.dart'; import '../../../db/hive/db.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; @@ -19,6 +20,7 @@ import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/keys/cw_key_data.dart'; import '../../../models/keys/view_only_wallet_data.dart'; +import '../../../models/node_model.dart'; import '../../../models/paymint/fee_object_model.dart'; import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -51,33 +53,33 @@ abstract class LibMoneroWallet final bus = GlobalEventBus.instance; // 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; + _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; - } - }, - ); + 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 { - await updateNode(); - }, - ); + _torPreferenceListener = bus.on().listen(( + event, + ) async { + await updateNode(); + }); // Potentially dangerous hack. See comments in _startInit() _startInit(); @@ -93,17 +95,13 @@ abstract class LibMoneroWallet .walletIdEqualTo(walletId) .watch(fireImmediately: true) .listen((utxos) async { - try { - await onUTXOsChanged(utxos); - await updateBalance(shouldUpdateUtxos: false); - } catch (e, s) { - lib_monero.Logging.log?.i( - "_startInit", - error: e, - stackTrace: s, - ); - } - }); + try { + await onUTXOsChanged(utxos); + await updateBalance(shouldUpdateUtxos: false); + } catch (e, s) { + lib_monero.Logging.log?.i("_startInit", error: e, stackTrace: s); + } + }); }); } @@ -143,12 +141,14 @@ abstract class LibMoneroWallet required String path, required String password, required int wordCount, + required String seedOffset, }); Future getRestoredWallet({ required String path, required String password, required String mnemonic, + required String seedOffset, int height = 0, }); @@ -179,11 +179,7 @@ abstract class LibMoneroWallet onNewBlock: onNewBlock, onBalancesChanged: onBalancesChanged, onError: (e, s) { - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); }, ), ); @@ -197,16 +193,14 @@ abstract class LibMoneroWallet if (libMoneroWallet == null) { wasNull = true; // libMoneroWalletT?.close(); - final path = await pathForWallet( - name: walletId, - type: compatType, - ); + final path = await pathForWallet(name: walletId, type: compatType); final String password; try { - password = (await secureStorageInterface.read( - key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), - ))!; + password = + (await secureStorageInterface.read( + key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), + ))!; } catch (e, s) { throw Exception("Password not found $e, $s"); } @@ -247,9 +241,7 @@ abstract class LibMoneroWallet } @Deprecated("Only used in the case of older wallets") - lib_monero_compat.WalletInfo? getLibMoneroWalletInfo( - String walletId, - ) { + lib_monero_compat.WalletInfo? getLibMoneroWalletInfo(String walletId) { try { return DB.instance.moneroWalletInfoBox.values.firstWhere( (info) => info.id == lib_monero_compat.hiveIdFor(walletId, compatType), @@ -297,9 +289,7 @@ abstract class LibMoneroWallet Future getKeys() async { final base = libMoneroWallet; - final oldInfo = getLibMoneroWalletInfo( - walletId, - ); + final oldInfo = getLibMoneroWalletInfo(walletId); if (base == null || (oldInfo != null && oldInfo.name != walletId)) { return null; } @@ -324,13 +314,14 @@ abstract class LibMoneroWallet } Future<(String, String)> - hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing() async { + hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing() async { final path = await pathForWallet(name: walletId, type: compatType); final String password; try { - password = (await secureStorageInterface.read( - key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), - ))!; + password = + (await secureStorageInterface.read( + key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), + ))!; } catch (e, s) { throw Exception("Password not found $e, $s"); } @@ -341,10 +332,7 @@ abstract class LibMoneroWallet @override Future init({bool? isRestore, int? wordCount}) async { - final path = await pathForWallet( - name: walletId, - type: compatType, - ); + final path = await pathForWallet(name: walletId, type: compatType); if (!(walletExists(path)) && isRestore != true) { if (wordCount == null) { throw Exception("Missing word count for new xmr/wow wallet!"); @@ -359,6 +347,7 @@ abstract class LibMoneroWallet path: path, password: password, wordCount: wordCount, + seedOffset: "", // default for non restored wallets for now ); final height = wallet.getRefreshFromBlockHeight(); @@ -410,6 +399,7 @@ abstract class LibMoneroWallet await refreshMutex.protect(() async { final mnemonic = await getMnemonic(); + final seedOffset = await getMnemonicPassphrase(); final seedLength = mnemonic.trim().split(" ").length; invalidSeedLengthCheck(seedLength); @@ -424,10 +414,7 @@ abstract class LibMoneroWallet final String name = walletId; - final path = await pathForWallet( - name: name, - type: compatType, - ); + final path = await pathForWallet(name: name, type: compatType); try { final password = generatePassword(); @@ -440,6 +427,7 @@ abstract class LibMoneroWallet password: password, mnemonic: mnemonic, height: height, + seedOffset: seedOffset, ); if (libMoneroWallet != null) { @@ -450,7 +438,8 @@ abstract class LibMoneroWallet _setListener(); - final newReceivingAddress = await getCurrentReceivingAddress() ?? + final newReceivingAddress = + await getCurrentReceivingAddress() ?? Address( walletId: walletId, derivationIndex: 0, @@ -481,43 +470,43 @@ abstract class LibMoneroWallet libMoneroWallet?.startListeners(); libMoneroWallet?.startAutoSaving(); } catch (e, s) { - Logging.instance.e("Exception rethrown from recoverFromMnemonic(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception rethrown from recoverFromMnemonic(): ", + error: e, + stackTrace: s, + ); rethrow; } }); } + // dumb temporary hack + bool _canPing = false; + @override Future pingCheck() async { - return (await libMoneroWallet?.isConnectedToDaemon()) ?? false; + if (_canPing) { + return (await libMoneroWallet?.isConnectedToDaemon()) ?? false; + } else { + return false; + } } @override Future updateNode() async { final node = getCurrentNode(); + if (_torNodeMismatchGuard(node)) { + throw Exception("TOR – clearnet mismatch"); + } + final host = node.host.endsWith(".onion") ? node.host : Uri.parse(node.host).host; ({InternetAddress host, int port})? proxy; - if (prefs.useTor) { - if (node.clearnetEnabled && !node.torEnabled) { - libMoneroWallet?.stopAutoSaving(); - libMoneroWallet?.stopListeners(); - libMoneroWallet?.stopSyncing(); - _setSyncStatus(lib_monero_compat.FailedSyncStatus()); - throw Exception("TOR - clearnet mismatch"); - } - proxy = TorService.sharedInstance.getProxyInfo(); - } else { - if (!node.clearnetEnabled && node.torEnabled) { - libMoneroWallet?.stopAutoSaving(); - libMoneroWallet?.stopListeners(); - libMoneroWallet?.stopSyncing(); - _setSyncStatus(lib_monero_compat.FailedSyncStatus()); - throw Exception("TOR - clearnet mismatch"); - } - } + proxy = + prefs.useTor && !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); try { @@ -530,7 +519,11 @@ abstract class LibMoneroWallet trusted: node.trusted ?? false, useSSL: node.useSSL, socksProxyAddress: - proxy == null ? null : "${proxy.host.address}:${proxy.port}", + node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", ); }); } else { @@ -541,7 +534,11 @@ abstract class LibMoneroWallet trusted: node.trusted ?? false, useSSL: node.useSSL, socksProxyAddress: - proxy == null ? null : "${proxy.host.address}:${proxy.port}", + node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", ); } libMoneroWallet?.startSyncing(); @@ -551,8 +548,11 @@ abstract class LibMoneroWallet _setSyncStatus(lib_monero_compat.ConnectedSyncStatus()); } catch (e, s) { _setSyncStatus(lib_monero_compat.FailedSyncStatus()); - Logging.instance.e("Exception caught in $runtimeType.updateNode(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception caught in $runtimeType.updateNode(): ", + error: e, + stackTrace: s, + ); } return; @@ -566,13 +566,14 @@ abstract class LibMoneroWallet return; } - final localTxids = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .heightGreaterThan(0) - .txidProperty() - .findAll(); + final localTxids = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightGreaterThan(0) + .txidProperty() + .findAll(); final allTxids = await base.getAllTxids(refresh: true); @@ -582,10 +583,7 @@ abstract class LibMoneroWallet return; } - final transactions = await base.getTxs( - txids: txidsToFetch, - refresh: false, - ); + final transactions = await base.getTxs(txids: txidsToFetch, refresh: false); final allOutputs = await base.getOutputs(includeSpent: true, refresh: true); @@ -673,14 +671,16 @@ abstract class LibMoneroWallet type: type, subType: TransactionSubType.none, otherData: jsonEncode({ - TxV2OdKeys.overrideFee: Amount( - rawValue: tx.fee, - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), - TxV2OdKeys.moneroAmount: Amount( - rawValue: tx.amount, - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + TxV2OdKeys.overrideFee: + Amount( + rawValue: tx.fee, + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + TxV2OdKeys.moneroAmount: + Amount( + rawValue: tx.amount, + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), TxV2OdKeys.moneroAccountIndex: tx.accountIndex, TxV2OdKeys.isMoneroTransaction: true, }), @@ -756,9 +756,10 @@ abstract class LibMoneroWallet Future pathForWallet({ required String name, required lib_monero_compat.WalletType type, - }) async => - await pathForWalletDir(name: name, type: type) - .then((path) => '$path/$name'); + }) async => await pathForWalletDir( + name: name, + type: type, + ).then((path) => '$path/$name'); void onSyncingUpdate({ required int syncHeight, @@ -903,16 +904,10 @@ abstract class LibMoneroWallet } GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highest, - walletId, - ), + RefreshPercentChangedEvent(highest, walletId), ); GlobalEventBus.instance.fire( - BlocksRemainingEvent( - blocksLeft, - walletId, - ), + BlocksRemainingEvent(blocksLeft, walletId), ); } else if (_syncStatus is lib_monero_compat.SyncedSyncStatus) { status = WalletSyncStatus.synced; @@ -922,10 +917,7 @@ abstract class LibMoneroWallet } else if (_syncStatus is lib_monero_compat.StartingSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highestPercentCached, - walletId, - ), + RefreshPercentChangedEvent(highestPercentCached, walletId), ); } else if (_syncStatus is lib_monero_compat.FailedSyncStatus) { status = WalletSyncStatus.unableToSync; @@ -933,18 +925,12 @@ abstract class LibMoneroWallet } else if (_syncStatus is lib_monero_compat.ConnectingSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highestPercentCached, - walletId, - ), + RefreshPercentChangedEvent(highestPercentCached, walletId), ); } else if (_syncStatus is lib_monero_compat.ConnectedSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - highestPercentCached, - walletId, - ), + RefreshPercentChangedEvent(highestPercentCached, walletId), ); } else if (_syncStatus is lib_monero_compat.LostConnectionSyncStatus) { status = WalletSyncStatus.unableToSync; @@ -953,11 +939,7 @@ abstract class LibMoneroWallet if (status != null) { GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - status, - walletId, - info.coin, - ), + WalletSyncStatusChangedEvent(status, walletId, info.coin), ); } } @@ -1008,6 +990,24 @@ abstract class LibMoneroWallet } } + bool _torNodeMismatchGuard(NodeModel node) { + _canPing = true; // Reset. + + final bool mismatch = + (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || + (!prefs.useTor && !node.clearnetEnabled && node.torEnabled); + + if (mismatch) { + _canPing = false; + libMoneroWallet?.stopAutoSaving(); + libMoneroWallet?.stopListeners(); + libMoneroWallet?.stopSyncing(); + _setSyncStatus(lib_monero_compat.FailedSyncStatus()); + } + + return mismatch; // Caller decides whether to throw. + } + // ============ Overrides ==================================================== @override @@ -1019,24 +1019,27 @@ abstract class LibMoneroWallet @override Future updateUTXOs() async { final List outputArray = []; - final utxos = await libMoneroWallet?.getOutputs(refresh: true) ?? + final utxos = + await libMoneroWallet?.getOutputs(refresh: true) ?? []; for (final rawUTXO in utxos) { if (!rawUTXO.spent) { - final current = await mainDB.isar.utxos - .where() - .walletIdEqualTo(walletId) - .filter() - .voutEqualTo(rawUTXO.vout) - .and() - .txidEqualTo(rawUTXO.hash) - .findFirst(); - final tx = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(rawUTXO.hash) - .findFirst(); + final current = + await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .voutEqualTo(rawUTXO.vout) + .and() + .txidEqualTo(rawUTXO.hash) + .findFirst(); + final tx = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(rawUTXO.hash) + .findFirst(); final otherDataMap = { UTXOOtherDataKeys.keyImage: rawUTXO.keyImage, @@ -1101,22 +1104,8 @@ abstract class LibMoneroWallet final node = getCurrentNode(); - if (prefs.useTor) { - if (node.clearnetEnabled && !node.torEnabled) { - libMoneroWallet?.stopAutoSaving(); - libMoneroWallet?.stopListeners(); - libMoneroWallet?.stopSyncing(); - _setSyncStatus(lib_monero_compat.FailedSyncStatus()); - throw Exception("TOR - clearnet mismatch"); - } - } else { - if (!node.clearnetEnabled && node.torEnabled) { - libMoneroWallet?.stopAutoSaving(); - libMoneroWallet?.stopListeners(); - libMoneroWallet?.stopSyncing(); - _setSyncStatus(lib_monero_compat.FailedSyncStatus()); - throw Exception("TOR - clearnet mismatch"); - } + if (_torNodeMismatchGuard(node)) { + throw Exception("TOR – clearnet mismatch"); } // this acquire should be almost instant due to above check. @@ -1161,8 +1150,11 @@ abstract class LibMoneroWallet isar: mainDB.isar, ); } catch (e, s) { - Logging.instance - .e("Exception in generateNewAddress(): ", error: e, stackTrace: s); + Logging.instance.e( + "Exception in generateNewAddress(): ", + error: e, + stackTrace: s, + ); } } @@ -1173,9 +1165,10 @@ abstract class LibMoneroWallet throw Exception(); } catch (_, s) { Logging.instance.e( - "checkReceivingAddressForTransactions called but reuse address flag set: $s", - error: e, - stackTrace: s); + "checkReceivingAddressForTransactions called but reuse address flag set: $s", + error: e, + stackTrace: s, + ); } } @@ -1185,9 +1178,10 @@ abstract class LibMoneroWallet if (entries != null) { for (final element in entries) { if (!element.isSpend) { - final int curAddressIndex = element.addressIndexes.isEmpty - ? 0 - : element.addressIndexes.reduce(max); + final int curAddressIndex = + element.addressIndexes.isEmpty + ? 0 + : element.addressIndexes.reduce(max); if (curAddressIndex > highestIndex) { highestIndex = curAddressIndex; } @@ -1206,11 +1200,12 @@ abstract class LibMoneroWallet // Use new index to derive a new receiving address final newReceivingAddress = addressFor(index: newReceivingIndex); - final existing = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(newReceivingAddress.value) - .findFirst(); + final existing = + await mainDB + .getAddresses(walletId) + .filter() + .valueEqualTo(newReceivingAddress.value) + .findFirst(); if (existing == null) { // Add that new change address await mainDB.putAddress(newReceivingAddress); @@ -1225,15 +1220,17 @@ abstract class LibMoneroWallet } } on SocketException catch (se, s) { Logging.instance.e( - "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", - error: e, - stackTrace: s); + "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", + error: e, + stackTrace: s, + ); return; } catch (e, s) { Logging.instance.e( - "Exception rethrown from _checkReceivingAddressForTransactions(): ", - error: e, - stackTrace: s); + "Exception rethrown from _checkReceivingAddressForTransactions(): ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -1241,13 +1238,13 @@ abstract class LibMoneroWallet // TODO: this needs some work. Prio's may need to be changed as well as estimated blocks @override Future get fees async => FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 15, - numberOfBlocksSlow: 20, - fast: lib_monero.TransactionPriority.high.value, - medium: lib_monero.TransactionPriority.medium.value, - slow: lib_monero.TransactionPriority.normal.value, - ); + numberOfBlocksFast: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, + fast: BigInt.from(lib_monero.TransactionPriority.high.value), + medium: BigInt.from(lib_monero.TransactionPriority.medium.value), + slow: BigInt.from(lib_monero.TransactionPriority.normal.value), + ); @override Future updateChainHeight() async { @@ -1296,7 +1293,7 @@ abstract class LibMoneroWallet } else { final totalInputsValue = txData.utxos! .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)); + .fold(BigInt.zero, (p, e) => p + e); sweep = txData.amount!.raw == totalInputsValue; } @@ -1321,25 +1318,28 @@ abstract class LibMoneroWallet } final height = await chainHeight; - final inputs = txData.utxos - ?.map( - (e) => lib_monero.Output( - address: e.address!, - hash: e.txid, - keyImage: e.keyImage!, - value: BigInt.from(e.value), - isFrozen: e.isBlocked, - isUnlocked: e.blockHeight != null && - (height - (e.blockHeight ?? 0)) >= - cryptoCurrency.minConfirms, - height: e.blockHeight ?? 0, - vout: e.vout, - spent: e.used ?? false, - spentHeight: null, // doesn't matter here - coinbase: e.isCoinbase, - ), - ) - .toList(); + final inputs = + txData.utxos + ?.whereType() + .map( + (e) => lib_monero.Output( + address: e.address!, + hash: e.utxo.txid, + keyImage: e.utxo.keyImage!, + value: e.value, + isFrozen: e.utxo.isBlocked, + isUnlocked: + e.utxo.blockHeight != null && + (height - (e.utxo.blockHeight ?? 0)) >= + cryptoCurrency.minConfirms, + height: e.utxo.blockHeight ?? 0, + vout: e.utxo.vout, + spent: e.utxo.used ?? false, + spentHeight: null, // doesn't matter here + coinbase: e.utxo.isCoinbase, + ), + ) + .toList(); return await prepareSendMutex.protect(() async { final lib_monero.PendingTransaction pendingTransaction; @@ -1380,8 +1380,11 @@ abstract class LibMoneroWallet throw ArgumentError("Invalid fee rate argument provided!"); } } catch (e, s) { - Logging.instance.i("Exception rethrown from prepare send(): ", - error: e, stackTrace: s); + Logging.instance.i( + "Exception rethrown from prepare send(): ", + error: e, + stackTrace: s, + ); if (e.toString().contains("Incorrect unlocked balance")) { throw Exception("Insufficient balance!"); @@ -1395,9 +1398,7 @@ abstract class LibMoneroWallet Future confirmSend({required TxData txData}) async { try { try { - await libMoneroWallet!.commitTx( - txData.pendingTransaction!, - ); + await libMoneroWallet!.commitTx(txData.pendingTransaction!); Logging.instance.d( "transaction ${txData.pendingTransaction!.txid} has been sent", @@ -1405,14 +1406,18 @@ abstract class LibMoneroWallet return txData.copyWith(txid: txData.pendingTransaction!.txid); } catch (e, s) { Logging.instance.e( - "${info.name} ${compatType.name.toLowerCase()} confirmSend: ", - error: e, - stackTrace: s); + "${info.name} ${compatType.name.toLowerCase()} confirmSend: ", + error: e, + stackTrace: s, + ); rethrow; } } catch (e, s) { - Logging.instance.e("Exception rethrown from confirmSend(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception rethrown from confirmSend(): ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -1435,10 +1440,7 @@ abstract class LibMoneroWallet final String name = walletId; - final path = await pathForWallet( - name: name, - type: compatType, - ); + final path = await pathForWallet(name: name, type: compatType); final password = generatePassword(); await secureStorageInterface.write( @@ -1461,7 +1463,8 @@ abstract class LibMoneroWallet _setListener(); - final newReceivingAddress = await getCurrentReceivingAddress() ?? + final newReceivingAddress = + await getCurrentReceivingAddress() ?? Address( walletId: walletId, derivationIndex: 0, @@ -1488,8 +1491,11 @@ abstract class LibMoneroWallet libMoneroWallet?.startListeners(); libMoneroWallet?.startAutoSaving(); } catch (e, s) { - Logging.instance.e("Exception rethrown from recoverViewOnly(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception rethrown from recoverViewOnly(): ", + error: e, + stackTrace: s, + ); rethrow; } }); diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart new file mode 100644 index 000000000..f18cba8d0 --- /dev/null +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -0,0 +1,1602 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cs_salvium/cs_salvium.dart' as lib_salvium; +import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; +import 'package:stack_wallet_backup/generate_password.dart'; + +import '../../../models/balance.dart'; +import '../../../models/input.dart'; +import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/isar/models/blockchain_data/utxo.dart'; +import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../models/keys/cw_key_data.dart'; +import '../../../models/keys/view_only_wallet_data.dart'; +import '../../../models/node_model.dart'; +import '../../../models/paymint/fee_object_model.dart'; +import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; +import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; +import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import '../../../services/event_bus/events/global/tor_status_changed_event.dart'; +import '../../../services/event_bus/events/global/updated_in_background_event.dart'; +import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../services/event_bus/global_event_bus.dart'; +import '../../../services/tor_service.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/stack_file_system.dart'; +import '../../crypto_currency/intermediate/cryptonote_currency.dart'; +import '../../isar/models/wallet_info.dart'; +import '../../models/tx_data.dart'; +import '../wallet.dart'; +import '../wallet_mixin_interfaces/multi_address_interface.dart'; +import '../wallet_mixin_interfaces/view_only_option_interface.dart'; +import 'cryptonote_wallet.dart'; + +abstract class LibSalviumWallet + extends CryptonoteWallet + with ViewOnlyOptionInterface + implements MultiAddressInterface { + @override + int get isarTransactionVersion => 2; + + LibSalviumWallet(super.currency) { + final bus = GlobalEventBus.instance; + + // 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 { + await updateNode(); + }); + + // Potentially dangerous hack. See comments in _startInit() + _startInit(); + } + // cw based wallet listener to handle synchronization of utxo frozen states + late final StreamSubscription> _streamSub; + Future _startInit() async { + // Delay required as `mainDB` is not initialized in constructor. + // This is a hack and could lead to a race condition. + Future.delayed(const Duration(seconds: 2), () { + _streamSub = mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .watch(fireImmediately: true) + .listen((utxos) async { + try { + await onUTXOsChanged(utxos); + await updateBalance(shouldUpdateUtxos: false); + } catch (e, s) { + lib_salvium.Logging.log?.i("_startInit", error: e, stackTrace: s); + } + }); + }); + } + + lib_salvium.Wallet? libSalviumWallet; + + SyncStatus? get syncStatus => _syncStatus; + SyncStatus? _syncStatus; + int _syncedCount = 0; + void _setSyncStatus(SyncStatus status) { + if (status is SyncedSyncStatus) { + if (_syncStatus is SyncedSyncStatus) { + _syncedCount++; + } + } else { + _syncedCount = 0; + } + + if (_syncedCount < 3) { + _syncStatus = status; + syncStatusChanged(); + } + } + + final prepareSendMutex = Mutex(); + final estimateFeeMutex = Mutex(); + + bool _txRefreshLock = false; + int _lastCheckedHeight = -1; + int _txCount = 0; + int currentKnownChainHeight = 0; + double highestPercentCached = 0; + + Future loadWallet({required String path, required String password}); + + Future getCreatedWallet({ + required String path, + required String password, + required int wordCount, + required String seedOffset, + }); + + Future getRestoredWallet({ + required String path, + required String password, + required String mnemonic, + required String seedOffset, + int height = 0, + }); + + Future getRestoredFromViewKeyWallet({ + required String path, + required String password, + required String address, + required String privateViewKey, + int height = 0, + }); + + void invalidSeedLengthCheck(int length); + + bool walletExists(String path); + + String getTxKeyFor({required String txid}) { + if (libSalviumWallet == null) { + throw Exception("Cannot get tx key in uninitialized libSalviumWallet"); + } + return libSalviumWallet!.getTxKey(txid); + } + + void _setListener() { + if (libSalviumWallet != null && libSalviumWallet!.getListeners().isEmpty) { + libSalviumWallet?.addListener( + lib_salvium.WalletListener( + onSyncingUpdate: onSyncingUpdate, + onNewBlock: onNewBlock, + onBalancesChanged: onBalancesChanged, + onError: (e, s) { + Logging.instance.w("$e\n$s", error: e, stackTrace: s); + }, + ), + ); + } + } + + @override + Future open() async { + bool wasNull = false; + + if (libSalviumWallet == null) { + wasNull = true; + // await libSalviumWallet?.close(); + final path = await pathForWallet(name: walletId); + + final String password; + try { + password = + (await secureStorageInterface.read( + key: _libSalviumWalletPasswordKey(walletId.toUpperCase()), + ))!; + } catch (e, s) { + throw Exception("Password not found $e, $s"); + } + + await loadWallet(path: path, password: password); + + _setListener(); + + await updateNode(); + } + + Address? currentAddress = await getCurrentReceivingAddress(); + if (currentAddress == null) { + currentAddress = addressFor(index: 0); + await mainDB.updateOrPutAddresses([currentAddress]); + } + if (info.cachedReceivingAddress != currentAddress.value) { + await info.updateReceivingAddress( + newAddress: currentAddress.value, + isar: mainDB.isar, + ); + } + + if (wasNull) { + try { + _setSyncStatus(ConnectingSyncStatus()); + libSalviumWallet?.startSyncing(); + } catch (_) { + _setSyncStatus(FailedSyncStatus()); + // TODO log + } + } + _setListener(); + libSalviumWallet?.startListeners(); + libSalviumWallet?.startAutoSaving(); + + unawaited(refresh()); + } + + Future save() async { + if (!Platform.isWindows) { + final appRoot = await StackFileSystem.applicationRootDirectory(); + await _backupWalletFiles(name: walletId, appRoot: appRoot); + } + await libSalviumWallet!.save(); + } + + Address addressFor({required int index, int account = 0}) { + final address = libSalviumWallet!.getAddress( + accountIndex: account, + addressIndex: index, + ); + + if (address.value.contains("111")) { + throw Exception("111 address found!"); + } + + final newReceivingAddress = Address( + walletId: walletId, + derivationIndex: index, + derivationPath: null, + value: address.value, + publicKey: [], + type: AddressType.cryptonote, + subType: AddressSubType.receiving, + ); + + return newReceivingAddress; + } + + Future getKeys() async { + final base = libSalviumWallet; + + if (base == null) { + return null; + } + try { + return CWKeyData( + walletId: walletId, + publicViewKey: base.getPublicViewKey(), + privateViewKey: base.getPrivateViewKey(), + publicSpendKey: base.getPublicSpendKey(), + privateSpendKey: base.getPrivateSpendKey(), + ); + } catch (e, s) { + Logging.instance.f("getKeys failed: ", error: e, stackTrace: s); + return CWKeyData( + walletId: walletId, + publicViewKey: "ERROR", + privateViewKey: "ERROR", + publicSpendKey: "ERROR", + privateSpendKey: "ERROR", + ); + } + } + + Future<(String, String)> + hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing() async { + final path = await pathForWallet(name: walletId); + final String password; + try { + password = + (await secureStorageInterface.read( + key: _libSalviumWalletPasswordKey(walletId), + ))!; + } catch (e, s) { + throw Exception("Password not found $e, $s"); + } + await loadWallet(path: path, password: password); + final wallet = libSalviumWallet!; + return (wallet.getAddress().value, wallet.getPrivateViewKey()); + } + + @override + Future init({bool? isRestore, int? wordCount}) async { + final path = await pathForWallet(name: walletId); + if (!(walletExists(path)) && isRestore != true) { + wordCount ??= 25; + try { + final password = generatePassword(); + await secureStorageInterface.write( + key: _libSalviumWalletPasswordKey(walletId), + value: password, + ); + final wallet = await getCreatedWallet( + path: path, + password: password, + wordCount: wordCount, + seedOffset: "", // default for non restored wallets for now + ); + + final height = wallet.getRefreshFromBlockHeight(); + + await info.updateRestoreHeight( + newRestoreHeight: height, + isar: mainDB.isar, + ); + + // special case for xmr/wow. Normally mnemonic + passphrase is saved + // before wallet.init() is called + await secureStorageInterface.write( + key: Wallet.mnemonicKey(walletId: walletId), + value: wallet.getSeed().trim(), + ); + await secureStorageInterface.write( + key: Wallet.mnemonicPassphraseKey(walletId: walletId), + value: "", + ); + } catch (e, s) { + Logging.instance.f("", error: e, stackTrace: s); + } + await updateNode(); + } + + return super.init(); + } + + @override + Future recover({required bool isRescan}) async { + if (isRescan) { + await refreshMutex.protect(() async { + // clear blockchain info + await mainDB.deleteWalletBlockchainData(walletId); + + highestPercentCached = 0; + unawaited(libSalviumWallet?.rescanBlockchain()); + libSalviumWallet?.startSyncing(); + // unawaited(save()); + }); + unawaited(refresh()); + return; + } + + if (isViewOnly) { + await recoverViewOnly(); + return; + } + + await refreshMutex.protect(() async { + final mnemonic = await getMnemonic(); + final seedOffset = await getMnemonicPassphrase(); + final seedLength = mnemonic.trim().split(" ").length; + + invalidSeedLengthCheck(seedLength); + + try { + final height = max(info.restoreHeight, 0); + + await info.updateRestoreHeight( + newRestoreHeight: height, + isar: mainDB.isar, + ); + + final String name = walletId; + + final path = await pathForWallet(name: name); + + try { + final password = generatePassword(); + await secureStorageInterface.write( + key: _libSalviumWalletPasswordKey(walletId), + value: password, + ); + final wallet = await getRestoredWallet( + path: path, + password: password, + mnemonic: mnemonic, + height: height, + seedOffset: seedOffset, + ); + + if (libSalviumWallet != null) { + await exit(); + } + + libSalviumWallet = wallet; + + _setListener(); + + final newReceivingAddress = + await getCurrentReceivingAddress() ?? + Address( + walletId: walletId, + derivationIndex: 0, + derivationPath: null, + value: wallet.getAddress().value, + publicKey: [], + type: AddressType.cryptonote, + subType: AddressSubType.receiving, + ); + + await mainDB.updateOrPutAddresses([newReceivingAddress]); + await info.updateReceivingAddress( + newAddress: newReceivingAddress.value, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.f("", error: e, stackTrace: s); + rethrow; + } + await updateNode(); + _setListener(); + + // libSalviumWallet?.setRecoveringFromSeed(isRecovery: true); + unawaited(libSalviumWallet?.rescanBlockchain()); + libSalviumWallet?.startSyncing(); + + // await save(); + libSalviumWallet?.startListeners(); + libSalviumWallet?.startAutoSaving(); + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from recoverFromMnemonic(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + }); + } + + bool _canPing = false; + + @override + Future pingCheck() async { + if (_canPing) { + return (await libSalviumWallet?.isConnectedToDaemon()) ?? false; + } else { + return false; + } + } + + @override + Future updateNode() async { + final node = getCurrentNode(); + + if (_torNodeMismatchGuard(node)) { + throw Exception("TOR – clearnet mismatch"); + } + + final host = + node.host.endsWith(".onion") ? node.host : Uri.parse(node.host).host; + ({InternetAddress host, int port})? proxy; + proxy = + prefs.useTor && !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; + + _setSyncStatus(ConnectingSyncStatus()); + try { + if (_requireMutex) { + await _torConnectingLock.protect(() async { + await libSalviumWallet?.connect( + daemonAddress: "$host:${node.port}", + daemonUsername: node.loginName, + daemonPassword: await node.getPassword(secureStorageInterface), + trusted: node.trusted ?? false, + useSSL: node.useSSL, + socksProxyAddress: + node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", + ); + }); + } else { + await libSalviumWallet?.connect( + daemonAddress: "$host:${node.port}", + daemonUsername: node.loginName, + daemonPassword: await node.getPassword(secureStorageInterface), + trusted: node.trusted ?? false, + useSSL: node.useSSL, + socksProxyAddress: + node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", + ); + } + libSalviumWallet?.startSyncing(); + libSalviumWallet?.startListeners(); + libSalviumWallet?.startAutoSaving(); + + // _setSyncStatus(ConnectedSyncStatus()); + } catch (e, s) { + // _setSyncStatus(FailedSyncStatus()); + Logging.instance.e( + "Exception caught in $runtimeType.updateNode(): ", + error: e, + stackTrace: s, + ); + } + + return; + } + + @override + Future updateTransactions() async { + final base = libSalviumWallet; + + if (base == null) { + return; + } + + final localTxids = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightGreaterThan(0) + .txidProperty() + .findAll(); + + final allTxids = await base.getAllTxids(refresh: true); + + final txidsToFetch = allTxids.toSet().difference(localTxids.toSet()); + + if (txidsToFetch.isEmpty) { + return; + } + + final transactions = await base.getTxs(txids: txidsToFetch, refresh: false); + + final allOutputs = await base.getOutputs(includeSpent: true, refresh: true); + + // final cachedTransactions = + // DB.instance.get(boxName: walletId, key: 'latest_tx_model') + // as TransactionData?; + // int latestTxnBlockHeight = + // DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + // as int? ?? + // 0; + // + // final txidsList = DB.instance + // .get(boxName: walletId, key: "cachedTxids") as List? ?? + // []; + // + // final Set cachedTxids = Set.from(txidsList); + + // TODO: filter to skip cached + confirmed txn processing in next step + // final unconfirmedCachedTransactions = + // cachedTransactions?.getAllTransactions() ?? {}; + // unconfirmedCachedTransactions + // .removeWhere((key, value) => value.confirmedStatus); + // + // if (cachedTransactions != null) { + // for (final tx in allTxHashes.toList(growable: false)) { + // final txHeight = tx["height"] as int; + // if (txHeight > 0 && + // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + // allTxHashes.remove(tx); + // } + // } + // } + // } + + final List txns = []; + + for (final tx in transactions) { + final associatedOutputs = allOutputs.where((e) => e.hash == tx.hash); + final List inputs = []; + final List outputs = []; + TransactionType type; + if (!tx.isSpend) { + type = TransactionType.incoming; + for (final output in associatedOutputs) { + outputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "", + valueStringSats: output.value.toString(), + addresses: [output.address], + walletOwns: true, + ), + ); + } + } else { + type = TransactionType.outgoing; + for (final output in associatedOutputs) { + inputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: null, + scriptSigAsm: null, + sequence: null, + outpoint: null, + addresses: [output.address], + valueStringSats: output.value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + } + } + + final txn = TransactionV2( + walletId: walletId, + blockHash: null, // not exposed via current cs_salvium + hash: tx.hash, + txid: tx.hash, + timestamp: (tx.timeStamp.millisecondsSinceEpoch ~/ 1000), + height: tx.blockHeight, + inputs: inputs, + outputs: outputs, + version: -1, // not exposed via current cs_salvium + type: type, + subType: TransactionSubType.none, + otherData: jsonEncode({ + TxV2OdKeys.overrideFee: + Amount( + rawValue: tx.fee, + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + TxV2OdKeys.moneroAmount: + Amount( + rawValue: tx.amount, + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + TxV2OdKeys.moneroAccountIndex: tx.accountIndex, + TxV2OdKeys.isMoneroTransaction: true, + }), + ); + + txns.add(txn); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } + + Future get availableBalance async { + try { + return Amount( + rawValue: libSalviumWallet!.getUnlockedBalance(), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } catch (_) { + return info.cachedBalance.spendable; + } + } + + Future get totalBalance async { + try { + final full = libSalviumWallet?.getBalance(); + if (full != null) { + return Amount( + rawValue: full, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } else { + final transactions = await libSalviumWallet!.getAllTxs(refresh: true); + BigInt transactionBalance = BigInt.zero; + for (final tx in transactions) { + if (!tx.isSpend) { + transactionBalance += tx.amount; + } else { + transactionBalance += -tx.amount - tx.fee; + } + } + + return Amount( + rawValue: transactionBalance, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + } catch (_) { + return info.cachedBalance.total; + } + } + + @override + Future exit() async { + Logging.instance.i("exit called on $walletId"); + libSalviumWallet?.stopAutoSaving(); + libSalviumWallet?.stopListeners(); + libSalviumWallet?.stopSyncing(); + await libSalviumWallet?.save(); + } + + Future pathForWalletDir({required String name}) async { + final Directory root = await StackFileSystem.applicationRootDirectory(); + return _pathForWalletDir(name: name, appRoot: root); + } + + Future pathForWallet({required String name}) async => + await pathForWalletDir(name: name).then((path) => '$path/$name'); + + void onSyncingUpdate({ + required int syncHeight, + required int nodeHeight, + String? message, + }) { + if (nodeHeight > 0 && syncHeight >= 0) { + currentKnownChainHeight = nodeHeight; + updateChainHeight(); + final blocksLeft = nodeHeight - syncHeight; + final SyncStatus status; + if (blocksLeft < 100) { + status = SyncedSyncStatus(); + + // if (!_hasSyncAfterStartup) { + // _hasSyncAfterStartup = true; + // await save(); + // } + // + // if (walletInfo.isRecovery!) { + // await setAsRecovered(); + // } + } else { + final percent = syncHeight / currentKnownChainHeight; + + status = SyncingSyncStatus( + blocksLeft, + percent, + currentKnownChainHeight, + ); + } + + _setSyncStatus(status); + _refreshTxDataHelper(); + } + } + + void onBalancesChanged({ + required BigInt newBalance, + required BigInt newUnlockedBalance, + }) async { + try { + await updateBalance(); + await updateTransactions(); + } catch (e, s) { + Logging.instance.w("onBalancesChanged(): ", error: e, stackTrace: s); + } + } + + void onNewBlock(int nodeHeight) async { + try { + await updateTransactions(); + } catch (e, s) { + Logging.instance.w("onNewBlock(): ", error: e, stackTrace: s); + } + } + + final _utxosUpdateLock = Mutex(); + Future onUTXOsChanged(List utxos) async { + await _utxosUpdateLock.protect(() async { + final cwUtxos = await libSalviumWallet?.getOutputs(refresh: true) ?? []; + + // bool changed = false; + + for (final cw in cwUtxos) { + final match = utxos.where( + (e) => + e.keyImage != null && + e.keyImage!.isNotEmpty && + e.keyImage == cw.keyImage, + ); + + if (match.isNotEmpty) { + final u = match.first; + + if (u.isBlocked) { + if (!cw.isFrozen) { + await libSalviumWallet?.freezeOutput(cw.keyImage); + // changed = true; + } + } else { + if (cw.isFrozen) { + await libSalviumWallet?.thawOutput(cw.keyImage); + // changed = true; + } + } + } + } + + // if (changed) { + // await libSalviumWallet?.updateUTXOs(); + // } + }); + } + + void onNewTransaction() { + // TODO: [prio=low] get rid of UpdatedInBackgroundEvent and move to + // adding the v2 tx to the db which would update ui automagically since the + // db is watched by the ui + // call this here? + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "New data found in $walletId ${info.name} in background!", + walletId, + ), + ); + } + + void syncStatusChanged() async { + final _syncStatus = syncStatus; + + if (_syncStatus != null) { + if (_syncStatus.progress() == 1 && refreshMutex.isLocked) { + refreshMutex.release(); + } + + WalletSyncStatus? status; + xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(true); + + if (_syncStatus is SyncingSyncStatus) { + final int blocksLeft = _syncStatus.blocksLeft; + + // ensure at least 1 to prevent math errors + final int height = max(1, _syncStatus.height); + + final nodeHeight = height + blocksLeft; + currentKnownChainHeight = nodeHeight; + + // final percent = height / nodeHeight; + final percent = _syncStatus.ptc; + + final highest = max(highestPercentCached, percent); + + final unchanged = highest == highestPercentCached; + if (unchanged) { + return; + } + + // update cached + if (highestPercentCached < percent) { + highestPercentCached = percent; + } + + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highest, walletId), + ); + GlobalEventBus.instance.fire( + BlocksRemainingEvent(blocksLeft, walletId), + ); + } else if (_syncStatus is SyncedSyncStatus) { + status = WalletSyncStatus.synced; + } else if (_syncStatus is NotConnectedSyncStatus) { + status = WalletSyncStatus.unableToSync; + xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); + } else if (_syncStatus is StartingSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercentCached, walletId), + ); + } else if (_syncStatus is FailedSyncStatus) { + status = WalletSyncStatus.unableToSync; + xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); + } else if (_syncStatus is ConnectingSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercentCached, walletId), + ); + } else if (_syncStatus is ConnectedSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercentCached, walletId), + ); + } else if (_syncStatus is LostConnectionSyncStatus) { + status = WalletSyncStatus.unableToSync; + xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); + } + + if (status != null) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent(status, walletId, info.coin), + ); + } + } + } + + @override + Future checkSaveInitialReceivingAddress() async { + // this doesn't work without opening the wallet first which takes a while + } + + // ============ Private ====================================================== + Future _refreshTxDataHelper() async { + if (_txRefreshLock) return; + _txRefreshLock = true; + + final _syncStatus = syncStatus; + + if (_syncStatus != null && _syncStatus is SyncingSyncStatus) { + final int blocksLeft = _syncStatus.blocksLeft; + final tenKChange = blocksLeft ~/ 10000; + + // only refresh transactions periodically during a sync + if (_lastCheckedHeight == -1 || tenKChange < _lastCheckedHeight) { + _lastCheckedHeight = tenKChange; + await _refreshTxData(); + } + } else { + await _refreshTxData(); + } + + _txRefreshLock = false; + } + + Future _refreshTxData() async { + await updateTransactions(); + final count = await mainDB.getTransactions(walletId).count(); + + if (count > _txCount) { + _txCount = count; + await updateBalance(); + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "New transaction data found in $walletId ${info.name}!", + walletId, + ), + ); + } + } + + bool _torNodeMismatchGuard(NodeModel node) { + _canPing = true; // Reset. + + final bool mismatch = + (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || + (!prefs.useTor && !node.clearnetEnabled && node.torEnabled); + + if (mismatch) { + _canPing = false; + libSalviumWallet?.stopAutoSaving(); + libSalviumWallet?.stopListeners(); + libSalviumWallet?.stopSyncing(); + _setSyncStatus(FailedSyncStatus()); + } + + return mismatch; // Caller decides whether to throw. + } + + // ============ Overrides ==================================================== + + @override + FilterOperation? get changeAddressFilterOperation => null; + + @override + FilterOperation? get receivingAddressFilterOperation => null; + + @override + Future updateUTXOs() async { + final List outputArray = []; + final utxos = + await libSalviumWallet?.getOutputs(refresh: true) ?? + []; + for (final rawUTXO in utxos) { + if (!rawUTXO.spent) { + final current = + await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .voutEqualTo(rawUTXO.vout) + .and() + .txidEqualTo(rawUTXO.hash) + .findFirst(); + final tx = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(rawUTXO.hash) + .findFirst(); + + final otherDataMap = { + UTXOOtherDataKeys.keyImage: rawUTXO.keyImage, + UTXOOtherDataKeys.spent: rawUTXO.spent, + }; + + final utxo = UTXO( + address: rawUTXO.address, + walletId: walletId, + txid: rawUTXO.hash, + vout: rawUTXO.vout, + value: rawUTXO.value.toInt(), + name: current?.name ?? "", + isBlocked: current?.isBlocked ?? rawUTXO.isFrozen, + blockedReason: current?.blockedReason ?? "", + isCoinbase: rawUTXO.coinbase, + blockHash: "", + blockHeight: + tx?.height ?? (rawUTXO.height > 0 ? rawUTXO.height : null), + blockTime: tx?.timestamp, + otherData: jsonEncode(otherDataMap), + ); + + outputArray.add(utxo); + } + } + + await mainDB.updateUTXOs(walletId, outputArray); + + return true; + } + + @override + Future updateBalance({bool shouldUpdateUtxos = true}) async { + if (shouldUpdateUtxos) { + await updateUTXOs(); + } + + final total = await totalBalance; + final available = await availableBalance; + + final balance = Balance( + total: total, + spendable: available, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + pendingSpendable: total - available, + ); + + await info.updateBalance(newBalance: balance, isar: mainDB.isar); + } + + @override + Future refresh() async { + // Awaiting this lock could be dangerous. + // Since refresh is periodic (generally) + if (refreshMutex.isLocked) { + return; + } + + final node = getCurrentNode(); + + if (_torNodeMismatchGuard(node)) { + throw Exception("TOR – clearnet mismatch"); + } + + // this acquire should be almost instant due to above check. + // Slight possibility of race but should be irrelevant + await refreshMutex.acquire(); + + libSalviumWallet?.startSyncing(); + _setSyncStatus(StartingSyncStatus()); + + await updateTransactions(); + await updateBalance(); + + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } + + if (refreshMutex.isLocked) { + refreshMutex.release(); + } + + final synced = await libSalviumWallet?.isSynced(); + + if (synced == true) { + _setSyncStatus(SyncedSyncStatus()); + } + } + + @override + Future generateNewReceivingAddress() async { + try { + final currentReceiving = await getCurrentReceivingAddress(); + + final newReceivingIndex = + currentReceiving == null ? 0 : currentReceiving.derivationIndex + 1; + + final newReceivingAddress = addressFor(index: newReceivingIndex); + + // Add that new receiving address + await mainDB.putAddress(newReceivingAddress); + await info.updateReceivingAddress( + newAddress: newReceivingAddress.value, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.e( + "Exception in generateNewAddress(): ", + error: e, + stackTrace: s, + ); + } + } + + @override + Future checkReceivingAddressForTransactions() async { + if (info.otherData[WalletInfoKeys.reuseAddress] == true) { + try { + throw Exception(); + } catch (_, s) { + Logging.instance.e( + "checkReceivingAddressForTransactions called but reuse address flag set: $s", + error: e, + stackTrace: s, + ); + } + } + + try { + int highestIndex = -1; + final entries = await libSalviumWallet?.getAllTxs(refresh: true); + if (entries != null) { + for (final element in entries) { + if (!element.isSpend) { + final int curAddressIndex = + element.addressIndexes.isEmpty + ? 0 + : element.addressIndexes.reduce(max); + if (curAddressIndex > highestIndex) { + highestIndex = curAddressIndex; + } + } + } + } + + // Check the new receiving index + final currentReceiving = await getCurrentReceivingAddress(); + final curIndex = currentReceiving?.derivationIndex ?? -1; + + if (highestIndex >= curIndex) { + // First increment the receiving index + final newReceivingIndex = curIndex + 1; + + // Use new index to derive a new receiving address + final newReceivingAddress = addressFor(index: newReceivingIndex); + + final existing = + await mainDB + .getAddresses(walletId) + .filter() + .valueEqualTo(newReceivingAddress.value) + .findFirst(); + if (existing == null) { + // Add that new change address + await mainDB.putAddress(newReceivingAddress); + } else { + // we need to update the address + await mainDB.updateAddress(existing, newReceivingAddress); + } + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + // keep checking until address with no tx history is set as current + await checkReceivingAddressForTransactions(); + } + } + } on SocketException catch (se, s) { + Logging.instance.e( + "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", + error: e, + stackTrace: s, + ); + return; + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from _checkReceivingAddressForTransactions(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + // TODO: this needs some work. Prio's may need to be changed as well as estimated blocks + @override + Future get fees async => FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, + fast: BigInt.from(lib_salvium.TransactionPriority.high.value), + medium: BigInt.from(lib_salvium.TransactionPriority.medium.value), + slow: BigInt.from(lib_salvium.TransactionPriority.normal.value), + ); + + @override + Future updateChainHeight() async { + await info.updateCachedChainHeight( + newHeight: currentKnownChainHeight, + isar: mainDB.isar, + ); + } + + @override + Future checkChangeAddressForTransactions() async { + // do nothing + } + + @override + Future generateNewChangeAddress() async { + // do nothing + } + + @override + Future prepareSend({required TxData txData}) async { + try { + final feeRate = txData.feeRateType; + if (feeRate is FeeRateType) { + lib_salvium.TransactionPriority feePriority; + switch (feeRate) { + case FeeRateType.fast: + feePriority = lib_salvium.TransactionPriority.high; + break; + case FeeRateType.average: + feePriority = lib_salvium.TransactionPriority.medium; + break; + case FeeRateType.slow: + feePriority = lib_salvium.TransactionPriority.normal; + break; + default: + throw ArgumentError("Invalid use of custom fee"); + } + + try { + final bool sweep; + + if (txData.utxos == null) { + final balance = await availableBalance; + sweep = txData.amount! == balance; + } else { + final totalInputsValue = txData.utxos! + .map((e) => e.value) + .fold(BigInt.zero, (p, e) => p + e); + sweep = txData.amount!.raw == totalInputsValue; + } + + // TODO: test this one day + // cs_salvium may not support this yet properly + if (sweep && txData.recipients!.length > 1) { + throw Exception("Send all not supported with multiple recipients"); + } + + final List outputs = []; + for (final recipient in txData.recipients!) { + final output = lib_salvium.Recipient( + address: recipient.address, + amount: recipient.amount.raw, + ); + + outputs.add(output); + } + + if (outputs.isEmpty) { + throw Exception("No recipients provided"); + } + + final height = await chainHeight; + final inputs = + txData.utxos + ?.whereType() + .map( + (e) => lib_salvium.Output( + address: e.utxo.address!, + hash: e.utxo.txid, + keyImage: e.utxo.keyImage!, + value: e.value, + isFrozen: e.utxo.isBlocked, + isUnlocked: + e.utxo.blockHeight != null && + (height - (e.utxo.blockHeight ?? 0)) >= + cryptoCurrency.minConfirms, + height: e.utxo.blockHeight ?? 0, + vout: e.utxo.vout, + spent: e.utxo.used ?? false, + spentHeight: null, // doesn't matter here + coinbase: e.utxo.isCoinbase, + ), + ) + .toList(); + + return await prepareSendMutex.protect(() async { + final lib_salvium.PendingTransaction pendingTransaction; + if (outputs.length == 1) { + pendingTransaction = await libSalviumWallet!.createTx( + output: outputs.first, + paymentId: "", + sweep: sweep, + priority: feePriority, + preferredInputs: inputs, + accountIndex: 0, // sw only uses account 0 at this time + ); + } else { + pendingTransaction = await libSalviumWallet!.createTxMultiDest( + outputs: outputs, + paymentId: "", + priority: feePriority, + preferredInputs: inputs, + sweep: sweep, + accountIndex: 0, // sw only uses account 0 at this time + ); + } + + final realFee = Amount( + rawValue: pendingTransaction.fee, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + return txData.copyWith( + fee: realFee, + pendingSalviumTransaction: pendingTransaction, + ); + }); + } catch (e) { + rethrow; + } + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.i( + "Exception rethrown from prepare send(): ", + error: e, + stackTrace: s, + ); + + if (e.toString().contains("Incorrect unlocked balance")) { + throw Exception("Insufficient balance!"); + } else { + throw Exception("Transaction failed with error: $e"); + } + } + } + + @override + Future confirmSend({required TxData txData}) async { + try { + try { + await libSalviumWallet!.commitTx(txData.pendingSalviumTransaction!); + + Logging.instance.d( + "transaction ${txData.pendingSalviumTransaction!.txid} has been sent", + ); + return txData.copyWith(txid: txData.pendingSalviumTransaction!.txid); + } catch (e, s) { + Logging.instance.e( + "${info.name} confirmSend: ", + error: e, + stackTrace: s, + ); + rethrow; + } + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from confirmSend(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + // ============== View only ================================================== + + @override + Future recoverViewOnly() async { + await refreshMutex.protect(() async { + final data = + await getViewOnlyWalletData() as CryptonoteViewOnlyWalletData; + + try { + final height = max(info.restoreHeight, 0); + + await info.updateRestoreHeight( + newRestoreHeight: height, + isar: mainDB.isar, + ); + + final String name = walletId; + + final path = await pathForWallet(name: name); + + final password = generatePassword(); + await secureStorageInterface.write( + key: _libSalviumWalletPasswordKey(walletId.toUpperCase()), + value: password, + ); + final wallet = await getRestoredFromViewKeyWallet( + path: path, + password: password, + address: data.address, + privateViewKey: data.privateViewKey, + height: height, + ); + + if (libSalviumWallet != null) { + await exit(); + } + + libSalviumWallet = wallet; + + _setListener(); + + final newReceivingAddress = + await getCurrentReceivingAddress() ?? + Address( + walletId: walletId, + derivationIndex: 0, + derivationPath: null, + value: wallet.getAddress().value, + publicKey: [], + type: AddressType.cryptonote, + subType: AddressSubType.receiving, + ); + + await mainDB.updateOrPutAddresses([newReceivingAddress]); + await info.updateReceivingAddress( + newAddress: newReceivingAddress.value, + isar: mainDB.isar, + ); + + await updateNode(); + _setListener(); + + unawaited(libSalviumWallet?.rescanBlockchain()); + libSalviumWallet?.startSyncing(); + + // await save(); + libSalviumWallet?.startListeners(); + libSalviumWallet?.startAutoSaving(); + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from recoverViewOnly(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + }); + } + + // ============== Private ==================================================== + + StreamSubscription? _torStatusListener; + StreamSubscription? _torPreferenceListener; + + final Mutex _torConnectingLock = Mutex(); + bool _requireMutex = false; +} + +String _libSalviumWalletPasswordKey(String walletName) => + "SALVIUM_WALLET_PASSWORD_${walletName.toUpperCase()}"; + +String _backupFileName(String originalPath) { + final pathParts = originalPath.split('/'); + final newName = '#_${pathParts.last}'; + pathParts.removeLast(); + pathParts.add(newName); + return pathParts.join('/'); +} + +Future _backupWalletFiles({ + required String name, + required Directory appRoot, +}) async { + final path = await _pathForWallet(name: name, appRoot: appRoot); + final cacheFile = File(path); + final keysFile = File('$path.keys'); + final addressListFile = File('$path.address.txt'); + final newCacheFilePath = _backupFileName(cacheFile.path); + final newKeysFilePath = _backupFileName(keysFile.path); + final newAddressListFilePath = _backupFileName(addressListFile.path); + + if (cacheFile.existsSync()) { + await cacheFile.copy(newCacheFilePath); + } + + if (keysFile.existsSync()) { + await keysFile.copy(newKeysFilePath); + } + + if (addressListFile.existsSync()) { + await addressListFile.copy(newAddressListFilePath); + } +} + +Future _pathForWalletDir({ + required String name, + required Directory appRoot, +}) async { + final walletsDir = Directory('${appRoot.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/salvium/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; +} + +Future _pathForWallet({ + required String name, + required Directory appRoot, +}) async => await _pathForWalletDir( + name: name, + appRoot: appRoot, +).then((path) => '$path/$name'); + +Future salviumWalletDir({ + required String walletId, + required Directory appRoot, +}) => _pathForWalletDir(name: walletId, appRoot: appRoot); + +// ============================================================================= +// The following sync status stuff copy pasted here for now to simplify the +// integration of salvium. +// TODO: eventually rework this (one day) + +abstract class SyncStatus { + const SyncStatus(); + double progress(); +} + +class SyncingSyncStatus extends SyncStatus { + SyncingSyncStatus(this.blocksLeft, this.ptc, this.height); + + final double ptc; + final int blocksLeft; + final int height; + + @override + double progress() => ptc; + + @override + String toString() => '$blocksLeft'; +} + +class SyncedSyncStatus extends SyncStatus { + @override + double progress() => 1.0; +} + +class NotConnectedSyncStatus extends SyncStatus { + const NotConnectedSyncStatus(); + + @override + double progress() => 0.0; +} + +class StartingSyncStatus extends SyncStatus { + @override + double progress() => 0.0; +} + +class FailedSyncStatus extends SyncStatus { + @override + double progress() => 1.0; +} + +class ConnectingSyncStatus extends SyncStatus { + @override + double progress() => 0.0; +} + +class ConnectedSyncStatus extends SyncStatus { + @override + double progress() => 0.0; +} + +class LostConnectionSyncStatus extends SyncStatus { + @override + double progress() => 1.0; +} + +// ============================================================================= diff --git a/lib/wallets/wallet/intermediate/lib_xelis_wallet.dart b/lib/wallets/wallet/intermediate/lib_xelis_wallet.dart index 901223469..2415af7bd 100644 --- a/lib/wallets/wallet/intermediate/lib_xelis_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_xelis_wallet.dart @@ -327,6 +327,10 @@ abstract class LibXelisWallet @override Future open() async { + while (exitInProgress) { + await Future.delayed(const Duration(milliseconds: 500)); + } + try { await connect(); } catch (e) { @@ -339,18 +343,25 @@ abstract class LibXelisWallet unawaited(refresh()); } + bool exitInProgress = false; + @override Future exit() async { - await refreshMutex.protect(() async { - timer?.cancel(); - timer = null; - - await _eventSubscription?.cancel(); - _eventSubscription = null; - - await libXelisWallet?.offlineMode(); - await super.exit(); - }); + exitInProgress = true; + try { + await refreshMutex.protect(() async { + timer?.cancel(); + timer = null; + + await _eventSubscription?.cancel(); + _eventSubscription = null; + + await libXelisWallet?.offlineMode(); + await super.exit(); + }); + } finally { + exitInProgress = false; + } } void invalidSeedLengthCheck(int length) { diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index be21c5282..554e04313 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -35,6 +35,7 @@ import 'impl/dogecoin_wallet.dart'; import 'impl/ecash_wallet.dart'; import 'impl/epiccash_wallet.dart'; import 'impl/ethereum_wallet.dart'; +import 'impl/fact0rn_wallet.dart'; import 'impl/firo_wallet.dart'; import 'impl/litecoin_wallet.dart'; import 'impl/monero_wallet.dart'; @@ -42,6 +43,7 @@ import 'impl/namecoin_wallet.dart'; import 'impl/nano_wallet.dart'; import 'impl/particl_wallet.dart'; import 'impl/peercoin_wallet.dart'; +import 'impl/salvium_wallet.dart'; import 'impl/solana_wallet.dart'; import 'impl/stellar_wallet.dart'; import 'impl/sub_wallets/eth_token_wallet.dart'; @@ -50,7 +52,6 @@ import 'impl/wownero_wallet.dart'; import 'impl/xelis_wallet.dart'; import 'intermediate/cryptonote_wallet.dart'; import 'wallet_mixin_interfaces/electrumx_interface.dart'; -import 'wallet_mixin_interfaces/lelantus_interface.dart'; import 'wallet_mixin_interfaces/mnemonic_interface.dart'; import 'wallet_mixin_interfaces/multi_address_interface.dart'; import 'wallet_mixin_interfaces/paynym_interface.dart'; @@ -127,11 +128,7 @@ abstract class Wallet { await updateChainHeight(); } catch (e, s) { // do nothing on failure (besides logging) - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); } // return regardless of whether it was updated or not as we want a @@ -173,7 +170,8 @@ abstract class Wallet { value: viewOnlyData!.toJsonEncodedString(), ); } else if (wallet is MnemonicInterface) { - if (wallet is CryptonoteWallet || wallet is XelisWallet) { // + if (wallet is CryptonoteWallet || wallet is XelisWallet) { + // // currently a special case due to the xmr/wow/xelis libraries handling their // own mnemonic generation on new wallet creation // if its a restore we must set them @@ -238,10 +236,11 @@ abstract class Wallet { required NodeService nodeService, required Prefs prefs, }) async { - final walletInfo = await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .findFirst(); + final walletInfo = + await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); Logging.instance.i( "Wallet.load loading" @@ -270,10 +269,7 @@ abstract class Wallet { required EthereumWallet ethWallet, required EthContract contract, }) { - final Wallet wallet = EthTokenWallet( - ethWallet, - contract, - ); + final Wallet wallet = EthTokenWallet(ethWallet, contract); wallet.prefs = ethWallet.prefs; wallet.nodeService = ethWallet.nodeService; @@ -287,27 +283,19 @@ abstract class Wallet { // ========== Static Util ==================================================== // secure storage key - static String mnemonicKey({ - required String walletId, - }) => + static String mnemonicKey({required String walletId}) => "${walletId}_mnemonic"; // secure storage key - static String mnemonicPassphraseKey({ - required String walletId, - }) => + static String mnemonicPassphraseKey({required String walletId}) => "${walletId}_mnemonicPassphrase"; // secure storage key - static String privateKeyKey({ - required String walletId, - }) => + static String privateKeyKey({required String walletId}) => "${walletId}_privateKey"; // secure storage key - static String getViewOnlyWalletDataSecStoreKey({ - required String walletId, - }) => + static String getViewOnlyWalletDataSecStoreKey({required String walletId}) => "${walletId}_viewOnlyWalletData"; //============================================================================ @@ -321,9 +309,7 @@ abstract class Wallet { required NodeService nodeService, required Prefs prefs, }) async { - final Wallet wallet = _loadWallet( - walletInfo: walletInfo, - ); + final Wallet wallet = _loadWallet(walletInfo: walletInfo); wallet.prefs = prefs; wallet.nodeService = nodeService; @@ -339,9 +325,7 @@ abstract class Wallet { .._walletId = walletInfo.walletId; } - static Wallet _loadWallet({ - required WalletInfo walletInfo, - }) { + static Wallet _loadWallet({required WalletInfo walletInfo}) { final net = walletInfo.coin.network; switch (walletInfo.coin.runtimeType) { case const (Banano): @@ -374,6 +358,9 @@ abstract class Wallet { case const (Ethereum): return EthereumWallet(net); + case const (Fact0rn): + return Fact0rnWallet(net); + case const (Firo): return FiroWallet(net); @@ -395,6 +382,9 @@ abstract class Wallet { case const (Peercoin): return PeercoinWallet(net); + case const (Salvium): + return SalviumWallet(net); + case const (Solana): return SolanaWallet(net); @@ -421,12 +411,11 @@ abstract class Wallet { _periodicPingCheck(); // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); + _networkAliveTimer = Timer.periodic(Constants.networkAliveTimerDuration, ( + _, + ) async { + _periodicPingCheck(); + }); } void _periodicPingCheck() async { @@ -438,27 +427,28 @@ abstract class Wallet { final bool hasNetwork = await pingCheck(); if (_isConnected != hasNetwork) { - final NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - status, - walletId, - cryptoCurrency, - ), - ); + final NodeConnectionStatus status = + hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent(status, walletId, cryptoCurrency), + ); + } _isConnected = hasNetwork; if (status == NodeConnectionStatus.disconnected) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + cryptoCurrency, + ), + ); + } } if (hasNetwork) { @@ -499,7 +489,7 @@ abstract class Wallet { /// updates the wallet info's cachedChainHeight Future updateChainHeight(); - Future estimateFeeFor(Amount amount, int feeRate); + Future estimateFeeFor(Amount amount, BigInt feeRate); Future get fees; @@ -518,28 +508,33 @@ abstract class Wallet { } NodeModel getCurrentNode() { - final node = nodeService.getPrimaryNodeFor(currency: cryptoCurrency) ?? - cryptoCurrency.defaultNode; + final node = + nodeService.getPrimaryNodeFor(currency: cryptoCurrency) ?? + cryptoCurrency.defaultNode(isPrimary: true); return node; } + bool doNotFireRefreshEvents = false; + // Should fire events Future refresh() async { final refreshCompleter = Completer(); final future = refreshCompleter.future.then( (_) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - cryptoCurrency, - ), - ); - + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + cryptoCurrency, + ), + ); + } if (shouldAutoSync) { - _periodicRefreshTimer ??= - Timer.periodic(const Duration(seconds: 150), (timer) async { + _periodicRefreshTimer ??= Timer.periodic(const Duration(seconds: 150), ( + timer, + ) async { // chain height check currently broken // if ((await chainHeight) != (await storedChainHeight)) { @@ -553,20 +548,22 @@ abstract class Wallet { } }, onError: (Object e, StackTrace s) { - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - cryptoCurrency, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + cryptoCurrency, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + cryptoCurrency, + ), + ); + } Logging.instance.e( "Caught exception in refreshWalletData()", error: e, @@ -584,7 +581,11 @@ abstract class Wallet { if (this is ElectrumXInterface) { (this as ElectrumXInterface?)?.refreshingPercent = percent; } - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(percent, walletId)); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(percent, walletId), + ); + } } // Should fire events @@ -596,7 +597,8 @@ abstract class Wallet { } final start = DateTime.now(); - final viewOnly = this is ViewOnlyOptionInterface && + final viewOnly = + this is ViewOnlyOptionInterface && (this as ViewOnlyOptionInterface).isViewOnly; try { @@ -604,13 +606,15 @@ abstract class Wallet { // Slight possibility of race but should be irrelevant await refreshMutex.acquire(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + cryptoCurrency, + ), + ); + } // add some small buffer before making calls. // this can probably be removed in the future but was added as a @@ -621,8 +625,9 @@ abstract class Wallet { final Set codesToCheck = {}; if (this is PaynymInterface && !viewOnly) { // isSegwit does not matter here at all - final myCode = - await (this as PaynymInterface).getPaymentCode(isSegwit: false); + final myCode = await (this as PaynymInterface).getPaymentCode( + isSegwit: false, + ); final nym = await PaynymIsApi().nym(myCode.toString()); if (nym.value != null) { @@ -685,8 +690,9 @@ abstract class Wallet { // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) { - await (this as PaynymInterface) - .checkForNotificationTransactionsTo(codesToCheck); + await (this as PaynymInterface).checkForNotificationTransactionsTo( + codesToCheck, + ); // check utxos again for notification outputs await updateUTXOs(); } @@ -694,13 +700,6 @@ abstract class Wallet { // await getAllTxsToWatch(); - // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. - if (this is LelantusInterface && !viewOnly) { - if (info.otherData[WalletInfoKeys.enableLelantusScanning] as bool? ?? - false) { - await (this as LelantusInterface).refreshLelantusData(); - } - } _fireRefreshPercentChange(0.90); await updateBalance(); @@ -746,10 +745,11 @@ abstract class Wallet { // Check if there's another wallet of this coin on the sync list. final List walletIds = []; for (final id in prefs.walletIdsSyncOnStartup) { - final wallet = mainDB.isar.walletInfo - .where() - .walletIdEqualTo(id) - .findFirstSync()!; + final wallet = + mainDB.isar.walletInfo + .where() + .walletIdEqualTo(id) + .findFirstSync()!; if (wallet.coin == cryptoCurrency) { walletIds.add(id); @@ -802,17 +802,11 @@ abstract class Wallet { return await mainDB.isar.addresses .buildQuery
( whereClauses: [ - IndexWhereClause.equalTo( - indexName: r"walletId", - value: [walletId], - ), + IndexWhereClause.equalTo(indexName: r"walletId", value: [walletId]), ], filter: filterOperation, sortBy: [ - const SortProperty( - property: r"derivationIndex", - sort: Sort.desc, - ), + const SortProperty(property: r"derivationIndex", sort: Sort.desc), ], ) .findFirst(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart index 371236180..0f428ec89 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart @@ -2,11 +2,11 @@ import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/src/utils/network.dart' as bitbox_utils; import 'package:isar/isar.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/logger.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../models/tx_data.dart'; @@ -18,8 +18,10 @@ mixin BCashInterface @override Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required List inputsWithKeys, }) async { + final insAndKeys = inputsWithKeys.cast(); + Logging.instance.d("Starting buildTransaction ----------"); // TODO: use coinlib @@ -35,11 +37,8 @@ mixin BCashInterface final List tempOutputs = []; // Add transaction inputs - for (int i = 0; i < utxoSigningData.length; i++) { - builder.addInput( - utxoSigningData[i].utxo.txid, - utxoSigningData[i].utxo.vout, - ); + for (int i = 0; i < insAndKeys.length; i++) { + builder.addInput(insAndKeys[i].utxo.txid, insAndKeys[i].utxo.vout); tempInputs.add( InputV2.isarCantDoRequiredInDefaultConstructor( @@ -47,13 +46,14 @@ mixin BCashInterface scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: insAndKeys[i].utxo.txid, + vout: insAndKeys[i].utxo.vout, ), - addresses: utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + addresses: + insAndKeys[i].utxo.address == null + ? [] + : [insAndKeys[i].utxo.address!], + valueStringSats: insAndKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -73,10 +73,9 @@ mixin BCashInterface OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", valueStringSats: txData.recipients![i].amount.raw.toString(), - addresses: [ - txData.recipients![i].address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [txData.recipients![i].address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -92,9 +91,9 @@ mixin BCashInterface try { // Sign the transaction accordingly - for (int i = 0; i < utxoSigningData.length; i++) { + for (int i = 0; i < insAndKeys.length; i++) { final bitboxEC = bitbox.ECPair.fromPrivateKey( - utxoSigningData[i].keyPair!.privateKey.data, + insAndKeys[i].key!.privateKey!.data, network: bitbox_utils.Network( cryptoCurrency.networkParams.privHDPrefix, cryptoCurrency.networkParams.pubHDPrefix, @@ -103,18 +102,17 @@ mixin BCashInterface cryptoCurrency.networkParams.wifPrefix, cryptoCurrency.networkParams.p2pkhPrefix, ), - compressed: utxoSigningData[i].keyPair!.privateKey.compressed, + compressed: insAndKeys[i].key!.privateKey!.compressed, ); - builder.sign( - i, - bitboxEC, - utxoSigningData[i].utxo.value, - ); + builder.sign(i, bitboxEC, insAndKeys[i].utxo.value); } } catch (e, s) { - Logging.instance.e("Caught exception while signing transaction: ", - error: e, stackTrace: s); + Logging.instance.e( + "Caught exception while signing transaction: ", + error: e, + stackTrace: s, + ); rethrow; } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index e393ceb9d..e978a0bd7 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -4,18 +4,20 @@ import 'dart:typed_data'; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; +import 'package:meta/meta.dart'; +import '../../../db/drift/database.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/client_manager.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../models/coinlib/exp2pkh_address.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../models/paymint/fee_object_model.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; @@ -31,6 +33,7 @@ import '../impl/firo_wallet.dart'; import '../impl/peercoin_wallet.dart'; import '../intermediate/bip39_hd_wallet.dart'; import 'cpfp_interface.dart'; +import 'mweb_interface.dart'; import 'paynym_interface.dart'; import 'rbf_interface.dart'; import 'view_only_option_interface.dart'; @@ -75,29 +78,39 @@ mixin ElectrumXInterface return false; } - Future> - helperRecipientsConvert(List addrs, List satValues) async { - final List<({String address, Amount amount, bool isChange})> results = []; + Future> helperRecipientsConvert( + List addrs, + List satValues, + ) async { + final List results = []; for (int i = 0; i < addrs.length; i++) { - results.add(( - address: addrs[i], - amount: Amount( - rawValue: satValues[i], - fractionDigits: cryptoCurrency.fractionDigits, + // assume address is valid at this point so if getAddressType fails for + // some reason default to unknown + final type = + cryptoCurrency.getAddressType(addrs[i]) ?? AddressType.unknown; + + results.add( + TxRecipient( + address: addrs[i], + amount: Amount( + rawValue: satValues[i], + fractionDigits: cryptoCurrency.fractionDigits, + ), + isChange: + (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(AddressSubType.change) + .and() + .valueEqualTo(addrs[i]) + .valueProperty() + .findFirst()) != + null, + addressType: type, ), - isChange: - (await mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(AddressSubType.change) - .and() - .valueEqualTo(addrs[i]) - .valueProperty() - .findFirst()) != - null, - )); + ); } return results; @@ -109,7 +122,8 @@ mixin ElectrumXInterface required bool isSendAll, required bool isSendAllCoinControlUtxos, int additionalOutputs = 0, - List? utxos, + List? utxos, + BigInt? overrideFeeAmount, }) async { Logging.instance.d("Starting coinSelection ----------"); @@ -120,34 +134,64 @@ mixin ElectrumXInterface throw Exception("Coin control used where utxos is null!"); } + Future
changeAddress() async { + if (txData.type == TxType.mweb || txData.type == TxType.mwebPegOut) { + return (await (this as MwebInterface).getMwebChangeAddress())!; + } else { + return (await getCurrentChangeAddress())!; + } + } + final recipientAddress = txData.recipients!.first.address; final satoshiAmountToSend = txData.amount!.raw; final int? satsPerVByte = txData.satsPerVByte; final selectedTxFeeRate = txData.feeRateAmount!; - final List availableOutputs = - utxos ?? await mainDB.getUTXOs(walletId).findAll(); + final List availableOutputs; + + if (txData.type == TxType.mweb || txData.type == TxType.mwebPegOut) { + if (utxos == null) { + final db = Drift.get(walletId); + final mwebUtxos = + await (db.select(db.mwebUtxos) + ..where((e) => e.used.equals(false))).get(); + + availableOutputs = mwebUtxos.map((e) => MwebInput(e)).toList(); + } else { + availableOutputs = utxos; + } + } else { + availableOutputs = + utxos ?? + (await mainDB.getUTXOs(walletId).findAll()) + .map((e) => StandardInput(e)) + .toList(); + } + final currentChainHeight = await chainHeight; final canCPFP = this is CpfpInterface && coinControl; final spendableOutputs = - availableOutputs - .where( - (e) => - !e.isBlocked && - (e.used != true) && - (canCPFP || - e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - )), - ) - .toList(); + availableOutputs.where((e) { + if (e is StandardInput) { + return !e.utxo.isBlocked && + (e.utxo.used != true) && + (canCPFP || + e.utxo.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )); + } else if (e is MwebInput) { + return !e.utxo.blocked && !e.utxo.used; + } else { + return false; + } + }).toList(); final spendableSatoshiValue = spendableOutputs.fold( BigInt.zero, - (p, e) => p + BigInt.from(e.value), + (p, e) => p + e.value, ); if (spendableSatoshiValue < satoshiAmountToSend) { @@ -180,7 +224,7 @@ mixin ElectrumXInterface BigInt satoshisBeingUsed = BigInt.zero; int inputsBeingConsumed = 0; - final List utxoObjectsToUse = []; + final List utxoObjectsToUse = []; if (!coinControl) { for ( @@ -189,7 +233,7 @@ mixin ElectrumXInterface i++ ) { utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += BigInt.from(spendableOutputs[i].value); + satoshisBeingUsed += spendableOutputs[i].value; inputsBeingConsumed += 1; } for ( @@ -198,9 +242,7 @@ mixin ElectrumXInterface i++ ) { utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += BigInt.from( - spendableOutputs[inputsBeingConsumed].value, - ); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; inputsBeingConsumed += 1; } } else { @@ -218,23 +260,37 @@ mixin ElectrumXInterface final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final inputsWithKeys = await addSigningKeys(utxoObjectsToUse); if (isSendAll || isSendAllCoinControlUtxos) { - if (satoshiAmountToSend != satoshisBeingUsed) { - throw Exception( - "Something happened that should never actually happen. " - "Please report this error to the developers.", + if ((overrideFeeAmount ?? BigInt.zero) + satoshiAmountToSend != + satoshisBeingUsed) { + Logging.instance.d("txData.type: ${txData.type}"); + Logging.instance.d("isSendAll: $isSendAll"); + Logging.instance.d( + "isSendAllCoinControlUtxos: $isSendAllCoinControlUtxos", ); + Logging.instance.d("overrideFeeAmount: $overrideFeeAmount"); + Logging.instance.d("satoshiAmountToSend: $satoshiAmountToSend"); + Logging.instance.d("satoshisBeingUsed: $satoshisBeingUsed"); + + // hack check + if (!(txData.type == TxType.mwebPegIn || + (txData.type == TxType.mweb && overrideFeeAmount != null))) { + throw Exception( + "Something happened that should never actually happen. " + "Please report this error to the developers.", + ); + } } return await _sendAllBuilder( txData: txData, recipientAddress: recipientAddress, - satoshiAmountToSend: satoshiAmountToSend, satoshisBeingUsed: satoshisBeingUsed, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, satsPerVByte: satsPerVByte, feeRatePerKB: selectedTxFeeRate, + overrideFeeAmount: overrideFeeAmount, ); } @@ -242,7 +298,7 @@ mixin ElectrumXInterface try { vSizeForOneOutput = (await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( [recipientAddress], @@ -262,10 +318,10 @@ mixin ElectrumXInterface try { vSizeForTwoOutPuts = (await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( - [recipientAddress, (await getCurrentChangeAddress())!.value], + [recipientAddress, (await changeAddress()).value], [ satoshiAmountToSend, maxBI( @@ -282,23 +338,27 @@ mixin ElectrumXInterface } // Assume 1 output, only for recipient and no change - final feeForOneOutput = BigInt.from( - satsPerVByte != null - ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ), - ); + final feeForOneOutput = + overrideFeeAmount ?? + BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForOneOutput) + : estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ), + ); // Assume 2 outputs, one for recipient and one for change - final feeForTwoOutputs = BigInt.from( - satsPerVByte != null - ? (satsPerVByte * vSizeForTwoOutPuts) - : estimateTxFee( - vSize: vSizeForTwoOutPuts, - feeRatePerKB: selectedTxFeeRate, - ), - ); + final feeForTwoOutputs = + overrideFeeAmount ?? + BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForTwoOutPuts) + : estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), + ); Logging.instance.d("feeForTwoOutputs: $feeForTwoOutputs"); Logging.instance.d("feeForOneOutput: $feeForOneOutput"); @@ -311,7 +371,7 @@ mixin ElectrumXInterface Logging.instance.d('Fee being paid: $difference sats'); Logging.instance.d('Estimated fee: $feeForOneOutput'); final txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, @@ -324,7 +384,7 @@ mixin ElectrumXInterface rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } @@ -354,15 +414,17 @@ mixin ElectrumXInterface // check if possible to add the change output if (changeOutputSize > cryptoCurrency.dustLimit.raw && difference - changeOutputSize == feeForTwoOutputs) { - // generate new change address if current change address has been used - await checkChangeAddressForTransactions(); - final String newChangeAddress = - (await getCurrentChangeAddress())!.value; + if (!(txData.type == TxType.mweb || + txData.type == TxType.mwebPegOut)) { + // generate new change address if current change address has been used + await checkChangeAddressForTransactions(); + } + final newChangeAddress = await changeAddress(); BigInt feeBeingPaid = difference - changeOutputSize; // add change output - recipientsArray.add(newChangeAddress); + recipientsArray.add(newChangeAddress.value); recipientsAmtArray.add(changeOutputSize); Logging.instance.d('2 outputs in tx'); @@ -373,12 +435,13 @@ mixin ElectrumXInterface Logging.instance.d('Estimated fee: $feeForTwoOutputs'); TxData txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), + usedUTXOs: inputsWithKeys, ), ); @@ -402,12 +465,13 @@ mixin ElectrumXInterface Logging.instance.d('Adjusted Estimated fee: $feeForTwoOutputs'); txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), + usedUTXOs: inputsWithKeys, ), ); } @@ -417,7 +481,7 @@ mixin ElectrumXInterface rawValue: feeBeingPaid, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize @@ -435,44 +499,52 @@ mixin ElectrumXInterface Future _sendAllBuilder({ required TxData txData, required String recipientAddress, - required BigInt satoshiAmountToSend, required BigInt satoshisBeingUsed, - required List utxoSigningData, + required List inputsWithKeys, required int? satsPerVByte, - required int feeRatePerKB, + required BigInt feeRatePerKB, + BigInt? overrideFeeAmount, }) async { Logging.instance.d("Attempting to send all $cryptoCurrency"); if (txData.recipients!.length != 1) { throw Exception("Send all to more than one recipient not yet supported"); } - final int vSizeForOneOutput = - (await buildTransaction( - utxoSigningData: utxoSigningData, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed - BigInt.one], + BigInt feeForOneOutput; + if (overrideFeeAmount == null) { + final int vSizeForOneOutput = + (await buildTransaction( + inputsWithKeys: inputsWithKeys, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed - BigInt.one], + ), ), - ), - )).vSize!; - BigInt feeForOneOutput = BigInt.from( - satsPerVByte != null - ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee(vSize: vSizeForOneOutput, feeRatePerKB: feeRatePerKB), - ); + )).vSize!; + feeForOneOutput = BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForOneOutput) + : estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: feeRatePerKB, + ), + ); - if (satsPerVByte == null) { - final roughEstimate = - roughFeeEstimate(utxoSigningData.length, 1, feeRatePerKB).raw; - if (feeForOneOutput < roughEstimate) { - feeForOneOutput = roughEstimate; + if (satsPerVByte == null) { + final roughEstimate = + roughFeeEstimate(inputsWithKeys.length, 1, feeRatePerKB).raw; + if (feeForOneOutput < roughEstimate) { + feeForOneOutput = roughEstimate; + } } + } else { + feeForOneOutput = overrideFeeAmount; } - final amount = satoshiAmountToSend - feeForOneOutput; + final satoshiAmountToSend = satoshisBeingUsed - feeForOneOutput; - if (amount.isNegative) { + if (satoshiAmountToSend.isNegative) { throw Exception( "Estimated fee ($feeForOneOutput sats) is greater than balance!", ); @@ -480,9 +552,12 @@ mixin ElectrumXInterface final data = await buildTransaction( txData: txData.copyWith( - recipients: await helperRecipientsConvert([recipientAddress], [amount]), + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshiAmountToSend], + ), ), - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, ); return data.copyWith( @@ -490,29 +565,36 @@ mixin ElectrumXInterface rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } - Future> fetchBuildTxData(List utxosToUse) async { + Future> addSigningKeys(List utxosToUse) async { // return data - final List signingData = []; + final List inputsWithKeys = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final derivePathType = cryptoCurrency.addressType( - address: utxosToUse[i].address!, - ); + final input = utxosToUse[i]; + if (input is MwebInput) { + inputsWithKeys.add(input); + } else if (input is StandardInput) { + final derivePathType = cryptoCurrency.addressType( + address: input.address!, + ); - signingData.add( - SigningData(derivePathType: derivePathType, utxo: utxosToUse[i]), - ); + inputsWithKeys.add( + StandardInput(input.utxo, derivePathType: derivePathType), + ); + } else { + throw Exception("Unknown input type ${input.runtimeType}"); + } } final root = await getRootHDNode(); - for (final sd in signingData) { + for (final sd in inputsWithKeys.whereType()) { coinlib.HDPrivateKey? keys; final address = await mainDB.getAddress(walletId, sd.utxo.address!); if (address?.derivationPath != null) { @@ -551,10 +633,10 @@ mixin ElectrumXInterface ); } - sd.keyPair = keys; + sd.key = keys; } - return signingData; + return inputsWithKeys; } catch (e, s) { Logging.instance.e("fetchBuildTxData() threw", error: e, stackTrace: s); rethrow; @@ -564,7 +646,7 @@ mixin ElectrumXInterface /// Builds and signs a transaction Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required List inputsWithKeys, }) async { Logging.instance.d("Starting buildTransaction ----------"); @@ -575,7 +657,7 @@ mixin ElectrumXInterface final List prevOuts = []; coinlib.Transaction clTx = coinlib.Transaction( - version: cryptoCurrency.transactionVersion, + version: txData.type.isMweb() ? 2 : cryptoCurrency.transactionVersion, inputs: [], outputs: [], ); @@ -586,86 +668,131 @@ mixin ElectrumXInterface ? 0xffffffff - 10 : 0xffffffff - 1; + bool isMweb = false; + bool hasNonWitnessInput = false; + // Add transaction inputs - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < inputsWithKeys.length; i++) { + final data = inputsWithKeys[i]; + if (data is MwebInput) { + isMweb = true; + final address = data.address; + + final addr = await mainDB.getAddress(walletId, address); + final index = addr!.derivationIndex; + + final input = coinlib.RawInput( + prevOut: coinlib.OutPoint( + Uint8List.fromList( + data.utxo.outputId.toUint8ListFromHex.reversed.toList(), + ), + index, + ), + scriptSig: Uint8List(0), + ); - final hash = Uint8List.fromList( - txid.toUint8ListFromHex.reversed.toList(), - ); + clTx = clTx.addInput(input); - final prevOutpoint = coinlib.OutPoint(hash, utxoSigningData[i].utxo.vout); + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: input.scriptSig.toHex, + scriptSigAsm: null, + sequence: sequence, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: data.utxo.outputId, + vout: index, + ), + addresses: [address], + valueStringSats: inputsWithKeys[i].value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + } else if (data is StandardInput) { + final txid = data.utxo.txid; - final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), - coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, - cryptoCurrency.networkParams, - ), - ); + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), + ); + + final prevOutpoint = coinlib.OutPoint(hash, data.utxo.vout); + + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(data.utxo.value), + coinlib.Address.fromString( + data.utxo.address!, + cryptoCurrency.networkParams, + ), + ); - prevOuts.add(prevOutput); + prevOuts.add(prevOutput); - final coinlib.Input input; + 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: sequence, - ); + switch (data.derivePathType) { + case DerivePathType.bip44: + case DerivePathType.bch44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + publicKey: data.key!.publicKey, + sequence: sequence, + ); - // TODO: fix this as it is (probably) wrong! - case DerivePathType.bip49: - throw Exception("TODO p2sh"); - // input = coinlib.P2SHMultisigInput( - // prevOut: prevOutpoint, - // program: coinlib.MultisigProgram.decompile( - // utxoSigningData[i].redeemScript!, - // ), - // sequence: sequence, - // ); - - case DerivePathType.bip84: - input = coinlib.P2WPKHInput( - prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, - sequence: sequence, - ); + // TODO: fix this as it is (probably) wrong! + case DerivePathType.bip49: + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // data.redeemScript!, + // ), + // sequence: sequence, + // ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + publicKey: data.key!.publicKey, + sequence: sequence, + ); - case DerivePathType.bip86: - input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); - default: - throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", - ); - } + default: + throw UnsupportedError( + "Unknown derivation path type found: ${data.derivePathType}", + ); + } + + if (input is! coinlib.WitnessInput) { + hasNonWitnessInput = true; + } - clTx = clTx.addInput(input); + clTx = clTx.addInput(input); - tempInputs.add( - InputV2.isarCantDoRequiredInDefaultConstructor( - scriptSigHex: input.scriptSig.toHex, - scriptSigAsm: null, - sequence: sequence, - outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: input.scriptSig.toHex, + scriptSigAsm: null, + sequence: sequence, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: data.utxo.txid, + vout: data.utxo.vout, + ), + addresses: data.utxo.address == null ? [] : [data.utxo.address!], + valueStringSats: data.utxo.value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, ), - addresses: - utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), - witness: null, - innerRedeemScriptAsm: null, - coinbase: null, - walletOwns: true, - ), - ); + ); + } else { + throw Exception("Unknown input type: ${inputsWithKeys[i].runtimeType}"); + } } // Add transaction output @@ -687,10 +814,19 @@ mixin ElectrumXInterface rethrow; } } - final output = coinlib.Output.fromAddress( - txData.recipients![i].amount.raw, - address, - ); + final coinlib.Output output; + if (address is coinlib.MwebAddress) { + isMweb = true; + output = coinlib.Output.fromProgram( + txData.recipients![i].amount.raw, + address.program, + ); + } else { + output = coinlib.Output.fromAddress( + txData.recipients![i].amount.raw, + address, + ); + } clTx = clTx.addOutput(output); @@ -712,35 +848,48 @@ mixin ElectrumXInterface ); } + if (isMweb) { + if (hasNonWitnessInput) { + throw Exception("Found non witness input in mweb tx"); + } + } + try { // Sign the transaction accordingly - for (var i = 0; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; - - if (clTx.inputs[i] is coinlib.TaprootKeyInput) { - final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, - ); - - clTx = clTx.signTaproot( - inputN: i, - key: taproot.tweakPrivateKey(key), - prevOuts: prevOuts, - ); - } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { - clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); - } else if (clTx.inputs[i] is coinlib.LegacyInput) { - clTx = clTx.signLegacy(inputN: i, key: key); - } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { - clTx = clTx.signTaprootSingleScriptSig( - inputN: i, - key: key, - prevOuts: prevOuts, - ); + for (var i = 0; i < inputsWithKeys.length; i++) { + final data = inputsWithKeys[i]; + + if (data is MwebInput) { + // do nothing + } else if (data is StandardInput) { + final value = BigInt.from(data.utxo.value); + final key = data.key!.privateKey!; + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot(internalKey: data.key!.publicKey); + + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is coinlib.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); + } } else { throw Exception( - "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + "Unknown input type: ${inputsWithKeys[i].runtimeType}", ); } } @@ -757,24 +906,29 @@ mixin ElectrumXInterface raw: clTx.toHex(), // dirty shortcut for peercoin's weirdness vSize: this is PeercoinWallet ? clTx.size : clTx.vSize(), - tempTx: TransactionV2( - walletId: walletId, - blockHash: null, - hash: clTx.hashHex, - txid: clTx.txid, - height: null, - timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, - inputs: List.unmodifiable(tempInputs), - outputs: List.unmodifiable(tempOutputs), - version: clTx.version, - type: - tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) && - txData.paynymAccountLite == null - ? TransactionType.sentToSelf - : TransactionType.outgoing, - subType: TransactionSubType.none, - otherData: null, - ), + tempTx: + txData.type.isMweb() + ? null + : TransactionV2( + walletId: walletId, + blockHash: null, + hash: clTx.hashHex, + txid: clTx.txid, + height: null, + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + version: clTx.version, + type: + tempOutputs + .map((e) => e.walletOwns) + .fold(true, (p, e) => p &= e) && + txData.paynymAccountLite == null + ? TransactionType.sentToSelf + : TransactionType.outgoing, + subType: TransactionSubType.none, + otherData: null, + ), ); } @@ -1225,17 +1379,17 @@ mixin ElectrumXInterface Amount.fromDecimal( fast, fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), + ).raw, medium: Amount.fromDecimal( medium, fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), + ).raw, slow: Amount.fromDecimal( slow, fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), + ).raw, ); Logging.instance.d("fetched fees: $feeObject"); @@ -1256,7 +1410,7 @@ mixin ElectrumXInterface } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { final available = info.cachedBalance.spendable; final utxos = _spendableUTXOs(await mainDB.getUTXOs(walletId).findAll()); @@ -1655,14 +1809,30 @@ mixin ElectrumXInterface txData = txData.copyWith( usedUTXOs: - txData.usedUTXOs!.map((e) => e.copyWith(used: true)).toList(), + txData.usedUTXOs!.map((e) { + if (e is StandardInput) { + return StandardInput( + e.utxo.copyWith(used: true), + derivePathType: e.derivePathType, + ); + } else if (e is MwebInput) { + return MwebInput(e.utxo.copyWith(used: true)); + } else { + return e; + } + }).toList(), // TODO revisit setting these both txHash: txHash, txid: txHash, ); // mark utxos as used - await mainDB.putUTXOs(txData.usedUTXOs!); + await mainDB.putUTXOs( + txData.usedUTXOs! + .whereType() + .map((e) => e.utxo) + .toList(), + ); return await updateSentCachedTxData(txData: txData); } catch (e, s) { @@ -1682,57 +1852,51 @@ mixin ElectrumXInterface throw Exception("No recipients in attempted transaction!"); } + final balance = + txData.type == TxType.mweb || txData.type == TxType.mwebPegOut + ? info.cachedBalanceSecondary + : info.cachedBalance; final feeRateType = txData.feeRateType; final customSatsPerVByte = txData.satsPerVByte; final feeRateAmount = txData.feeRateAmount; final utxos = txData.utxos; + bool isSendAll = false; + final bool coinControl = utxos != null; final isSendAllCoinControlUtxos = coinControl && txData.amount!.raw == - utxos - .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)); + utxos.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); + + final TxData result; if (customSatsPerVByte != null) { // check for send all - bool isSendAll = false; + isSendAll = false; if (txData.ignoreCachedBalanceChecks || - txData.amount == info.cachedBalance.spendable) { + txData.amount == balance.spendable) { isSendAll = true; } if (coinControl && this is CpfpInterface && - txData.amount == - (info.cachedBalance.spendable + - info.cachedBalance.pendingSpendable)) { + txData.amount == (balance.spendable + balance.pendingSpendable)) { isSendAll = true; } - final result = await coinSelection( - txData: txData.copyWith(feeRateAmount: -1), + result = await coinSelection( + txData: txData.copyWith(feeRateAmount: BigInt.from(-1)), isSendAll: isSendAll, utxos: utxos?.toList(), coinControl: coinControl, isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, ); - - Logging.instance.d("PREPARE SEND RESULT: $result"); - - if (result.fee!.raw.toInt() < result.vSize!) { - throw Exception( - "Error in fee calculation: Transaction fee cannot be less than vSize", - ); - } - - return result; - } else if (feeRateType is FeeRateType || feeRateAmount is int) { - late final int rate; + } else if (feeRateType is FeeRateType || feeRateAmount is BigInt) { + late final BigInt rate; if (feeRateType is FeeRateType) { - int fee = 0; + BigInt fee = BigInt.zero; final feeObject = await fees; switch (feeRateType) { case FeeRateType.fast: @@ -1749,35 +1913,63 @@ mixin ElectrumXInterface } rate = fee; } else { - rate = feeRateAmount as int; + rate = feeRateAmount!; } // check for send all - bool isSendAll = false; - if (txData.amount == info.cachedBalance.spendable) { + isSendAll = false; + if (txData.amount == balance.spendable) { isSendAll = true; } - final result = await coinSelection( + result = await coinSelection( txData: txData.copyWith(feeRateAmount: rate), isSendAll: isSendAll, utxos: utxos?.toList(), coinControl: coinControl, isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, ); + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } - Logging.instance.d("prepare send: $result"); - if (result.fee!.raw.toInt() < result.vSize!) { - throw Exception( - "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " - "be less than vSize (${result.vSize})", + if (result.fee!.raw.toInt() < result.vSize!) { + throw Exception( + "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " + "be less than vSize (${result.vSize})", + ); + } + + // mweb + if (result.type.isMweb()) { + final fee = await (this as MwebInterface).mwebFee(txData: result); + + TxData mwebData = await coinSelection( + txData: result.copyWith( + recipients: result.recipients!.where((e) => !(e.isChange)).toList(), + ), + utxos: utxos?.toList(), + coinControl: coinControl, + isSendAll: isSendAll, + isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, + overrideFeeAmount: fee.raw, + ); + + if (mwebData.type == TxType.mwebPegIn) { + mwebData = await buildTransaction( + txData: mwebData, + inputsWithKeys: mwebData.usedUTXOs!, ); } - - return result; - } else { - throw ArgumentError("Invalid fee rate argument provided!"); + final data = await (this as MwebInterface).processMwebTransaction( + mwebData, + ); + return data.copyWith(fee: fee); } + + Logging.instance.d("prepare send: $result"); + + return result; } catch (e, s) { Logging.instance.e( "Exception rethrown from prepareSend(): ", @@ -1788,6 +1980,7 @@ mixin ElectrumXInterface } } + @mustCallSuper @override Future init() async { try { @@ -1832,8 +2025,8 @@ mixin ElectrumXInterface // =========================================================================== // ========== Interface functions ============================================ - int estimateTxFee({required int vSize, required int feeRatePerKB}); - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}); + Amount roughFeeEstimate(int inputCount, int outputCount, BigInt feeRatePerKB); Future> fetchAddressesForElectrumXScan(); @@ -1864,7 +2057,10 @@ mixin ElectrumXInterface .toList(); } - Future _sweepAllEstimate(int feeRate, List usableUTXOs) async { + Future _sweepAllEstimate( + BigInt feeRate, + List usableUTXOs, + ) async { final available = usableUTXOs .map((e) => BigInt.from(e.value)) .fold(BigInt.zero, (p, e) => p + e); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart deleted file mode 100644 index 148bd61a2..000000000 --- a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart +++ /dev/null @@ -1,1230 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; -import 'package:decimal/decimal.dart'; -import 'package:isar/isar.dart'; -import 'package:lelantus/lelantus.dart' as lelantus; -import 'package:tuple/tuple.dart'; - -import '../../../models/balance.dart'; -import '../../../models/isar/models/isar_models.dart'; -import '../../../models/lelantus_fee_data.dart'; -import '../../../utilities/amount/amount.dart'; -import '../../../utilities/enums/derive_path_type_enum.dart'; -import '../../../utilities/extensions/impl/uint8_list.dart'; -import '../../../utilities/format.dart'; -import '../../../utilities/logger.dart'; -import '../../api/lelantus_ffi_wrapper.dart'; -import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; -import '../../models/tx_data.dart'; -import '../intermediate/bip39_hd_wallet.dart'; -import 'electrumx_interface.dart'; - -mixin LelantusInterface - on Bip39HDWallet, ElectrumXInterface { - Future estimateFeeForLelantus(Amount amount) async { - final lelantusEntries = await _getLelantusEntry(); - final int spendAmount = amount.raw.toInt(); - if (spendAmount == 0 || lelantusEntries.isEmpty) { - return Amount( - rawValue: BigInt.from(LelantusFeeData(0, 0, []).fee), - fractionDigits: cryptoCurrency.fractionDigits, - ); - } - - final result = await LelantusFfiWrapper.estimateJoinSplitFee( - spendAmount: amount, - subtractFeeFromAmount: true, - lelantusEntries: lelantusEntries, - isTestNet: cryptoCurrency.network.isTestNet, - ); - - return Amount( - rawValue: BigInt.from(result.fee), - fractionDigits: cryptoCurrency.fractionDigits, - ); - } - - Future> _getLelantusEntry() async { - final List lelantusCoins = await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .isUsedEqualTo(false) - .not() - .group( - (q) => q - .valueEqualTo("0") - .or() - .anonymitySetIdEqualTo(LelantusFfiWrapper.ANONYMITY_SET_EMPTY_ID), - ) - .findAll(); - - final root = await getRootHDNode(); - - final waitLelantusEntries = lelantusCoins.map((coin) async { - final derivePath = cryptoCurrency.constructDerivePath( - derivePathType: DerivePathType.bip44, - chain: LelantusFfiWrapper.MINT_INDEX, - index: coin.mintIndex, - ); - - try { - final keyPair = root.derivePath(derivePath); - final String privateKey = keyPair.privateKey.data.toHex; - return lelantus.DartLelantusEntry( - coin.isUsed ? 1 : 0, - 0, - coin.anonymitySetId, - int.parse(coin.value), - coin.mintIndex, - privateKey, - ); - } catch (e, s) { - Logging.instance.e("error bad key"); - Logging.instance.t("error bad key", error: e, stackTrace: s); - return lelantus.DartLelantusEntry(1, 0, 0, 0, 0, ''); - } - }).toList(); - - final lelantusEntries = await Future.wait(waitLelantusEntries); - - if (lelantusEntries.isNotEmpty) { - // should be redundant as _getUnspentCoins() should - // already remove all where value=0 - lelantusEntries.removeWhere((element) => element.amount == 0); - } - - return lelantusEntries; - } - - Future prepareSendLelantus({ - required TxData txData, - }) async { - if (txData.recipients!.length != 1) { - throw Exception( - "Lelantus send requires a single recipient", - ); - } - - if (txData.recipients!.first.amount.raw > - BigInt.from(LelantusFfiWrapper.MINT_LIMIT)) { - throw Exception( - "Lelantus sends of more than 5001 are currently disabled", - ); - } - - try { - // check for send all - bool isSendAll = false; - final balance = info.cachedBalanceSecondary.spendable; - if (txData.recipients!.first.amount == balance) { - // print("is send all"); - isSendAll = true; - } - - final lastUsedIndex = - await mainDB.getHighestUsedMintIndex(walletId: walletId); - final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; - - final root = await getRootHDNode(); - - final derivePath = cryptoCurrency.constructDerivePath( - derivePathType: DerivePathType.bip44, - chain: 0, - index: 0, - ); - final partialDerivationPath = derivePath.substring( - 0, - derivePath.length - 3, - ); - - final result = await LelantusFfiWrapper.createJoinSplitTransaction( - txData: txData, - subtractFeeFromAmount: isSendAll, - nextFreeMintIndex: nextFreeMintIndex, - locktime: await chainHeight, - lelantusEntries: await _getLelantusEntry(), - anonymitySets: await fetchAnonymitySets(), - cryptoCurrency: cryptoCurrency, - partialDerivationPath: partialDerivationPath, - hexRootPrivateKey: root.privateKey.data.toHex, - chaincode: root.chaincode, - ); - - Logging.instance.d("prepared fee: ${result.fee}"); - Logging.instance.d("prepared vSize: ${result.vSize}"); - - // fee should never be less than vSize sanity check - if (result.fee!.raw.toInt() < result.vSize!) { - throw Exception( - "Error in fee calculation: Transaction fee cannot be less than vSize", - ); - } - return result; - } catch (e, s) { - Logging.instance.e( - "Exception rethrown in firo prepareSend()", - error: e, - stackTrace: s, - ); - rethrow; - } - } - - Future confirmSendLelantus({ - required TxData txData, - }) async { - final latestSetId = await electrumXClient.getLelantusLatestCoinId(); - final txid = await electrumXClient.broadcastTransaction( - rawTx: txData.raw!, - ); - - assert(txid == txData.txid!); - - final lastUsedIndex = - await mainDB.getHighestUsedMintIndex(walletId: walletId); - final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; - - if (txData.spendCoinIndexes != null) { - // This is a joinsplit - - final spentCoinIndexes = txData.spendCoinIndexes!; - final List updatedCoins = []; - - // Update all of the coins that have been spent. - - for (final index in spentCoinIndexes) { - final possibleCoin = await mainDB.isar.lelantusCoins - .where() - .mintIndexWalletIdEqualTo(index, walletId) - .findFirst(); - - if (possibleCoin != null) { - updatedCoins.add(possibleCoin.copyWith(isUsed: true)); - } - } - - // if a jmint was made add it to the unspent coin index - final jmint = LelantusCoin( - walletId: walletId, - mintIndex: nextFreeMintIndex, - value: (txData.jMintValue ?? 0).toString(), - txid: txid, - anonymitySetId: latestSetId, - isUsed: false, - isJMint: true, - otherData: null, - ); - - try { - await mainDB.isar.writeTxn(() async { - for (final c in updatedCoins) { - await mainDB.isar.lelantusCoins.deleteByMintIndexWalletId( - c.mintIndex, - c.walletId, - ); - } - await mainDB.isar.lelantusCoins.putAll(updatedCoins); - - await mainDB.isar.lelantusCoins.put(jmint); - }); - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - rethrow; - } - - final amount = txData.amount!; - - // add the send transaction - final transaction = Transaction( - walletId: walletId, - txid: txid, - timestamp: (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: TransactionType.outgoing, - subType: TransactionSubType.join, - amount: amount.raw.toInt(), - amountString: amount.toJsonString(), - fee: txData.fee!.raw.toInt(), - height: txData.height, - isCancelled: false, - isLelantus: true, - slateId: null, - nonce: null, - otherData: null, - // otherData: transactionInfo["otherData"] as String?, - inputs: [], - outputs: [], - numberOfMessages: null, - ); - - final transactionAddress = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(txData.recipients!.first.address) - .findFirst() ?? - Address( - walletId: walletId, - value: txData.recipients!.first.address, - derivationIndex: -1, - derivationPath: null, - type: AddressType.nonWallet, - subType: AddressSubType.nonWallet, - publicKey: [], - ); - - final List> txnsData = []; - - txnsData.add(Tuple2(transaction, transactionAddress)); - - await mainDB.addNewTransactionData(txnsData, walletId); - } else { - // This is a mint - Logging.instance.t("this is a mint"); - - final List updatedCoins = []; - - for (final mintMap in txData.mintsMapLelantus!) { - final index = mintMap['index'] as int; - final mint = LelantusCoin( - walletId: walletId, - mintIndex: index, - value: (mintMap['value'] as int).toString(), - txid: txid, - anonymitySetId: latestSetId, - isUsed: false, - isJMint: false, - otherData: null, - ); - - updatedCoins.add(mint); - } - // Logging.instance.log(coins); - try { - await mainDB.isar.writeTxn(() async { - await mainDB.isar.lelantusCoins.putAll(updatedCoins); - }); - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - rethrow; - } - } - - return txData.copyWith( - txid: txid, - ); - } - - Future>> fastFetch(List allTxHashes) async { - final List> allTransactions = []; - - const futureLimit = 30; - final List>> transactionFutures = []; - int currentFutureCount = 0; - for (final txHash in allTxHashes) { - final Future> transactionFuture = - electrumXCachedClient.getTransaction( - txHash: txHash, - verbose: true, - cryptoCurrency: cryptoCurrency, - ); - transactionFutures.add(transactionFuture); - currentFutureCount++; - if (currentFutureCount > futureLimit) { - currentFutureCount = 0; - await Future.wait(transactionFutures); - for (final fTx in transactionFutures) { - final tx = await fTx; - // delete unused large parts - tx.remove("hex"); - tx.remove("lelantusData"); - - allTransactions.add(tx); - } - } - } - if (currentFutureCount != 0) { - currentFutureCount = 0; - await Future.wait(transactionFutures); - for (final fTx in transactionFutures) { - final tx = await fTx; - // delete unused large parts - tx.remove("hex"); - tx.remove("lelantusData"); - - allTransactions.add(tx); - } - } - return allTransactions; - } - - Future> getJMintTransactions( - List transactions, - ) async { - try { - final Map txs = {}; - final List> allTransactions = - await fastFetch(transactions); - - for (int i = 0; i < allTransactions.length; i++) { - try { - final tx = allTransactions[i]; - - var sendIndex = 1; - if (tx["vout"][0]["value"] != null && - Decimal.parse(tx["vout"][0]["value"].toString()) > Decimal.zero) { - sendIndex = 0; - } - tx["amount"] = tx["vout"][sendIndex]["value"]; - tx["address"] = tx["vout"][sendIndex]["scriptPubKey"]["addresses"][0]; - tx["fees"] = tx["vin"][0]["nFees"]; - - final Amount amount = Amount.fromDecimal( - Decimal.parse(tx["amount"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ); - - final txn = Transaction( - walletId: walletId, - txid: tx["txid"] as String, - timestamp: tx["time"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: TransactionType.outgoing, - subType: TransactionSubType.join, - amount: amount.raw.toInt(), - amountString: amount.toJsonString(), - fee: Amount.fromDecimal( - Decimal.parse(tx["fees"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ).raw.toInt(), - height: tx["height"] as int?, - isCancelled: false, - isLelantus: true, - slateId: null, - otherData: null, - nonce: null, - inputs: [], - outputs: [], - numberOfMessages: null, - ); - - final address = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(tx["address"] as String) - .findFirst() ?? - Address( - walletId: walletId, - value: tx["address"] as String, - derivationIndex: -2, - derivationPath: null, - type: AddressType.nonWallet, - subType: AddressSubType.unknown, - publicKey: [], - ); - - txs[address] = txn; - } catch (e, s) { - Logging.instance.i( - "Exception caught in getJMintTransactions(): ", - error: e, - stackTrace: s, - ); - rethrow; - } - } - return txs; - } catch (e, s) { - Logging.instance.i( - "Exception rethrown in getJMintTransactions(): ", - error: e, - stackTrace: s, - ); - rethrow; - } - } - - Future>> fetchAnonymitySets() async { - try { - final latestSetId = await electrumXClient.getLelantusLatestCoinId(); - - final List> sets = []; - final List>> anonFutures = []; - for (int i = 1; i <= latestSetId; i++) { - final set = electrumXCachedClient.getAnonymitySet( - groupId: "$i", - cryptoCurrency: info.coin, - ); - anonFutures.add(set); - } - await Future.wait(anonFutures); - for (int i = 1; i <= latestSetId; i++) { - final Map set = (await anonFutures[i - 1]); - set["setId"] = i; - sets.add(set); - } - return sets; - } catch (e, s) { - Logging.instance.e( - "Exception rethrown from refreshAnonymitySets: ", - error: e, - stackTrace: s, - ); - rethrow; - } - } - - Future> getSetDataMap(int latestSetId) async { - final Map setDataMap = {}; - final anonymitySets = await fetchAnonymitySets(); - for (int setId = 1; setId <= latestSetId; setId++) { - final setData = anonymitySets - .firstWhere((element) => element["setId"] == setId, orElse: () => {}); - - if (setData.isNotEmpty) { - setDataMap[setId] = setData; - } - } - return setDataMap; - } - - // TODO: verify this function does what we think it does - Future refreshLelantusData() async { - final lelantusCoins = await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .isUsedEqualTo(false) - .not() - .valueEqualTo(0.toString()) - .findAll(); - - final List updatedCoins = []; - - final usedSerialNumbersSet = - (await electrumXCachedClient.getUsedCoinSerials( - cryptoCurrency: info.coin, - )) - .toSet(); - - final root = await getRootHDNode(); - - for (final coin in lelantusCoins) { - final _derivePath = cryptoCurrency.constructDerivePath( - derivePathType: DerivePathType.bip44, - chain: LelantusFfiWrapper.MINT_INDEX, - index: coin.mintIndex, - ); - - final mintKeyPair = root.derivePath(_derivePath); - - final String serialNumber = lelantus.GetSerialNumber( - int.parse(coin.value), - mintKeyPair.privateKey.data.toHex, - coin.mintIndex, - isTestnet: cryptoCurrency.network.isTestNet, - ); - final bool isUsed = usedSerialNumbersSet.contains(serialNumber); - - if (isUsed) { - updatedCoins.add(coin.copyWith(isUsed: isUsed)); - } - - final tx = await mainDB.getTransaction(walletId, coin.txid); - if (tx == null) { - Logging.instance.e( - "Transaction with txid=REDACTED not found in local db!", - ); - Logging.instance.d( - "Transaction with txid=${coin.txid} not found in local db!", - ); - } - } - - if (updatedCoins.isNotEmpty) { - try { - await mainDB.isar.writeTxn(() async { - for (final c in updatedCoins) { - await mainDB.isar.lelantusCoins.deleteByMintIndexWalletId( - c.mintIndex, - c.walletId, - ); - } - await mainDB.isar.lelantusCoins.putAll(updatedCoins); - }); - } catch (e, s) { - Logging.instance.f( - " ", - error: e, - stackTrace: s, - ); - rethrow; - } - } - } - - /// Should only be called within the standard wallet [recover] function due to - /// mutex locking. Otherwise behaviour MAY be undefined. - Future recoverLelantusWallet({ - required int latestSetId, - required Map setDataMap, - required Set usedSerialNumbers, - }) async { - final root = await getRootHDNode(); - - final derivePath = cryptoCurrency.constructDerivePath( - derivePathType: DerivePathType.bip44, - chain: 0, - index: 0, - ); - - // get "m/$purpose'/$coinType'/$account'/" from "m/$purpose'/$coinType'/$account'/0/0" - final partialDerivationPath = derivePath.substring( - 0, - derivePath.length - 3, - ); - - final result = await LelantusFfiWrapper.restore( - hexRootPrivateKey: root.privateKey.data.toHex, - chaincode: root.chaincode, - cryptoCurrency: cryptoCurrency, - latestSetId: latestSetId, - setDataMap: setDataMap, - usedSerialNumbers: usedSerialNumbers, - walletId: walletId, - partialDerivationPath: partialDerivationPath, - ); - - final currentHeight = await chainHeight; - - final txns = await mainDB - .getTransactions(walletId) - .filter() - .isLelantusIsNull() - .or() - .isLelantusEqualTo(false) - .findAll(); - - // TODO: [prio=high] shouldn't these be v2? If it doesn't matter than we can get rid of this logic - // Edit the receive transactions with the mint fees. - final List editedTransactions = []; - - for (final coin in result.lelantusCoins) { - final String txid = coin.txid; - Transaction? tx; - try { - tx = txns.firstWhere((e) => e.txid == txid); - } catch (_) { - tx = null; - } - - if (tx == null || tx.subType == TransactionSubType.join) { - // This is a jmint. - continue; - } - - final List inputTxns = []; - for (final input in tx.inputs) { - Transaction? inputTx; - try { - inputTx = txns.firstWhere((e) => e.txid == input.txid); - } catch (_) { - inputTx = null; - } - if (inputTx != null) { - inputTxns.add(inputTx); - } - } - if (inputTxns.isEmpty) { - //some error. - Logging.instance.f( - "cryptic \"//some error\" occurred in staticProcessRestore on lelantus coin: $coin", - ); - continue; - } - - final int mintFee = tx.fee; - final int sharedFee = mintFee ~/ inputTxns.length; - for (final inputTx in inputTxns) { - final edited = Transaction( - walletId: inputTx.walletId, - txid: inputTx.txid, - timestamp: inputTx.timestamp, - type: inputTx.type, - subType: TransactionSubType.mint, - amount: inputTx.amount, - amountString: Amount( - rawValue: BigInt.from(inputTx.amount), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), - fee: sharedFee, - height: inputTx.height, - isCancelled: false, - isLelantus: true, - slateId: null, - otherData: txid, - nonce: null, - inputs: inputTx.inputs, - outputs: inputTx.outputs, - numberOfMessages: null, - )..address.value = inputTx.address.value; - editedTransactions.add(edited); - } - } - // Logging.instance.log(editedTransactions, addToDebugMessagesDB: false); - - final Map transactionMap = {}; - for (final e in txns) { - transactionMap[e.txid] = e; - } - // Logging.instance.log(transactionMap, addToDebugMessagesDB: false); - - // update with edited transactions - for (final tx in editedTransactions) { - transactionMap[tx.txid] = tx; - } - - transactionMap.removeWhere( - (key, value) => - result.lelantusCoins.any((element) => element.txid == key) || - ((value.height == -1 || value.height == null) && - !value.isConfirmed(currentHeight, cryptoCurrency.minConfirms)), - ); - - try { - await mainDB.isar.writeTxn(() async { - await mainDB.isar.lelantusCoins.putAll(result.lelantusCoins); - }); - } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); - // don't just rethrow since isar likes to strip stack traces for some reason - throw Exception("e=$e & s=$s"); - } - - final Map> data = {}; - - for (final entry in transactionMap.entries) { - data[entry.key] = Tuple2(entry.value.address.value, entry.value); - } - - // Create the joinsplit transactions. - final spendTxs = await getJMintTransactions( - result.spendTxIds, - ); - Logging.instance.d("lelantus spendTxs: $spendTxs"); - - for (final element in spendTxs.entries) { - final address = element.value.address.value ?? - data[element.value.txid]?.item1 ?? - element.key; - // Address( - // walletId: walletId, - // value: transactionInfo["address"] as String, - // derivationIndex: -1, - // type: AddressType.nonWallet, - // subType: AddressSubType.nonWallet, - // publicKey: [], - // ); - - data[element.value.txid] = Tuple2(address, element.value); - } - - final List> txnsData = []; - - for (final value in data.values) { - final transactionAddress = value.item1!; - final outs = - value.item2.outputs.where((_) => true).toList(growable: false); - final ins = value.item2.inputs.where((_) => true).toList(growable: false); - - txnsData.add( - Tuple2( - value.item2.copyWith(inputs: ins, outputs: outs).item1, - transactionAddress, - ), - ); - } - - await mainDB.addNewTransactionData(txnsData, walletId); - } - - /// Builds and signs a transaction - Future buildMintTransaction({required TxData txData}) async { - final signingData = await fetchBuildTxData(txData.utxos!.toList()); - - 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); - - final int height = await chainHeight; - - txb.setLockTime(height); - 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, - data!.output!, - ); - amount += signingData[i].utxo.value; - } - - for (final mintsElement in txData.mintsMapLelantus!) { - Logging.instance.d("using $mintsElement"); - final Uint8List mintu8 = - Format.stringToUint8List(mintsElement['script'] as String); - txb.addOutput(mintu8, mintsElement['value'] as int); - } - - for (var i = 0; i < signingData.length; i++) { - txb.sign( - vin: i, - keyPair: bitcoindart.ECPair.fromPrivateKey( - signingData[i].keyPair!.privateKey.data, - network: convertedNetwork, - compressed: signingData[i].keyPair!.privateKey.compressed, - ), - witnessValue: signingData[i].utxo.value, - ); - } - final incomplete = txb.buildIncomplete(); - final txId = incomplete.getId(); - final txHex = incomplete.toHex(); - final int fee = amount - incomplete.outs[0].value!; - - final builtHex = txb.build(); - - return txData.copyWith( - recipients: [ - ( - amount: Amount( - rawValue: BigInt.from(incomplete.outs[0].value!), - fractionDigits: cryptoCurrency.fractionDigits, - ), - address: "no address for lelantus mints", - isChange: false, - ), - ], - vSize: builtHex.virtualSize(), - txid: txId, - raw: txHex, - height: height, - txType: TransactionType.outgoing, - txSubType: TransactionSubType.mint, - fee: Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ), - ); - - // return { - // "transaction": builtHex, - // "txid": txId, - // "txHex": txHex, - // "value": amount - fee, - // "fees": Amount( - // rawValue: BigInt.from(fee), - // fractionDigits: coin.fractionDigits, - // ).decimal.toDouble(), - // "height": height, - // "txType": "Sent", - // "confirmed_status": false, - // "amount": Amount( - // rawValue: BigInt.from(amount), - // fractionDigits: coin.fractionDigits, - // ).decimal.toDouble(), - // "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000, - // "subType": "mint", - // "mintsMap": mintsMap, - // }; - } - - /// Returns the mint transaction hex to mint all of the available funds. - Future _mintSelection() async { - final currentChainHeight = await chainHeight; - final List availableOutputs = await mainDB - .getUTXOs(walletId) - .filter() - .isBlockedEqualTo(false) - .findAll(); - final List spendableOutputs = []; - - // Build list of spendable outputs and totaling their satoshi amount - for (var i = 0; i < availableOutputs.length; i++) { - if (availableOutputs[i].isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ) == - true && - !(availableOutputs[i].isCoinbase && - availableOutputs[i].getConfirmations(currentChainHeight) <= - 101)) { - spendableOutputs.add(availableOutputs[i]); - } - } - - final lelantusCoins = await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .not() - .valueEqualTo(0.toString()) - .findAll(); - - final data = await mainDB - .getTransactions(walletId) - .filter() - .isLelantusIsNull() - .or() - .isLelantusEqualTo(false) - .findAll(); - - for (final value in data) { - if (value.inputs.isNotEmpty) { - for (final element in value.inputs) { - if (lelantusCoins.any((e) => e.txid == value.txid) && - spendableOutputs.firstWhere( - (output) => output?.txid == element.txid, - orElse: () => null, - ) != - null) { - spendableOutputs - .removeWhere((output) => output!.txid == element.txid); - } - } - } - } - - // If there is no Utxos to mint then stop the function. - if (spendableOutputs.isEmpty) { - throw Exception("_mintSelection(): No spendable outputs found"); - } - - int satoshisBeingUsed = 0; - final Set utxoObjectsToUse = {}; - - for (var i = 0; i < spendableOutputs.length; i++) { - final spendable = spendableOutputs[i]; - if (spendable != null) { - utxoObjectsToUse.add(spendable); - satoshisBeingUsed += spendable.value; - } - } - - final mintsWithoutFee = await _createMintsFromAmount(satoshisBeingUsed); - - TxData txData = await buildMintTransaction( - txData: TxData( - utxos: utxoObjectsToUse, - mintsMapLelantus: mintsWithoutFee, - ), - ); - - final Decimal dvSize = Decimal.fromInt(txData.vSize!); - - final feesObject = await fees; - - final Decimal fastFee = Amount( - rawValue: BigInt.from(feesObject.fast), - fractionDigits: cryptoCurrency.fractionDigits, - ).decimal; - int firoFee = - (dvSize * fastFee * Decimal.fromInt(100000)).toDouble().ceil(); - // int firoFee = (vSize * feesObject.fast * (1 / 1000.0) * 100000000).ceil(); - - if (firoFee < txData.vSize!) { - firoFee = txData.vSize! + 1; - } - firoFee = firoFee + 10; - final int satoshiAmountToSend = satoshisBeingUsed - firoFee; - - final mintsWithFee = await _createMintsFromAmount(satoshiAmountToSend); - - txData = await buildMintTransaction( - txData: txData.copyWith( - mintsMapLelantus: mintsWithFee, - ), - ); - - return txData; - } - - Future>> _createMintsFromAmount(int total) async { - if (total > LelantusFfiWrapper.MINT_LIMIT) { - throw Exception( - "Lelantus mints of more than 5001 are currently disabled", - ); - } - - int tmpTotal = total; - int counter = 0; - final lastUsedIndex = - await mainDB.getHighestUsedMintIndex(walletId: walletId); - final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; - - final isTestnet = cryptoCurrency.network.isTestNet; - - final root = await getRootHDNode(); - - final mints = >[]; - while (tmpTotal > 0) { - final index = nextFreeMintIndex + counter; - - final mintKeyPair = root.derivePath( - cryptoCurrency.constructDerivePath( - derivePathType: DerivePathType.bip44, - chain: LelantusFfiWrapper.MINT_INDEX, - index: index, - ), - ); - - final privateKeyHex = mintKeyPair.privateKey.data.toHex; - final seedId = Format.uint8listToString(mintKeyPair.identifier); - - final String mintTag = lelantus.CreateTag( - privateKeyHex, - index, - seedId, - isTestnet: isTestnet, - ); - final List> anonymitySets; - try { - anonymitySets = await fetchAnonymitySets(); - } catch (e, s) { - Logging.instance.f( - "Firo needs better internet to create mints: ", - error: e, - stackTrace: s, - ); - rethrow; - } - - bool isUsedMintTag = false; - - // stupid dynamic maps - for (final set in anonymitySets) { - final setCoins = set["coins"] as List; - for (final coin in setCoins) { - if (coin[1] == mintTag) { - isUsedMintTag = true; - break; - } - } - if (isUsedMintTag) { - break; - } - } - - if (isUsedMintTag) { - Logging.instance.d("Found used index when minting"); - } - - if (!isUsedMintTag) { - final mintValue = min( - tmpTotal, - (isTestnet - ? LelantusFfiWrapper.MINT_LIMIT_TESTNET - : LelantusFfiWrapper.MINT_LIMIT), - ); - final mint = await LelantusFfiWrapper.getMintScript( - amount: Amount( - rawValue: BigInt.from(mintValue), - fractionDigits: cryptoCurrency.fractionDigits, - ), - privateKeyHex: privateKeyHex, - index: index, - seedId: seedId, - isTestNet: isTestnet, - ); - - mints.add({ - "value": mintValue, - "script": mint, - "index": index, - }); - tmpTotal = tmpTotal - - (isTestnet - ? LelantusFfiWrapper.MINT_LIMIT_TESTNET - : LelantusFfiWrapper.MINT_LIMIT); - } - - counter++; - } - return mints; - } - - Future anonymizeAllLelantus() async { - try { - final mintResult = await _mintSelection(); - - await confirmSendLelantus(txData: mintResult); - - unawaited(refresh()); - } catch (e, s) { - Logging.instance.w( - "Exception caught in anonymizeAllLelantus(): ", - error: e, - stackTrace: s, - ); - rethrow; - } - } - - @override - Future updateBalance() async { - // call to super to update transparent balance - final normalBalanceFuture = super.updateBalance(); - - final lelantusCoins = await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .isUsedEqualTo(false) - .not() - .valueEqualTo(0.toString()) - .findAll(); - - final currentChainHeight = await chainHeight; - int intLelantusBalance = 0; - int unconfirmedLelantusBalance = 0; - - for (final lelantusCoin in lelantusCoins) { - final Transaction? txn = mainDB.isar.transactions - .where() - .txidWalletIdEqualTo( - lelantusCoin.txid, - walletId, - ) - .findFirstSync(); - - if (txn == null) { - Logging.instance.e( - "Transaction not found in DB for lelantus coin", - ); - Logging.instance.d( - "Transaction not found in DB for lelantus coin: $lelantusCoin", - ); - } else { - if (txn.isLelantus != true) { - Logging.instance.f( - "Bad database state found in ${info.name} $walletId for _refreshBalance lelantus", - ); - } - - if (txn.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms)) { - // mint tx, add value to balance - intLelantusBalance += int.parse(lelantusCoin.value); - } else { - unconfirmedLelantusBalance += int.parse(lelantusCoin.value); - } - } - } - - final balancePrivate = Balance( - total: Amount( - rawValue: BigInt.from(intLelantusBalance + unconfirmedLelantusBalance), - fractionDigits: cryptoCurrency.fractionDigits, - ), - spendable: Amount( - rawValue: BigInt.from(intLelantusBalance), - fractionDigits: cryptoCurrency.fractionDigits, - ), - blockedTotal: Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - pendingSpendable: Amount( - rawValue: BigInt.from(unconfirmedLelantusBalance), - fractionDigits: cryptoCurrency.fractionDigits, - ), - ); - await info.updateBalanceSecondary( - newBalance: balancePrivate, - isar: mainDB.isar, - ); - - // wait for updated uxtos to get updated public balance - await normalBalanceFuture; - } -} diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart new file mode 100644 index 000000000..4480ef81b --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -0,0 +1,965 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; +import 'package:drift/drift.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:isar/isar.dart'; +import 'package:mweb_client/mweb_client.dart'; + +import '../../../db/drift/database.dart'; +import '../../../models/balance.dart'; +import '../../../models/input.dart'; +import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../models/isar/models/isar_models.dart'; +import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; +import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; +import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../services/event_bus/global_event_bus.dart'; +import '../../../services/mwebd_service.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../../utilities/extensions/extensions.dart'; +import '../../../utilities/logger.dart'; +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../isar/models/wallet_info.dart'; +import '../../models/tx_data.dart'; +import '../intermediate/external_wallet.dart'; +import 'electrumx_interface.dart'; + +mixin MwebInterface + on ElectrumXInterface + implements ExternalWallet { + StreamSubscription? _mwebUtxoSubscription; + + Future get _scanSecret async => + (await getRootHDNode()).derivePath("m/1000'/0'").privateKey.data; + Future get _spendSecret async => + (await getRootHDNode()).derivePath("m/1000'/1'").privateKey.data; + Future get _spendPub async => + (await getRootHDNode()).derivePath("m/1000'/1'").publicKey.data; + + Future getCurrentReceivingMwebAddress() async { + return await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst(); + } + + Future getMwebChangeAddress() async { + return await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .subTypeEqualTo(AddressSubType.change) + .and() + .derivationIndexEqualTo(0) + .findFirst(); + } + + Future get _client async { + final client = await MwebdService.instance.getClient( + cryptoCurrency.network, + ); + if (client == null) { + throw Exception("Fetched mweb client returned null"); + } + return client; + } + + WalletSyncStatus? _syncStatusMwebCache; + WalletSyncStatus? get _syncStatusMweb => _syncStatusMwebCache; + set _syncStatusMweb(WalletSyncStatus? newValue) { + switch (newValue) { + case null: + doNotFireRefreshEvents = true; + case WalletSyncStatus.unableToSync: + doNotFireRefreshEvents = true; + case WalletSyncStatus.synced: + doNotFireRefreshEvents = false; + case WalletSyncStatus.syncing: + doNotFireRefreshEvents = true; + } + + _syncStatusMwebCache = newValue; + } + + Timer? _mwebdPolling; + int currentKnownChainHeight = 0; + double highestPercentCached = 0; + void _startPollingMwebd() async { + _mwebdPolling?.cancel(); + _mwebdPolling = Timer.periodic(const Duration(seconds: 5), (_) async { + try { + final status = await MwebdService.instance.getServerStatus( + cryptoCurrency.network, + ); + + Logging.instance.t( + "$walletId ${info.name} _polling mwebd status: $status", + ); + + if (status == null) { + throw Exception( + "Mwebd server status is null. Was mwebd initialized?", + ); + } + + final currentKnownChainHeight = await chainHeight; + + final ({int remaining, double percent})? syncInfo; + + if (status.blockHeaderHeight < currentKnownChainHeight) { + syncInfo = ( + remaining: currentKnownChainHeight - status.blockHeaderHeight, + percent: status.blockHeaderHeight / currentKnownChainHeight, + ); + } else if (status.mwebHeaderHeight < currentKnownChainHeight) { + syncInfo = ( + remaining: currentKnownChainHeight - status.mwebHeaderHeight, + percent: status.mwebHeaderHeight / currentKnownChainHeight, + ); + } else if (status.mwebUtxosHeight < currentKnownChainHeight) { + syncInfo = (remaining: 1, percent: 0.99); + } else { + syncInfo = null; + } + + WalletSyncStatus? syncStatus; + + if (syncInfo != null) { + final previous = highestPercentCached; + highestPercentCached = math.max( + highestPercentCached, + syncInfo.percent, + ); + + if (previous != highestPercentCached) { + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercentCached, walletId), + ); + GlobalEventBus.instance.fire( + BlocksRemainingEvent(syncInfo.remaining, walletId), + ); + } + + syncStatus = WalletSyncStatus.syncing; + } else { + syncStatus = WalletSyncStatus.synced; + } + + _syncStatusMweb = syncStatus; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent(syncStatus, walletId, info.coin), + ); + } catch (e, s) { + Logging.instance.e( + "mweb wallet polling error", + error: e, + stackTrace: s, + ); + _syncStatusMweb = WalletSyncStatus.unableToSync; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent(_syncStatusMweb!, walletId, info.coin), + ); + } + }); + } + + Future _stopUpdateMwebUtxos() async => + await _mwebUtxoSubscription?.cancel(); + + Future _startUpdateMwebUtxos() async { + await _stopUpdateMwebUtxos(); + + final client = await _client; + + Logging.instance.i("info.restoreHeight: ${info.restoreHeight}"); + Logging.instance.i( + "info.otherData[WalletInfoKeys.mwebScanHeight]: ${info.otherData[WalletInfoKeys.mwebScanHeight]}", + ); + final fromHeight = + info.otherData[WalletInfoKeys.mwebScanHeight] as int? ?? + info.restoreHeight; + + final request = UtxosRequest( + fromHeight: fromHeight, + scanSecret: await _scanSecret, + ); + + final db = Drift.get(walletId); + _mwebUtxoSubscription = (await client.utxos(request)).listen((utxo) async { + Logging.instance.t( + "Found UTXO in stream: Utxo(" + "height: ${utxo.height}, " + "value: ${utxo.value}, " + "address: ${utxo.address}, " + "outputId: ${utxo.outputId}, " + "blockTime: ${utxo.blockTime}" + ")", + ); + + if (utxo.address.isNotEmpty && utxo.outputId.isNotEmpty) { + try { + await db.transaction(() async { + final prev = + await (db.select(db.mwebUtxos)..where( + (e) => e.outputId.equals(utxo.outputId), + )).getSingleOrNull(); + + if (prev == null) { + final newUtxo = MwebUtxosCompanion( + outputId: Value(utxo.outputId), + address: Value(utxo.address), + value: Value(utxo.value.toInt()), + height: Value(utxo.height), + blockTime: Value(utxo.blockTime), + blocked: const Value(false), + used: const Value(false), + ); + + await db.into(db.mwebUtxos).insert(newUtxo); + } else { + await db + .update(db.mwebUtxos) + .replace( + prev.copyWith( + blockTime: utxo.blockTime, + height: utxo.height, + ), + ); + } + }); + + Address? addr = await mainDB.getAddress(walletId, utxo.address); + while (addr == null || addr.value != utxo.address) { + addr = await generateNextMwebAddress(); + await mainDB.updateOrPutAddresses([addr]); + } + + // TODO get real txid one day + final fakeTxid = "mweb_outputId_${utxo.outputId}"; + + final tx = TransactionV2( + walletId: walletId, + blockHash: null, // ?? + hash: "", + txid: fakeTxid, + timestamp: + utxo.height < 1 + ? DateTime.now().millisecondsSinceEpoch ~/ 1000 + : utxo.blockTime, + height: utxo.height, + inputs: [], + outputs: [ + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "", + valueStringSats: utxo.value.toString(), + addresses: [utxo.address], + walletOwns: true, + ), + ], + version: 2, // probably + type: TransactionType.incoming, + subType: TransactionSubType.mweb, + otherData: jsonEncode({ + TxV2OdKeys.overrideFee: + Amount( + rawValue: + BigInt + .zero, // TODO fill in correctly when we have a real txid + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + }), + ); + + await mainDB.updateOrPutTransactionV2s([tx]); + + await updateBalance(); + + if (utxo.height > fromHeight) { + await info.updateOtherData( + newEntries: {WalletInfoKeys.mwebScanHeight: utxo.height}, + isar: mainDB.isar, + ); + } + } catch (e, s) { + Logging.instance.f( + "Failed to insert/update mweb utxo", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w("Empty mweb utxo not added to db... ??"); + } + }); + } + + Future _initMweb() async { + try { + // check server is up + final status = await MwebdService.instance.getServerStatus( + cryptoCurrency.network, + ); + if (status == null) { + await MwebdService.instance.initService(cryptoCurrency.network); + } + + _startPollingMwebd(); + } catch (e, s) { + Logging.instance.e("testing initMweb failed", error: e, stackTrace: s); + } + } + + /// [isChange] will always return the change address at index 0 !!!!! + Future
generateNextMwebAddress({bool isChange = false}) async { + if (!info.isMwebEnabled) { + throw Exception( + "Tried calling generateNextMwebAddress with mweb disabled for $walletId ${info.name}", + ); + } + + final int nextIndex; + if (isChange) { + nextIndex = 0; + } else { + final highestStoredIndex = + (await getCurrentReceivingMwebAddress())?.derivationIndex ?? 0; + + nextIndex = highestStoredIndex + 1; + } + + final client = await _client; + + final response = await client.address( + await _scanSecret, + await _spendPub, + nextIndex, + ); + + return Address( + walletId: walletId, + value: response, + publicKey: [], + derivationIndex: nextIndex, + derivationPath: null, + type: AddressType.mweb, + subType: isChange ? AddressSubType.change : AddressSubType.receiving, + ); + } + + Future processMwebTransaction(TxData txData) async { + final client = await _client; + final response = await client.create( + CreateRequest( + rawTx: txData.raw!.toUint8ListFromHex, + scanSecret: await _scanSecret, + spendSecret: await _spendSecret, + feeRatePerKb: Int64(txData.feeRateAmount!.toInt()), + dryRun: false, + ), + ); + + if (txData.type == TxType.mwebPegIn) { + cl.Transaction clTx = cl.Transaction.fromBytes( + Uint8List.fromList(response.rawTx), + ); + + assert(response.rawTx.toString() == clTx.toBytes().toList().toString()); + final List prevOuts = []; + + for (int i = 0; i < txData.usedUTXOs!.length; i++) { + final data = txData.usedUTXOs![i]; + if (data is StandardInput) { + final prevOutput = cl.Output.fromAddress( + BigInt.from(data.utxo.value), + cl.Address.fromString( + data.utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + } + } + + for (int i = 0; i < txData.usedUTXOs!.length; i++) { + final data = txData.usedUTXOs![i]; + + if (data is MwebInput) { + // do nothing + } else if (data is StandardInput) { + final value = BigInt.from(data.utxo.value); + final key = data.key!.privateKey!; + if (clTx.inputs[i] is cl.TaprootKeyInput) { + final taproot = cl.Taproot(internalKey: data.key!.publicKey); + + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is cl.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is cl.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is cl.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); + } + } else { + throw Exception("Unknown input type: ${data.runtimeType}"); + } + } + return txData.copyWith(raw: clTx.toHex()); + } else { + return txData.copyWith(raw: Uint8List.fromList(response.rawTx).toHex); + } + } + + Future _confirmSendMweb({required TxData txData}) async { + if (!info.isMwebEnabled) { + throw Exception( + "Tried calling _confirmSendMweb with mweb disabled for $walletId ${info.name}", + ); + } + + try { + Logging.instance.d("_confirmSendMweb txData: $txData"); + + final client = await _client; + + final response = await client.broadcast( + BroadcastRequest(rawTx: txData.raw!.toUint8ListFromHex), + ); + + final txHash = response.txid; + Logging.instance.d("Sent txHash: $txHash"); + + txData = txData.copyWith( + usedUTXOs: + txData.usedUTXOs!.map((e) { + if (e is StandardInput) { + return StandardInput( + e.utxo.copyWith(used: true), + derivePathType: e.derivePathType, + ); + } else if (e is MwebInput) { + return MwebInput(e.utxo.copyWith(used: true)); + } else { + return e; + } + }).toList(), + txHash: txHash, + txid: txHash, + ); + + // mark utxos as used + await mainDB.putUTXOs( + txData.usedUTXOs! + .whereType() + .map((e) => e.utxo) + .toList(), + ); + + // Update used mweb utxos as used in database + final usedMwebUtxos = + txData.usedUTXOs!.whereType().map((e) => e.utxo).toList(); + + Logging.instance.i("Used mweb inputs: $usedMwebUtxos"); + + if (usedMwebUtxos.isNotEmpty) { + final db = Drift.get(walletId); + await db.transaction(() async { + for (final used in usedMwebUtxos) { + await db.update(db.mwebUtxos).replace(used); + } + }); + } + + return await updateSentCachedTxData(txData: txData); + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from _confirmSendMweb(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + @override + Future prepareSend({required TxData txData}) async { + final hasMwebOutputs = + txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; + if (hasMwebOutputs) { + // assume pegin tx + txData = txData.copyWith(type: TxType.mwebPegIn); + } + + return super.prepareSend(txData: txData); + } + + /// prepare mweb transaction where spending mweb outputs + Future prepareSendMweb({required TxData txData}) async { + final hasMwebOutputs = + txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; + + final type = hasMwebOutputs ? TxType.mweb : TxType.mwebPegOut; + + txData = txData.copyWith(type: type); + + return super.prepareSend(txData: txData); + } + + Future anonymizeAllMweb() async { + if (!info.isMwebEnabled) { + Logging.instance.e( + "Tried calling anonymizeAllMweb with mweb disabled for $walletId ${info.name}", + ); + return; + } + + try { + final currentHeight = await chainHeight; + + final spendableUtxos = + await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedEqualTo(false).or().usedIsNull()) + .and() + .valueGreaterThan(0) + .findAll(); + + spendableUtxos.removeWhere( + (e) => + !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), + ); + + if (spendableUtxos.isEmpty) { + throw Exception("No available UTXOs found to anonymize"); + } + + final amount = spendableUtxos.fold( + Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits), + (p, e) => + p + + Amount( + rawValue: BigInt.from(e.value), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + + // TODO finish + final txData = await prepareSend( + txData: TxData( + type: TxType.mwebPegIn, + feeRateType: FeeRateType.average, + recipients: [ + TxRecipient( + address: (await getCurrentReceivingMwebAddress())!.value, + amount: amount, + isChange: false, + addressType: AddressType.mweb, + ), + ], + ), + ); + + await _confirmSendMweb(txData: txData); + } catch (e, s) { + Logging.instance.w( + "Exception caught in anonymizeAllMweb(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + Future _checkAddresses() async { + // check change first as it is index 0 + Address? changeAddress = await getMwebChangeAddress(); + if (changeAddress == null) { + changeAddress = await generateNextMwebAddress(isChange: true); + await mainDB.putAddress(changeAddress); + } + + // check recieving + Address? address = await getCurrentReceivingMwebAddress(); + if (address == null) { + address = await generateNextMwebAddress(); + await mainDB.putAddress(address); + } + } + + // =========================================================================== + + @override + Future confirmSend({required TxData txData}) async { + if (txData.type.isMweb()) { + return await _confirmSendMweb(txData: txData); + } else { + return await super.confirmSend(txData: txData); + } + } + + @override + Future open() async { + if (info.isMwebEnabled) { + try { + await _initMweb(); + + await _checkAddresses(); + + unawaited(_startUpdateMwebUtxos()); + } catch (e, s) { + // do nothing, still allow user into wallet + Logging.instance.e( + "$runtimeType init() failed", + error: e, + stackTrace: s, + ); + } + } + } + + @override + Future updateBalance() async { + // call to super to update transparent balance + final normalBalanceFuture = super.updateBalance(); + + if (info.isMwebEnabled) { + final start = DateTime.now(); + try { + final currentHeight = await chainHeight; + final db = Drift.get(walletId); + final mwebUtxos = + await (db.select(db.mwebUtxos) + ..where((e) => e.used.equals(false))).get(); + + 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 mwebUtxos) { + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + satoshiBalanceTotal += utxoAmount; + + if (utxo.blocked) { + satoshiBalanceBlocked += utxoAmount; + } else { + if (utxo.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + // overrideMinConfirms: TODO: set this??? + )) { + satoshiBalanceSpendable += utxoAmount; + } else { + satoshiBalancePending += utxoAmount; + } + } + } + + final balance = Balance( + total: satoshiBalanceTotal, + spendable: satoshiBalanceSpendable, + blockedTotal: satoshiBalanceBlocked, + pendingSpendable: satoshiBalancePending, + ); + + await info.updateBalanceSecondary( + newBalance: balance, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.e( + "$runtimeType updateBalance mweb $walletId ${info.name}: ", + error: e, + stackTrace: s, + ); + } finally { + Logging.instance.d( + "${info.name} updateBalance mweb duration:" + " ${DateTime.now().difference(start)}", + ); + } + } + + // wait for normalBalanceFuture to complete before returning + await normalBalanceFuture; + } + + @override + Future recover({required bool isRescan}) async { + if (isViewOnly) { + await recoverViewOnly(isRescan: isRescan); + return; + } + + final start = DateTime.now(); + final root = await getRootHDNode(); + + final List addresses})>> receiveFutures = + []; + final List addresses})>> changeFutures = + []; + + const receiveChain = 0; + const changeChain = 1; + + const txCountBatchSize = 12; + + try { + await refreshMutex.protect(() async { + if (isRescan) { + await _stopUpdateMwebUtxos(); + + // clear cache + await electrumXCachedClient.clearSharedTransactionCache( + cryptoCurrency: info.coin, + ); + // clear blockchain info + await mainDB.deleteWalletBlockchainData(walletId); + + // reset scan/listen height + await info.updateOtherData( + newEntries: {WalletInfoKeys.mwebScanHeight: info.restoreHeight}, + isar: mainDB.isar, + ); + + // reset balance to 0 + await info.updateBalanceSecondary( + newBalance: Balance.zeroFor(currency: cryptoCurrency), + isar: mainDB.isar, + ); + + // clear all mweb utxos + final db = Drift.get(walletId); + await db.transaction(() async => await db.delete(db.mwebUtxos).go()); + + if (info.isMwebEnabled) { + await _checkAddresses(); + + // only restart scanning if mweb enabled + unawaited(_startUpdateMwebUtxos()); + } + } + + // receiving addresses + Logging.instance.i("checking receiving addresses..."); + + final canBatch = await serverCanBatch; + + for (final type in cryptoCurrency.supportedDerivationPathTypes) { + receiveFutures.add( + canBatch + ? checkGapsBatched(txCountBatchSize, root, type, receiveChain) + : checkGapsLinearly(root, type, receiveChain), + ); + } + + // change addresses + Logging.instance.d("checking change addresses..."); + for (final type in cryptoCurrency.supportedDerivationPathTypes) { + changeFutures.add( + canBatch + ? checkGapsBatched(txCountBatchSize, root, type, changeChain) + : checkGapsLinearly(root, type, changeChain), + ); + } + + // io limitations may require running these linearly instead + final futuresResult = await Future.wait([ + Future.wait(receiveFutures), + Future.wait(changeFutures), + ]); + + final receiveResults = futuresResult[0]; + final changeResults = futuresResult[1]; + + final List
addressesToStore = []; + + int highestReceivingIndexWithHistory = 0; + + for (final tuple in receiveResults) { + if (tuple.addresses.isEmpty) { + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } + } else { + highestReceivingIndexWithHistory = math.max( + tuple.index, + highestReceivingIndexWithHistory, + ); + addressesToStore.addAll(tuple.addresses); + } + } + + int highestChangeIndexWithHistory = 0; + // If restoring a wallet that never sent any funds with change, then set changeArray + // manually. If we didn't do this, it'd store an empty array. + for (final tuple in changeResults) { + if (tuple.addresses.isEmpty) { + await checkChangeAddressForTransactions(); + } else { + highestChangeIndexWithHistory = math.max( + tuple.index, + highestChangeIndexWithHistory, + ); + addressesToStore.addAll(tuple.addresses); + } + } + + // remove extra addresses to help minimize risk of creating a large gap + addressesToStore.removeWhere( + (e) => + e.subType == AddressSubType.change && + e.derivationIndex > highestChangeIndexWithHistory, + ); + addressesToStore.removeWhere( + (e) => + e.subType == AddressSubType.receiving && + e.derivationIndex > highestReceivingIndexWithHistory, + ); + + await mainDB.updateOrPutAddresses(addressesToStore); + }); + + unawaited(refresh()); + Logging.instance.i( + "Mweb recover for " + "${info.name}: ${DateTime.now().difference(start)}", + ); + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from mweb_interface recover(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + @override + Future exit() async { + _mwebdPolling?.cancel(); + _mwebdPolling = null; + await super.exit(); + } + + bool isMwebAddress(String address) { + try { + cl.MwebAddress.fromString(address, network: cryptoCurrency.networkParams); + return true; + } catch (_) { + return false; + } + } + + Future mwebFee({required TxData txData}) async { + final outputs = txData.recipients!; + final utxos = txData.usedUTXOs!; + + final sumOfUtxosValue = utxos.fold(BigInt.zero, (p, e) => p + e.value); + + final preOutputSum = outputs.fold(BigInt.zero, (p, e) => p + e.amount.raw); + final fee = sumOfUtxosValue - preOutputSum; + + final client = await _client; + + final resp = await client.create( + CreateRequest( + rawTx: txData.raw!.toUint8ListFromHex, + scanSecret: await _scanSecret, + spendSecret: await _spendSecret, + feeRatePerKb: Int64(txData.feeRateAmount!.toInt()), + dryRun: true, + ), + ); + + final processedTx = cl.Transaction.fromBytes( + Uint8List.fromList(resp.rawTx), + ); + + BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; + final posUtxos = + utxos + .where( + (utxo) => processedTx.inputs.any( + (input) => + input.prevOut.hash.toHex == + Uint8List.fromList( + utxo.id.toUint8ListFromHex.reversed.toList(), + ).toHex, + ), + ) + .toList(); + + final posOutputSum = processedTx.outputs.fold( + BigInt.zero, + (acc, output) => acc + output.value, + ); + final mwebInputSum = + sumOfUtxosValue - posUtxos.fold(BigInt.zero, (p, e) => p + e.value); + final expectedPegin = maxBI(BigInt.zero, (preOutputSum - mwebInputSum)); + BigInt feeIncrease = posOutputSum - expectedPegin; + + if (expectedPegin > BigInt.zero) { + feeIncrease += BigInt.from( + (txData.feeRateAmount! / BigInt.from(1000) * 41).ceil(), + ); + } + + return Amount( + rawValue: fee + feeIncrease, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } +} diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart index 1d725afb4..f9efe7975 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart @@ -28,9 +28,7 @@ import '../intermediate/bip39_wallet.dart'; const _kWorkServer = "https://nodes.nanswap.com/XNO"; Map _buildHeaders(String url) { - final result = { - 'Content-type': 'application/json', - }; + final result = {'Content-type': 'application/json'}; if (url case "https://nodes.nanswap.com/XNO" || "https://nodes.nanswap.com/BAN") { result["nodes-api-key"] = kNanoSwapRpcApiKey; @@ -52,28 +50,24 @@ mixin NanoInterface on Bip39Wallet { _hackedCheckTorNodePrefs(); return _httpClient .post( - url: Uri.parse(_kWorkServer), // this should be a - headers: _buildHeaders(_kWorkServer), - body: json.encode( - { - "action": "work_generate", - "hash": hash, - }, - ), - proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) + url: Uri.parse(_kWorkServer), // this should be a + headers: _buildHeaders(_kWorkServer), + body: json.encode({"action": "work_generate", "hash": hash}), + proxyInfo: + prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ) .then((_httpClient) { - if (_httpClient.code == 200) { - final Map decoded = - json.decode(_httpClient.body) as Map; - if (decoded.containsKey("error")) { - throw Exception("Received error ${decoded["error"]}"); - } - return decoded["work"] as String?; - } else { - throw Exception("Received error ${_httpClient.code}"); - } - }); + if (_httpClient.code == 200) { + final Map decoded = + json.decode(_httpClient.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + return decoded["work"] as String?; + } else { + throw Exception("Received error ${_httpClient.code}"); + } + }); } Future _getPrivateKeyFromMnemonic() async { @@ -87,8 +81,10 @@ mixin NanoInterface on Bip39Wallet { await _getPrivateKeyFromMnemonic(), ); - final addressString = - NanoAccounts.createAccount(cryptoCurrency.nanoAccountType, publicKey); + final addressString = NanoAccounts.createAccount( + cryptoCurrency.nanoAccountType, + publicKey, + ); return Address( walletId: walletId, @@ -146,8 +142,9 @@ mixin NanoInterface on Bip39Wallet { ); final balanceData = jsonDecode(balanceResponse.body); - final BigInt currentBalance = - BigInt.parse(balanceData["balance"].toString()); + final BigInt currentBalance = BigInt.parse( + balanceData["balance"].toString(), + ); final BigInt txAmount = BigInt.parse(amountRaw); final BigInt balanceAfterTx = currentBalance + txAmount; @@ -162,16 +159,19 @@ mixin NanoInterface on Bip39Wallet { // link = send block hash: final String link = blockHash; // this "linkAsAccount" is meaningless: - final String linkAsAccount = - NanoAccounts.createAccount(NanoAccountType.BANANO, blockHash); + final String linkAsAccount = NanoAccounts.createAccount( + NanoAccountType.BANANO, + blockHash, + ); // construct the receive block: final Map receiveBlock = { "type": "state", "account": publicAddress, - "previous": openBlock - ? "0000000000000000000000000000000000000000000000000000000000000000" - : frontier, + "previous": + openBlock + ? "0000000000000000000000000000000000000000000000000000000000000000" + : frontier, "representative": representative, "balance": balanceAfterTx.toString(), "link": link, @@ -320,9 +320,11 @@ mixin NanoInterface on Bip39Wallet { @override Future updateNode() async { - _cachedNode = NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? - info.coin.defaultNode; + _cachedNode = + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? + info.coin.defaultNode(isPrimary: true); unawaited(refresh()); } @@ -330,9 +332,10 @@ mixin NanoInterface on Bip39Wallet { @override NodeModel getCurrentNode() { return _cachedNode ?? - NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? - info.coin.defaultNode; + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? + info.coin.defaultNode(isPrimary: true); } @override @@ -365,11 +368,7 @@ mixin NanoInterface on Bip39Wallet { final response = await _httpClient.post( url: uri, headers: _buildHeaders(node.host), - body: jsonEncode( - { - "action": "version", - }, - ), + body: jsonEncode({"action": "version"}), proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, ); @@ -485,15 +484,9 @@ mixin NanoInterface on Bip39Wallet { } // return the hash of the transaction: - return txData.copyWith( - txid: decoded["hash"].toString(), - ); + return txData.copyWith(txid: decoded["hash"].toString()); } catch (e, s) { - Logging.instance.e( - "Error sending transaction", - error: e, - stackTrace: s, - ); + Logging.instance.e("Error sending transaction", error: e, stackTrace: s); rethrow; } } @@ -544,8 +537,9 @@ mixin NanoInterface on Bip39Wallet { ); // this should really have proper type checking and error propagation but I'm out of time - final newData = - Map.from((await jsonDecode(response.body)) as Map); + final newData = Map.from( + (await jsonDecode(response.body)) as Map, + ); if (newData["previous"] is String) { if (data?["history"] is List) { @@ -572,9 +566,10 @@ mixin NanoInterface on Bip39Wallet { final data = await _fetchAll(publicAddress, null, null); - final transactions = data["history"] is List - ? data["history"] as List - : []; + final transactions = + data["history"] is List + ? data["history"] as List + : []; if (transactions.isEmpty) { return; } else { @@ -612,17 +607,18 @@ mixin NanoInterface on Bip39Wallet { numberOfMessages: null, ); - final Address address = transactionType == TransactionType.incoming - ? receivingAddress - : Address( - walletId: walletId, - publicKey: [], - value: tx["account"].toString(), - derivationIndex: 0, - derivationPath: null, - type: info.mainAddressType, - subType: AddressSubType.nonWallet, - ); + final Address address = + transactionType == TransactionType.incoming + ? receivingAddress + : Address( + walletId: walletId, + publicKey: [], + value: tx["account"].toString(), + derivationIndex: 0, + derivationPath: null, + type: info.mainAddressType, + subType: AddressSubType.nonWallet, + ); final Tuple2 tuple = Tuple2(transaction, address); transactionList.add(tuple); } @@ -653,8 +649,9 @@ mixin NanoInterface on Bip39Wallet { final data = jsonDecode(response.body); final balance = Balance( total: Amount( - rawValue: (BigInt.parse(data["balance"].toString()) + - BigInt.parse(data["receivable"].toString())), + rawValue: + (BigInt.parse(data["balance"].toString()) + + BigInt.parse(data["receivable"].toString())), fractionDigits: cryptoCurrency.fractionDigits, ), spendable: Amount( @@ -703,10 +700,8 @@ mixin NanoInterface on Bip39Wallet { ); final infoData = jsonDecode(infoResponse.body); - final height = int.tryParse( - infoData["confirmation_height"].toString(), - ) ?? - 0; + final height = + int.tryParse(infoData["confirmation_height"].toString()) ?? 0; await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } catch (e, s) { @@ -734,21 +729,21 @@ mixin NanoInterface on Bip39Wallet { @override // nano has no fees - Future estimateFeeFor(Amount amount, int feeRate) async => Amount( - rawValue: BigInt.from(0), - fractionDigits: cryptoCurrency.fractionDigits, - ); + Future estimateFeeFor(Amount amount, BigInt feeRate) async => Amount( + rawValue: BigInt.from(0), + fractionDigits: cryptoCurrency.fractionDigits, + ); @override // nano has no fees Future get fees async => FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 1, - numberOfBlocksSlow: 1, - fast: 0, - medium: 0, - slow: 0, - ); + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: BigInt.zero, + medium: BigInt.zero, + slow: BigInt.zero, + ); void _hackedCheckTorNodePrefs() { final node = getCurrentNode(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index 5c0ace879..3ad0425d5 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -14,11 +14,11 @@ import 'package:tuple/tuple.dart'; import '../../../exceptions/wallet/insufficient_balance_exception.dart'; import '../../../exceptions/wallet/paynym_send_exception.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/bip32_utils.dart'; import '../../../utilities/bip47_utils.dart'; @@ -379,10 +379,11 @@ mixin PaynymInterface return prepareSend( txData: txData.copyWith( recipients: [ - ( + TxRecipient( address: sendToAddress.value, amount: txData.recipients!.first.amount, isChange: false, + addressType: sendToAddress.type, ), ], ), @@ -457,7 +458,7 @@ mixin PaynymInterface } Future prepareNotificationTx({ - required int selectedTxFeeRate, + required BigInt selectedTxFeeRate, required String targetPaymentCodeString, int additionalOutputs = 0, List? utxos, @@ -526,12 +527,15 @@ mixin PaynymInterface } // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final inputsWithKeys = + (await addSigningKeys( + utxoObjectsToUse.map((e) => StandardInput(e)).toList(), + )).whereType().toList(); final vSizeForNoChange = BigInt.from( (await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, // override amount to get around absurd fees error overrideAmountForTesting: satoshisBeingUsed, @@ -541,7 +545,7 @@ mixin PaynymInterface final vSizeForWithChange = BigInt.from( (await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: satoshisBeingUsed - amountToSend.raw, )).item2, ); @@ -584,7 +588,7 @@ mixin PaynymInterface feeForWithChange) { var txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: changeAmount, ); @@ -597,7 +601,7 @@ mixin PaynymInterface feeBeingPaid += BigInt.one; txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: changeAmount, ); } @@ -605,10 +609,11 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -616,7 +621,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -626,7 +631,7 @@ mixin PaynymInterface // than the dust limit. Try without change final txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, ); @@ -635,10 +640,11 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -646,7 +652,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -657,7 +663,7 @@ mixin PaynymInterface // build without change here final txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, ); @@ -666,10 +672,11 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -677,7 +684,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -706,7 +713,7 @@ mixin PaynymInterface // equal to its vSize Future> _createNotificationTx({ required String targetPaymentCodeString, - required List utxoSigningData, + required List inputsWithKeys, required BigInt change, BigInt? overrideAmountForTesting, }) async { @@ -717,7 +724,7 @@ mixin PaynymInterface ); final myCode = await getPaymentCode(isSegwit: false); - final utxo = utxoSigningData.first.utxo; + final utxo = inputsWithKeys.first.utxo; final txPoint = utxo.txid.toUint8ListFromHex.reversed.toList(); final txPointIndex = utxo.vout; @@ -726,10 +733,10 @@ mixin PaynymInterface final buffer = rev.buffer.asByteData(); buffer.setUint32(txPoint.length, txPointIndex, Endian.little); - final myKeyPair = utxoSigningData.first.keyPair!; + final myKeyPair = inputsWithKeys.first.key!; final S = SecretPoint( - myKeyPair.privateKey.data, + myKeyPair.privateKey!.data, targetPaymentCode.notificationPublicKey(), ); @@ -756,8 +763,8 @@ mixin PaynymInterface outputs: [], ); - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < inputsWithKeys.length; i++) { + final txid = inputsWithKeys[i].utxo.txid; final hash = Uint8List.fromList( txid.toUint8ListFromHex.reversed.toList(), @@ -765,13 +772,13 @@ mixin PaynymInterface final prevOutpoint = coinlib.OutPoint( hash, - utxoSigningData[i].utxo.vout, + inputsWithKeys[i].utxo.vout, ); final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), + BigInt.from(inputsWithKeys[i].utxo.value), coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, + inputsWithKeys[i].utxo.address!, cryptoCurrency.networkParams, ), ); @@ -780,12 +787,12 @@ mixin PaynymInterface final coinlib.Input input; - switch (utxoSigningData[i].derivePathType) { + switch (inputsWithKeys[i].derivePathType) { case DerivePathType.bip44: case DerivePathType.bch44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: 0xffffffff - 1, ); @@ -803,7 +810,7 @@ mixin PaynymInterface case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: 0xffffffff - 1, ); @@ -812,7 +819,7 @@ mixin PaynymInterface default: throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + "Unknown derivation path type found: ${inputsWithKeys[i].derivePathType}", ); } @@ -860,21 +867,21 @@ mixin PaynymInterface clTx = clTx.signTaproot( inputN: 0, - key: taproot.tweakPrivateKey(myKeyPair.privateKey), + key: taproot.tweakPrivateKey(myKeyPair.privateKey!), prevOuts: prevOuts, ); } else if (clTx.inputs[0] is coinlib.LegacyWitnessInput) { clTx = clTx.signLegacyWitness( inputN: 0, - key: myKeyPair.privateKey, + key: myKeyPair.privateKey!, value: BigInt.from(utxo.value), ); } else if (clTx.inputs[0] is coinlib.LegacyInput) { - clTx = clTx.signLegacy(inputN: 0, key: myKeyPair.privateKey); + clTx = clTx.signLegacy(inputN: 0, key: myKeyPair.privateKey!); } else if (clTx.inputs[0] is coinlib.TaprootSingleScriptSigInput) { clTx = clTx.signTaprootSingleScriptSig( inputN: 0, - key: myKeyPair.privateKey, + key: myKeyPair.privateKey!, prevOuts: prevOuts, ); } else { @@ -884,13 +891,13 @@ mixin PaynymInterface } // sign rest of possible inputs - for (int i = 1; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; + for (int i = 1; i < inputsWithKeys.length; i++) { + final value = BigInt.from(inputsWithKeys[i].utxo.value); + final key = inputsWithKeys[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, + internalKey: inputsWithKeys[i].key!.publicKey, ); clTx = clTx.signTaproot( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart index 2e609aeb0..03a8b6dd6 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:isar/isar.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; import '../../../utilities/amount/amount.dart'; @@ -46,14 +47,15 @@ mixin RbfInterface required TransactionV2 oldTransaction, required int newRate, }) async { - final note = await mainDB.isar.transactionNotes - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(oldTransaction.txid) - .findFirst(); + final note = + await mainDB.isar.transactionNotes + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(oldTransaction.txid) + .findFirst(); - final Set utxos = {}; + final Set utxos = {}; for (final input in oldTransaction.inputs) { final utxo = UTXO( walletId: walletId, @@ -71,7 +73,7 @@ mixin RbfInterface address: input.addresses.first, ); - utxos.add(utxo); + utxos.add(StandardInput(utxo)); } final List recipients = []; @@ -86,43 +88,44 @@ mixin RbfInterface final isChange = addressModel?.subType == AddressSubType.change; recipients.add( - ( + TxRecipient( address: address, amount: Amount( - rawValue: output.value, - fractionDigits: cryptoCurrency.fractionDigits), + rawValue: output.value, + fractionDigits: cryptoCurrency.fractionDigits, + ), isChange: isChange, + addressType: cryptoCurrency.getAddressType(address)!, ), ); } - final oldFee = oldTransaction - .getFee(fractionDigits: cryptoCurrency.fractionDigits) - .raw; - final inSum = utxos - .map((e) => BigInt.from(e.value)) - .fold(BigInt.zero, (p, e) => p + e); + final oldFee = + oldTransaction + .getFee(fractionDigits: cryptoCurrency.fractionDigits) + .raw; + final inSum = utxos.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); final noChange = recipients.map((e) => e.isChange).fold(false, (p, e) => p || e) == - false; - final otherAvailableUtxos = await mainDB - .getUTXOs(walletId) - .filter() - .isBlockedEqualTo(false) - .and() - .group( - (q) => q.usedIsNull().or().usedEqualTo(false), - ) - .findAll(); + false; + final otherAvailableUtxos = + await mainDB + .getUTXOs(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedIsNull().or().usedEqualTo(false)) + .findAll(); final height = await chainHeight; otherAvailableUtxos.removeWhere( - (e) => !e.isConfirmed( - height, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ), + (e) => + !e.isConfirmed( + height, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), ); TxData txData = TxData( @@ -138,13 +141,14 @@ mixin RbfInterface // safe to assume send all? txData = txData.copyWith( recipients: [ - ( + TxRecipient( address: recipients.first.address, amount: Amount( rawValue: inSum, fractionDigits: cryptoCurrency.fractionDigits, ), isChange: false, + addressType: recipients.first.addressType, ), ], ); @@ -159,8 +163,9 @@ mixin RbfInterface throw Exception("New fee in RBF has not changed at all"); } - final indexOfChangeOutput = - txData.recipients!.indexWhere((e) => e.isChange); + final indexOfChangeOutput = txData.recipients!.indexWhere( + (e) => e.isChange, + ); final removed = txData.recipients!.removeAt(indexOfChangeOutput); @@ -172,13 +177,11 @@ mixin RbfInterface // update recipients txData.recipients!.insert( indexOfChangeOutput, - ( - address: removed.address, + removed.copyWith( amount: Amount( rawValue: newChangeAmount, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: removed.isChange, ), ); Logging.instance.d( @@ -206,7 +209,7 @@ mixin RbfInterface fractionDigits: cryptoCurrency.fractionDigits, ), ), - utxoSigningData: await fetchBuildTxData(txData.utxos!.toList()), + inputsWithKeys: await addSigningKeys(txData.utxos!.toList()), ); // if change amount is negative @@ -232,19 +235,17 @@ mixin RbfInterface } txData.recipients!.insert( indexOfChangeOutput, - ( - address: removed.address, + removed.copyWith( amount: Amount( rawValue: newChangeAmount, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: removed.isChange, ), ); final newUtxoSet = { - ...txData.utxos!, - ...extraUtxos, + ...txData.utxos!.whereType(), + ...extraUtxos.map((e) => StandardInput(e)), }; // TODO: remove assert @@ -264,7 +265,7 @@ mixin RbfInterface fractionDigits: cryptoCurrency.fractionDigits, ), ), - utxoSigningData: await fetchBuildTxData(newUtxoSet.toList()), + inputsWithKeys: await addSigningKeys(newUtxoSet.toList()), ); } } else { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 347bf9f2c..671c79770 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -5,26 +5,30 @@ import 'dart:math'; import 'package:bitcoindart/bitcoindart.dart' as btc; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart' as spark +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart' + as spark show Log; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:logger/logger.dart'; +import '../../../db/drift/database.dart' show Drift; import '../../../db/sqlite/firo_cache.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; -import '../../../models/signing_data.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; +import '../../../services/spark_names_service.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/prefs.dart'; +import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../isar/models/spark_coin.dart'; import '../../isar/models/wallet_info.dart'; @@ -57,15 +61,10 @@ String _hashTag(String tag) { void initSparkLogging(Level level) { final levels = Level.values.where((e) => e >= level).map((e) => e.name); - spark.Log.levels - .addAll(LoggingLevel.values.where((e) => levels.contains(e.name))); - spark.Log.onLog = ( - level, - value, { - error, - stackTrace, - required time, - }) { + spark.Log.levels.addAll( + LoggingLevel.values.where((e) => levels.contains(e.name)), + ); + spark.Log.onLog = (level, value, {error, stackTrace, required time}) { Logging.instance.log( level.getLoggerLevel(), value, @@ -84,27 +83,24 @@ abstract class _SparkIsolate { static Future initialize() async { final level = Prefs.instance.logLevel; - _isolate = await Isolate.spawn( - (SendPort sendPort) { - initSparkLogging(level); // ensure logging is set up in isolate + _isolate = await Isolate.spawn((SendPort sendPort) { + initSparkLogging(level); // ensure logging is set up in isolate - final receivePort = ReceivePort(); + final receivePort = ReceivePort(); - sendPort.send(receivePort.sendPort); + sendPort.send(receivePort.sendPort); - receivePort.listen((message) async { - if (message is List && message.length == 3) { - final function = message[0] as Function; - final argument = message[1]; - final replyPort = message[2] as SendPort; + receivePort.listen((message) async { + if (message is List && message.length == 3) { + final function = message[0] as Function; + final argument = message[1]; + final replyPort = message[2] as SendPort; - final result = await function(argument); - replyPort.send(result); - } - }); - }, - _receivePort.sendPort, - ); + final result = await function(argument); + replyPort.send(result); + } + }); + }, _receivePort.sendPort); _sendPort = await _receivePort.first as SendPort; } @@ -139,8 +135,7 @@ mixin SparkInterface static bool validateSparkAddress({ required String address, required bool isTestNet, - }) => - LibSpark.validateAddress(address: address, isTestNet: isTestNet); + }) => LibSpark.validateAddress(address: address, isTestNet: isTestNet); Future hashTag(String tag) async { try { @@ -192,19 +187,20 @@ mixin SparkInterface @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.spark) - .or() - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.spark) + .or() + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -265,20 +261,22 @@ mixin SparkInterface ); } else { // fetch spendable spark coins - final coins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(false) - .and() - .heightIsNotNull() - .and() - .not() - .valueIntStringEqualTo("0") - .findAll(); - - final available = - coins.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); + final coins = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .and() + .heightIsNotNull() + .and() + .not() + .valueIntStringEqualTo("0") + .findAll(); + + final available = coins + .map((e) => e.value) + .fold(BigInt.zero, (p, e) => p + e); if (amount.raw > available) { return Amount( @@ -288,16 +286,17 @@ mixin SparkInterface } // prepare coin data for ffi - final serializedCoins = coins - .map( - (e) => ( - serializedCoin: e.serializedCoinB64!, - serializedCoinContext: e.contextB64!, - groupId: e.groupId, - height: e.height!, - ), - ) - .toList(); + final serializedCoins = + coins + .map( + (e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + ), + ) + .toList(); final root = await getRootHDNode(); final String derivationPath; @@ -315,6 +314,8 @@ mixin SparkInterface serializedCoins: serializedCoins, // privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), privateRecipientsCount: 1, // ROUGHLY! + utxoNum: 0, // TODO not zero? + additionalTxSize: 0, // spark name script size ); if (estimate < 0) { @@ -329,9 +330,7 @@ mixin SparkInterface } /// Spark to Spark/Transparent (spend) creation - Future prepareSendSpark({ - required TxData txData, - }) async { + Future prepareSendSpark({required TxData txData}) async { // There should be at least one output. if (!(txData.recipients?.isNotEmpty == true || txData.sparkRecipients?.isNotEmpty == true)) { @@ -343,14 +342,15 @@ mixin SparkInterface throw Exception("Spark shielded output limit exceeded."); } - final transparentSumOut = - (txData.recipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e, - ); + final transparentSumOut = (txData.recipients ?? []) + .map((e) => e.amount) + .fold( + 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 @@ -365,29 +365,31 @@ mixin SparkInterface ); } - final sparkSumOut = - (txData.sparkRecipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e, - ); + final sparkSumOut = (txData.sparkRecipients ?? []) + .map((e) => e.amount) + .fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e, + ); final txAmount = transparentSumOut + sparkSumOut; // fetch spendable spark coins - final coins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(false) - .and() - .heightIsNotNull() - .and() - .not() - .valueIntStringEqualTo("0") - .findAll(); + final coins = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .and() + .heightIsNotNull() + .and() + .not() + .valueIntStringEqualTo("0") + .findAll(); final available = info.cachedBalanceTertiary.spendable; @@ -398,16 +400,17 @@ mixin SparkInterface final bool isSendAll = available == txAmount; // prepare coin data for ffi - final serializedCoins = coins - .map( - (e) => ( - serializedCoin: e.serializedCoinB64!, - serializedCoinContext: e.contextB64!, - groupId: e.groupId, - height: e.height!, - ), - ) - .toList(); + final serializedCoins = + coins + .map( + (e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + ), + ) + .toList(); final currentId = await electrumXClient.getSparkLatestCoinId(); final List> setMaps = []; @@ -433,43 +436,36 @@ mixin SparkInterface "blockHash": info.blockHash, "setHash": info.setHash, "coinGroupID": i, - "coins": resultSet - .map( - (e) => [ - e.serialized, - e.txHash, - e.context, - ], - ) - .toList(), + "coins": + resultSet.map((e) => [e.serialized, e.txHash, e.context]).toList(), }; setData["coinGroupID"] = i; setMaps.add(setData); - idAndBlockHashes.add( - ( - groupId: i, - blockHash: setData["blockHash"] as String, - ), - ); + idAndBlockHashes.add(( + groupId: i, + blockHash: setData["blockHash"] as String, + )); } - 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(), - ), - ) - .toList(); + 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(), + ), + ) + .toList(); final root = await getRootHDNode(); final String derivationPath; @@ -480,31 +476,16 @@ mixin SparkInterface } final privateKey = root.derivePath(derivationPath).privateKey.data; - final txb = btc.TransactionBuilder( - network: _bitcoinDartNetwork, - ); + final txb = btc.TransactionBuilder(network: _bitcoinDartNetwork); txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); - List< - ({ - String address, - Amount amount, - bool isChange, - })>? recipientsWithFeeSubtracted; - List< - ({ - String address, - Amount amount, - String memo, - bool isChange, - })>? sparkRecipientsWithFeeSubtracted; - final recipientCount = (txData.recipients - ?.where( - (e) => e.amount.raw > BigInt.zero, - ) - .length ?? - 0); + List? recipientsWithFeeSubtracted; + List<({String address, Amount amount, String memo, bool isChange})>? + sparkRecipientsWithFeeSubtracted; + final recipientCount = + (txData.recipients?.where((e) => e.amount.raw > BigInt.zero).length ?? + 0); final totalRecipientCount = recipientCount + (txData.sparkRecipients?.length ?? 0); final BigInt estimatedFee; @@ -516,6 +497,8 @@ mixin SparkInterface subtractFeeFromAmount: true, serializedCoins: serializedCoins, privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), + utxoNum: recipientCount, + additionalTxSize: 0, // name script size ); estimatedFee = BigInt.from(estFee); } else { @@ -530,18 +513,17 @@ mixin SparkInterface } for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { - sparkRecipientsWithFeeSubtracted!.add( - ( - address: txData.sparkRecipients![i].address, - amount: Amount( - rawValue: txData.sparkRecipients![i].amount.raw - - (estimatedFee ~/ BigInt.from(totalRecipientCount)), - fractionDigits: cryptoCurrency.fractionDigits, - ), - memo: txData.sparkRecipients![i].memo, - isChange: sparkChangeAddress == txData.sparkRecipients![i].address, + sparkRecipientsWithFeeSubtracted!.add(( + address: txData.sparkRecipients![i].address, + amount: Amount( + rawValue: + txData.sparkRecipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(totalRecipientCount)), + fractionDigits: cryptoCurrency.fractionDigits, ), - ); + memo: txData.sparkRecipients![i].memo, + isChange: sparkChangeAddress == txData.sparkRecipients![i].address, + )); } // temp tx data to show in gui while waiting for real data from server @@ -553,14 +535,13 @@ mixin SparkInterface continue; } recipientsWithFeeSubtracted!.add( - ( - address: txData.recipients![i].address, + txData.recipients![i].copyWith( amount: Amount( - rawValue: txData.recipients![i].amount.raw - + rawValue: + txData.recipients![i].amount.raw - (estimatedFee ~/ BigInt.from(totalRecipientCount)), fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: txData.recipients![i].isChange, ), ); @@ -577,10 +558,9 @@ mixin SparkInterface OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: scriptPubKey.toHex, valueStringSats: recipientsWithFeeSubtracted[i].amount.raw.toString(), - addresses: [ - recipientsWithFeeSubtracted[i].address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [recipientsWithFeeSubtracted[i].address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -598,10 +578,9 @@ mixin SparkInterface OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: Uint8List.fromList([OP_SPARKSMINT]).toHex, valueStringSats: recip.amount.raw.toString(), - addresses: [ - recip.address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [recip.address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -624,48 +603,112 @@ mixin SparkInterface ); extractedTx.setPayload(Uint8List(0)); - final spend = await computeWithLibSparkLogging( - _createSparkSend, - ( + ({Uint8List script, int size})? noProofNameTxData; + if (txData.sparkNameInfo != null) { + noProofNameTxData = LibSpark.createSparkNameScript( + sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks, + name: txData.sparkNameInfo!.name, + additionalInfo: txData.sparkNameInfo!.additionalInfo, + scalarHex: extractedTx.getId(), privateKeyHex: privateKey.toHex, - index: kDefaultSparkIndex, - recipients: txData.recipients - ?.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, - ), - ) - .toList() ?? - [], - serializedCoins: serializedCoins, - allAnonymitySets: allAnonymitySets, - idAndBlockHashes: idAndBlockHashes - .map( - (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash)), - ) - .toList(), - txHash: extractedTx.getHash(), - ), - ); + spendKeyIndex: kDefaultSparkIndex, + diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex, + isTestNet: cryptoCurrency.network != CryptoCurrencyNetwork.main, + ignoreProof: true, + hashFailSafe: 0, + ); + } + + final spend = await computeWithLibSparkLogging(_createSparkSend, ( + privateKeyHex: privateKey.toHex, + index: kDefaultSparkIndex, + recipients: + txData.recipients + ?.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, + ), + ) + .toList() ?? + [], + serializedCoins: serializedCoins, + allAnonymitySets: allAnonymitySets, + idAndBlockHashes: + idAndBlockHashes + .map( + (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash)), + ) + .toList(), + txHash: extractedTx.getHash(), + additionalTxSize: + txData.sparkNameInfo == null ? 0 : noProofNameTxData!.size, + )); for (final outputScript in spend.outputScripts) { extractedTx.addOutput(outputScript, 0); } extractedTx.setPayload(spend.serializedSpendPayload); + + if (txData.sparkNameInfo != null) { + // this is name reg tx + + extractedTx.setPayload( + Uint8List.fromList([ + ...spend.serializedSpendPayload, + ...noProofNameTxData!.script, + ]), + ); + + final hash = extractedTx.getId(); + + ({Uint8List script, int size})? nameScriptData; + int hashFailSafe = 0; + while (nameScriptData == null) { + try { + nameScriptData = LibSpark.createSparkNameScript( + sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks, + name: txData.sparkNameInfo!.name, + additionalInfo: txData.sparkNameInfo!.additionalInfo, + scalarHex: hash, + privateKeyHex: privateKey.toHex, + spendKeyIndex: kDefaultSparkIndex, + diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex, + isTestNet: cryptoCurrency.network != CryptoCurrencyNetwork.main, + ignoreProof: false, + hashFailSafe: hashFailSafe, + ); + break; + } catch (e) { + if (e.toString() != "Exception: hash fail") { + rethrow; + } + hashFailSafe++; + } + } + + extractedTx.setPayload( + Uint8List.fromList([ + ...spend.serializedSpendPayload, + ...nameScriptData.script, + ]), + ); + } + final rawTxHex = extractedTx.toHex(); if (isSendAll) { @@ -687,10 +730,11 @@ mixin SparkInterface sequence: 0xffffffff, outpoint: null, addresses: [], - valueStringSats: tempOutputs - .map((e) => e.value) - .fold(fee.raw, (p, e) => p + e) - .toString(), + valueStringSats: + tempOutputs + .map((e) => e.value) + .fold(fee.raw, (p, e) => p + e) + .toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -709,12 +753,10 @@ mixin SparkInterface usedCoin.height == e.height && usedCoin.groupId == e.groupId && base64Decode(e.serializedCoinB64!).toHex.startsWith( - base64Decode(usedCoin.serializedCoin).toHex, - ), + base64Decode(usedCoin.serializedCoin).toHex, + ), ) - .copyWith( - isUsed: true, - ), + .copyWith(isUsed: true), ); } catch (_) { throw Exception( @@ -735,15 +777,12 @@ mixin SparkInterface timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(tempInputs), outputs: List.unmodifiable(tempOutputs), - type: tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) - ? TransactionType.sentToSelf - : TransactionType.outgoing, + type: + tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.sparkSpend, - otherData: jsonEncode( - { - "overrideFee": fee.toJsonString(), - }, - ), + otherData: jsonEncode({"overrideFee": fee.toJsonString()}), height: null, version: 3, ), @@ -752,9 +791,7 @@ mixin SparkInterface } // this may not be needed for either mints or spends or both - Future confirmSendSpark({ - required TxData txData, - }) async { + Future confirmSendSpark({required TxData txData}) async { try { Logging.instance.d("confirmSend txData: $txData"); @@ -821,11 +858,7 @@ mixin SparkInterface for (final data in sparkDataToCheck) { for (int i = 0; i < data.coins.length; i++) { - rawCoins.add([ - data.coins[i], - data.txid, - data.serialContext.first, - ]); + rawCoins.add([data.coins[i], data.txid, data.serialContext.first]); } checkedTxids.add(data.txid); @@ -836,21 +869,23 @@ mixin SparkInterface // if there is new data we try and identify the coins if (rawCoins.isNotEmpty) { // run identify off main isolate - final myCoins = await computeWithLibSparkLogging( - _identifyCoins, - ( - anonymitySetCoins: rawCoins, - groupId: groupId, - privateKeyHexSet: privateKeyHexSet, - walletId: walletId, - isTestNet: cryptoCurrency.network.isTestNet, - ), - ); + final myCoins = await computeWithLibSparkLogging(_identifyCoins, ( + anonymitySetCoins: rawCoins, + groupId: groupId, + privateKeyHexSet: privateKeyHexSet, + walletId: walletId, + isTestNet: cryptoCurrency.network.isTestNet, + )); // add checked txids after identification _mempoolTxidsChecked.addAll(checkedTxids); - result.addAll(myCoins); + for (final coin in myCoins) { + final match = sparkDataToCheck.firstWhere( + (e) => e.serialContext.contains(coin.contextB64!), + ); + result.add(coin.copyWith(isLocked: match.isLocked)); + } } return result; @@ -872,21 +907,13 @@ mixin SparkInterface // returns next percent double _triggerEventHelper(double current, double increment) { refreshingPercent = current; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - current, - walletId, - ), - ); + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(current, walletId)); return current + increment; } // Linearly make calls so there is less chance of timing out or otherwise breaking Future refreshSparkData( - ( - double startingPercent, - double endingPercent, - )? refreshProgressRange, + (double startingPercent, double endingPercent)? refreshProgressRange, ) async { final start = DateTime.now(); try { @@ -898,9 +925,9 @@ mixin SparkInterface for (int id = 1; id < latestGroupId; id++) { final setExists = await FiroCacheCoordinator.checkSetInfoForGroupIdExists( - id, - cryptoCurrency.network, - ); + id, + cryptoCurrency.network, + ); if (!setExists) { groupIds.add(id); } @@ -908,7 +935,8 @@ mixin SparkInterface } groupIds.add(latestGroupId); - final steps = groupIds.length + + final steps = + groupIds.length + 1 // get used tags step + 1 // check updated cache step @@ -919,9 +947,10 @@ mixin SparkInterface + 1; // update balance - final percentIncrement = refreshProgressRange == null - ? null - : (refreshProgressRange.$2 - refreshProgressRange.$1) / steps; + final percentIncrement = + refreshProgressRange == null + ? null + : (refreshProgressRange.$2 - refreshProgressRange.$1) / steps; double currentPercent = refreshProgressRange?.$1 ?? 0; // fetch and update process for each set groupId as required @@ -963,8 +992,8 @@ mixin SparkInterface // after that block. final groupIdBlockHashMap = info.otherData[WalletInfoKeys.firoSparkCacheSetBlockHashCache] - as Map? ?? - {}; + as Map? ?? + {}; // iterate through the cache, fetching spark coin data that hasn't been // processed by this wallet yet @@ -977,19 +1006,14 @@ mixin SparkInterface ); final anonymitySetResult = await FiroCacheCoordinator.getSetCoinsForGroupId( - i, - afterBlockHash: lastCheckedHash, - network: cryptoCurrency.network, - ); - final coinsRaw = anonymitySetResult - .map( - (e) => [ - e.serialized, - e.txHash, - e.context, - ], - ) - .toList(); + i, + afterBlockHash: lastCheckedHash, + network: cryptoCurrency.network, + ); + final coinsRaw = + anonymitySetResult + .map((e) => [e.serialized, e.txHash, e.context]) + .toList(); if (coinsRaw.isNotEmpty) { rawCoinsBySetId[i] = coinsRaw; @@ -1005,33 +1029,36 @@ mixin SparkInterface // get address(es) to get the private key hex strings required for // identifying spark coins - final sparkAddresses = await mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .typeEqualTo(AddressType.spark) - .findAll(); + final sparkAddresses = + await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .findAll(); final root = await getRootHDNode(); - final Set privateKeyHexSet = sparkAddresses - .map( - (e) => - root.derivePath(e.derivationPath!.value).privateKey.data.toHex, - ) - .toSet(); + final Set privateKeyHexSet = + sparkAddresses + .map( + (e) => + root + .derivePath(e.derivationPath!.value) + .privateKey + .data + .toHex, + ) + .toSet(); // try to identify any coins in the unchecked set data final List newlyIdCoins = []; for (final groupId in rawCoinsBySetId.keys) { - final myCoins = await computeWithLibSparkLogging( - _identifyCoins, - ( - anonymitySetCoins: rawCoinsBySetId[groupId]!, - groupId: groupId, - privateKeyHexSet: privateKeyHexSet, - walletId: walletId, - isTestNet: cryptoCurrency.network.isTestNet, - ), - ); + final myCoins = await computeWithLibSparkLogging(_identifyCoins, ( + anonymitySetCoins: rawCoinsBySetId[groupId]!, + groupId: groupId, + privateKeyHexSet: privateKeyHexSet, + walletId: walletId, + isTestNet: cryptoCurrency.network.isTestNet, + )); newlyIdCoins.addAll(myCoins); } // if any were found, add to database @@ -1066,14 +1093,15 @@ mixin SparkInterface } // get unused and or unconfirmed coins from db - final coinsToCheck = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .heightIsNull() - .or() - .isUsedEqualTo(false) - .findAll(); + final coinsToCheck = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .heightIsNull() + .or() + .isUsedEqualTo(false) + .findAll(); Set? spentCoinTags; // only fetch tags from db if we need them to compare against any items @@ -1104,9 +1132,10 @@ mixin SparkInterface checked = coin; } } else { - checked = spentCoinTags!.contains(coin.lTagHash) - ? coin.copyWith(isUsed: true) - : coin; + checked = + spentCoinTags!.contains(coin.lTagHash) + ? coin.copyWith(isUsed: true) + : coin; } checkedCoins.add(checked); @@ -1126,12 +1155,15 @@ mixin SparkInterface final currentHeight = await chainHeight; // get all unused coins to update wallet spark balance - final unusedCoins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(false) - .findAll(); + final unusedCoins = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .findAll(); + + final sparkNamesUpdateFuture = refreshSparkNames(); final total = Amount( rawValue: unusedCoins @@ -1164,9 +1196,14 @@ mixin SparkInterface newBalance: sparkBalance, isar: mainDB.isar, ); + + await sparkNamesUpdateFuture; } catch (e, s) { - Logging.instance - .e("$runtimeType $walletId ${info.name}: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType $walletId ${info.name}: ", + error: e, + stackTrace: s, + ); rethrow; } finally { Logging.instance.d( @@ -1177,13 +1214,14 @@ mixin SparkInterface } Future> getSparkSpendTransactionIds() async { - final tags = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(true) - .lTagHashProperty() - .findAll(); + final tags = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(true) + .lTagHashProperty() + .findAll(); final pairs = await FiroCacheCoordinator.getUsedCoinTxidsFor( tags: tags, @@ -1195,9 +1233,7 @@ mixin SparkInterface /// Should only be called within the standard wallet [recover] function due to /// mutex locking. Otherwise behaviour MAY be undefined. - Future recoverSparkWallet({ - required int latestSparkCoinId, - }) async { + Future recoverSparkWallet({required int latestSparkCoinId}) async { // generate spark addresses if non existing if (await getCurrentReceivingSparkAddress() == null) { final address = await generateNextSparkAddress(); @@ -1207,12 +1243,124 @@ mixin SparkInterface try { await refreshSparkData(null); } catch (e, s) { - Logging.instance - .e("$runtimeType $walletId ${info.name}: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType $walletId ${info.name}: ", + error: e, + stackTrace: s, + ); rethrow; } } + Future refreshSparkNames() async { + try { + Logging.instance.i("Refreshing spark names for $walletId ${info.name}"); + + final db = Drift.get(walletId); + final myNameStrings = + await db.managers.sparkNames.map((e) => e.name).get(); + final names = await electrumXClient.getSparkNames(); + + // start update shared cache of all names + final nameUpdateFuture = SparkNamesService.update( + names, + network: cryptoCurrency.network, + ); + + final myAddresses = + (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .valueProperty() + .findAll()) + .toSet(); + + // some look ahead + // TODO revisit this and clean up (track pre gen'd addresses instead of generating every time) + // arbitrary number of addresses + const lookAheadCount = 100; + final highestStoredDiversifier = + (await getCurrentReceivingSparkAddress())?.derivationIndex; + + final root = await getRootHDNode(); + final String derivationPath; + if (cryptoCurrency.network.isTestNet) { + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; + } else { + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; + } + final keys = root.derivePath(derivationPath); + + // default to starting at 1 if none found + int diversifier = (highestStoredDiversifier ?? 0) + 1; + + final maxDiversifier = diversifier + lookAheadCount; + + while (diversifier < maxDiversifier) { + // change address check + if (diversifier == kSparkChange) { + diversifier++; + } + final addressString = await LibSpark.getAddress( + privateKey: keys.privateKey.data, + index: kDefaultSparkIndex, + diversifier: diversifier, + isTestNet: cryptoCurrency.network.isTestNet, + ); + + myAddresses.add(addressString); + + diversifier++; + } + + names.retainWhere( + (e) => + myAddresses.contains(e.address) && !myNameStrings.contains(e.name), + ); + Logging.instance.d("Found $names new spark names"); + + if (names.isNotEmpty) { + final List< + ({ + String name, + String address, + int validUntil, + String? additionalInfo, + }) + > + data = []; + + for (final name in names) { + final info = await electrumXClient.getSparkNameData( + sparkName: name.name, + ); + + data.add(( + name: name.name, + address: name.address, + validUntil: info.validUntil, + additionalInfo: info.additionalInfo, + )); + } + + await db.upsertSparkNames(data); + } + + // finally ensure shared cache update has completed + await nameUpdateFuture; + } catch (e, s) { + Logging.instance.e( + "refreshing spark names for $walletId \"${info.name}\" failed", + error: e, + stackTrace: s, + ); + } + } + // modelled on CSparkWallet::CreateSparkMintTransactions https://github.com/firoorg/firo/blob/39c41e5e7ec634ced3700fe3f4f5509dc2e480d0/src/spark/sparkwallet.cpp#L752 Future> _createSparkMintTransactions({ required List availableUtxos, @@ -1229,8 +1377,9 @@ mixin SparkInterface // addresses when confirming the transactions later as well assert(outputs.length == 1); - BigInt valueToMint = - outputs.map((e) => e.value).reduce((value, element) => value + element); + BigInt valueToMint = outputs + .map((e) => e.value) + .reduce((value, element) => value + element); if (valueToMint <= BigInt.zero) { throw Exception("Cannot mint amount=$valueToMint"); @@ -1251,9 +1400,10 @@ mixin SparkInterface // setup some vars int nChangePosInOut = -1; final int nChangePosRequest = nChangePosInOut; - List outputs_ = outputs - .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) - .toList(); // deep copy + List outputs_ = + outputs + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); // deep copy final feesObject = await fees; final currentHeight = await chainHeight; final random = Random.secure(); @@ -1262,11 +1412,12 @@ mixin SparkInterface valueAndUTXOs.shuffle(random); while (valueAndUTXOs.isNotEmpty) { - final lockTime = random.nextInt(10) == 0 - ? max(0, currentHeight - random.nextInt(100)) - : currentHeight; + final lockTime = + random.nextInt(10) == 0 + ? max(0, currentHeight - random.nextInt(100)) + : currentHeight; const txVersion = 1; - final List vin = []; + final List vin = []; final List<(dynamic, int, String?)> vout = []; BigInt nFeeRet = BigInt.zero; @@ -1279,7 +1430,7 @@ mixin SparkInterface } BigInt nValueToSelect, mintedValue; - final List setCoins = []; + final List setCoins = []; bool skipCoin = false; // Start with no fee and loop until there is enough fee @@ -1311,9 +1462,10 @@ mixin SparkInterface setCoins.clear(); // deep copy - final remainingOutputs = outputs_ - .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) - .toList(); + final remainingOutputs = + outputs_ + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); final List singleTxOutputs = []; if (autoMintAll) { @@ -1328,8 +1480,10 @@ mixin SparkInterface BigInt remainingMintValue = BigInt.parse(mintedValue.toString()); while (remainingMintValue > BigInt.zero) { - final singleMintValue = - _min(remainingMintValue, remainingOutputs.first.value); + final singleMintValue = _min( + remainingMintValue, + remainingOutputs.first.value, + ); singleTxOutputs.add( MutableSparkRecipient( remainingOutputs.first.address, @@ -1372,15 +1526,16 @@ mixin SparkInterface // Generate dummy mint coins to save time final dummyRecipients = LibSpark.createSparkMintRecipients( - outputs: singleTxOutputs - .map( - (e) => ( - sparkAddress: e.address, - value: e.value.toInt(), - memo: "", - ), - ) - .toList(), + outputs: + singleTxOutputs + .map( + (e) => ( + sparkAddress: e.address, + value: e.value.toInt(), + memo: "", + ), + ) + .toList(), serialContext: Uint8List(0), generate: false, ); @@ -1393,20 +1548,22 @@ mixin SparkInterface 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 BigInt nValueIn = BigInt.zero; for (final utxo in itr) { if (nValueToSelect > nValueIn) { - setCoins.add((await fetchBuildTxData([utxo])).first); + setCoins.add( + (await addSigningKeys([ + StandardInput(utxo), + ])).whereType().first, + ); nValueIn += BigInt.from(utxo.value); } } @@ -1429,10 +1586,11 @@ mixin SparkInterface } final changeAddress = await getCurrentChangeAddress(); - vout.insert( - nChangePosInOut, - (changeAddress!.value, nChange.toInt(), null), - ); + vout.insert(nChangePosInOut, ( + changeAddress!.value, + nChange.toInt(), + null, + )); } } @@ -1445,47 +1603,45 @@ mixin SparkInterface for (final sd in setCoins) { vin.add(sd); - final pubKey = sd.keyPair!.publicKey.data; + final pubKey = sd.key!.publicKey.data; final btc.PaymentData? data; switch (sd.derivePathType) { case DerivePathType.bip44: - data = btc - .P2PKH( - data: btc.PaymentData( - pubkey: pubKey, - ), - network: _bitcoinDartNetwork, - ) - .data; + 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; + 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; + data = + btc + .P2WPKH( + data: btc.PaymentData(pubkey: pubKey), + network: _bitcoinDartNetwork, + ) + .data; break; case DerivePathType.bip86: @@ -1511,9 +1667,9 @@ mixin SparkInterface dummyTxb.sign( vin: i, keyPair: btc.ECPair.fromPrivateKey( - setCoins[i].keyPair!.privateKey.data, + setCoins[i].key!.privateKey!.data, network: _bitcoinDartNetwork, - compressed: setCoins[i].keyPair!.privateKey.compressed, + compressed: setCoins[i].key!.privateKey!.compressed, ), witnessValue: setCoins[i].utxo.value, @@ -1530,10 +1686,7 @@ mixin SparkInterface } final nFeeNeeded = BigInt.from( - estimateTxFee( - vSize: nBytes, - feeRatePerKB: feesObject.medium, - ), + estimateTxFee(vSize: nBytes, feeRatePerKB: feesObject.medium), ); // One day we'll do this properly if (nFeeRet >= nFeeNeeded) { @@ -1548,25 +1701,19 @@ mixin SparkInterface // Generate real mint coins final serialContext = LibSpark.serializeMintContext( - inputs: setCoins - .map( - (e) => ( - e.utxo.txid, - e.utxo.vout, - ), - ) - .toList(), + inputs: setCoins.map((e) => (e.utxo.txid, e.utxo.vout)).toList(), ); final recipients = LibSpark.createSparkMintRecipients( - outputs: singleTxOutputs - .map( - (e) => ( - sparkAddress: e.address, - memo: e.memo, - value: e.value.toInt(), - ), - ) - .toList(), + outputs: + singleTxOutputs + .map( + (e) => ( + sparkAddress: e.address, + memo: e.memo, + value: e.value.toInt(), + ), + ) + .toList(), serialContext: serialContext, generate: true, ); @@ -1591,9 +1738,10 @@ mixin SparkInterface } // deep copy - outputs_ = remainingOutputs - .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) - .toList(); + outputs_ = + remainingOutputs + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); break; // Done, enough fee included. } @@ -1616,47 +1764,45 @@ mixin SparkInterface txb.setVersion(txVersion); txb.setLockTime(lockTime); for (final input in vin) { - final pubKey = input.keyPair!.publicKey.data; + final pubKey = input.key!.publicKey.data; final btc.PaymentData? data; switch (input.derivePathType) { case DerivePathType.bip44: - data = btc - .P2PKH( - data: btc.PaymentData( - pubkey: pubKey, - ), - network: _bitcoinDartNetwork, - ) - .data; + 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; + 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; + data = + btc + .P2WPKH( + data: btc.PaymentData(pubkey: pubKey), + network: _bitcoinDartNetwork, + ) + .data; break; case DerivePathType.bip86: @@ -1707,7 +1853,8 @@ mixin SparkInterface addresses: [ if (addressOrScript is String) addressOrScript.toString(), ], - walletOwns: (await mainDB.isar.addresses + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -1728,9 +1875,9 @@ mixin SparkInterface txb.sign( vin: i, keyPair: btc.ECPair.fromPrivateKey( - vin[i].keyPair!.privateKey.data, + vin[i].key!.privateKey!.data, network: _bitcoinDartNetwork, - compressed: vin[i].keyPair!.privateKey.compressed, + compressed: vin[i].key!.privateKey!.compressed, ), witnessValue: vin[i].utxo.value, @@ -1752,21 +1899,24 @@ mixin SparkInterface assert(outputs.length == 1); final data = TxData( - sparkRecipients: vout - .where((e) => e.$1 is Uint8List) // ignore change - .map( - (e) => ( - address: outputs.first - .address, // for display purposes on confirm tx screen. See todos above - memo: "", - amount: Amount( - rawValue: BigInt.from(e.$2), - fractionDigits: cryptoCurrency.fractionDigits, - ), - isChange: false, // ok? - ), - ) - .toList(), + sparkRecipients: + vout + .where((e) => e.$1 is Uint8List) // ignore change + .map( + (e) => ( + address: + outputs + .first + .address, // for display purposes on confirm tx screen. See todos above + memo: "", + amount: Amount( + rawValue: BigInt.from(e.$2), + fractionDigits: cryptoCurrency.fractionDigits, + ), + isChange: false, // ok? + ), + ) + .toList(), vSize: builtTx.virtualSize(), txid: builtTx.getId(), raw: builtTx.toHex(), @@ -1774,7 +1924,7 @@ mixin SparkInterface rawValue: nFeeRet, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: vin.map((e) => e.utxo).toList(), + usedUTXOs: vin, tempTx: TransactionV2( walletId: walletId, blockHash: null, @@ -1853,23 +2003,25 @@ mixin SparkInterface const subtractFeeFromAmount = true; // must be true for mint all final currentHeight = await chainHeight; - final spendableUtxos = await mainDB.isar.utxos - .where() - .walletIdEqualTo(walletId) - .filter() - .isBlockedEqualTo(false) - .and() - .group((q) => q.usedEqualTo(false).or().usedIsNull()) - .and() - .valueGreaterThan(0) - .findAll(); + final spendableUtxos = + await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedEqualTo(false).or().usedIsNull()) + .and() + .valueGreaterThan(0) + .findAll(); spendableUtxos.removeWhere( - (e) => !e.isConfirmed( - currentHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ), + (e) => + !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), ); if (spendableUtxos.isEmpty) { @@ -1910,15 +2062,12 @@ mixin SparkInterface if (txData.sparkRecipients?.isNotEmpty != true) { throw Exception("Missing spark recipients."); } - final recipients = txData.sparkRecipients! - .map( - (e) => MutableSparkRecipient( - e.address, - e.amount.raw, - e.memo, - ), - ) - .toList(); + final recipients = + txData.sparkRecipients! + .map( + (e) => MutableSparkRecipient(e.address, e.amount.raw, e.memo), + ) + .toList(); final total = recipients .map((e) => e.value) @@ -1930,14 +2079,16 @@ mixin SparkInterface throw Exception("Attempted send of zero amount"); } - final utxos = txData.utxos; + final utxos = + txData.utxos?.whereType().map((e) => e.utxo).toList(); final bool coinControl = utxos != null; - final utxosTotal = coinControl - ? utxos - .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)) - : null; + final utxosTotal = + coinControl + ? utxos + .map((e) => e.value) + .fold(BigInt.zero, (p, e) => p + BigInt.from(e)) + : null; if (coinControl && utxosTotal! < total) { throw Exception("Insufficient selected UTXOs!"); @@ -1947,7 +2098,8 @@ mixin SparkInterface final currentHeight = await chainHeight; - final availableOutputs = utxos?.toList() ?? + final availableOutputs = + utxos?.toList() ?? await mainDB.isar.utxos .where() .walletIdEqualTo(walletId) @@ -1961,17 +2113,18 @@ mixin SparkInterface final canCPFP = this is CpfpInterface && coinControl; - final spendableUtxos = availableOutputs - .where( - (e) => - canCPFP || - e.isConfirmed( - currentHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ), - ) - .toList(); + final spendableUtxos = + availableOutputs + .where( + (e) => + canCPFP || + e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), + ) + .toList(); if (spendableUtxos.isEmpty) { throw Exception("No available UTXOs found to anonymize"); @@ -2015,10 +2168,92 @@ mixin SparkInterface return txData.copyWith(sparkMints: await Future.wait(futures)); } + Future prepareSparkNameTransaction({ + required String name, + required String address, + required int years, + required String additionalInfo, + }) async { + // TODO remove after block 1104500 + if (cryptoCurrency.network == CryptoCurrencyNetwork.main) { + final height = await fetchChainHeight(); + if (height < 1104500) { + throw Exception("Spark names not enabled on main net yet"); + } + } + + if (years < 1 || years > kMaxNameRegistrationLengthYears) { + throw Exception("Invalid spark name registration period years: $years"); + } + + if (name.isEmpty || name.length > kMaxNameLength) { + throw Exception("Invalid spark name length: ${name.length}"); + } + if (!RegExp(kNameRegexString).hasMatch(name)) { + throw Exception("Invalid symbols found in spark name: $name"); + } + + if (additionalInfo.toUint8ListFromUtf8.length > + kMaxAdditionalInfoLengthBytes) { + throw Exception( + "Additional info exceeds $kMaxAdditionalInfoLengthBytes bytes.", + ); + } + + final sparkAddress = await mainDB.getAddress(walletId, address); + if (sparkAddress == null) { + throw Exception("Address '$address' not found in local DB."); + } + if (sparkAddress.type != AddressType.spark) { + throw Exception("Address '$address' is not a spark address."); + } + + final data = ( + name: name, + additionalInfo: additionalInfo, + validBlocks: years * 365 * 24 * 24, + sparkAddress: sparkAddress, + ); + + final String destinationAddress; + switch (cryptoCurrency.network) { + case CryptoCurrencyNetwork.main: + destinationAddress = kStage3DevelopmentFundAddressMainNet; + break; + + case CryptoCurrencyNetwork.test: + destinationAddress = kStage3DevelopmentFundAddressTestNet; + break; + + default: + throw Exception( + "Invalid network '${cryptoCurrency.network}' for spark name registration.", + ); + } + + final txData = await prepareSendSpark( + txData: TxData( + sparkNameInfo: data, + recipients: [ + TxRecipient( + address: destinationAddress, + amount: Amount.fromDecimal( + Decimal.fromInt(kStandardSparkNamesFee[name.length] * years), + fractionDigits: cryptoCurrency.fractionDigits, + ), + isChange: false, + addressType: cryptoCurrency.getAddressType(destinationAddress)!, + ), + ], + ), + ); + + return txData; + } + @override Future updateBalance() async { - // call to super to update transparent balance (and lelantus balance if - // what ever class this mixin is used on uses LelantusInterface as well) + // call to super to update transparent balance final normalBalanceFuture = super.updateBalance(); // todo: spark balance aka update info.tertiaryBalance here? @@ -2031,63 +2266,71 @@ mixin SparkInterface // ====================== Private ============================================ btc.NetworkType get _bitcoinDartNetwork => btc.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: btc.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ); + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); } /// Top level function which should be called wrapped in [compute] Future< - ({ - Uint8List serializedSpendPayload, - List outputScripts, - int fee, - List< - ({ - int groupId, - int height, - String serializedCoin, - String serializedCoinContext - })> usedCoins, - })> _createSparkSend( + ({ + Uint8List serializedSpendPayload, + List outputScripts, + int fee, + List< + ({ + int groupId, + int height, + String serializedCoin, + 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, + ({ + String sparkAddress, + int amount, + bool subtractFeeFromAmount, + String memo, + }) + > + privateRecipients, List< - ({ - String serializedCoin, - String serializedCoinContext, - int groupId, - int height, - })> serializedCoins, + ({ + 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, + ({ + int setId, + String setHash, + List<({String serializedCoin, String txHash})> set, + }) + > + allAnonymitySets, + List<({int setId, Uint8List blockHash})> idAndBlockHashes, Uint8List txHash, - }) args, + int additionalTxSize, + }) + args, ) async { final spend = LibSpark.createSparkSendTransaction( privateKeyHex: args.privateKeyHex, @@ -2098,6 +2341,7 @@ Future< allAnonymitySets: args.allAnonymitySets, idAndBlockHashes: args.idAndBlockHashes, txHash: args.txHash, + additionalTxSize: args.additionalTxSize, ); return spend; @@ -2111,7 +2355,8 @@ Future> _identifyCoins( Set privateKeyHexSet, String walletId, bool isTestNet, - }) args, + }) + args, ) async { final List myCoins = []; @@ -2200,12 +2445,13 @@ class MutableSparkRecipient { } } -typedef SerializedCoinData = ({ - int groupId, - int height, - String serializedCoin, - String serializedCoinContext -}); +typedef SerializedCoinData = + ({ + int groupId, + int height, + String serializedCoin, + String serializedCoinContext, + }); Future _asyncSparkFeesWrapper({ required String privateKeyHex, @@ -2214,18 +2460,19 @@ Future _asyncSparkFeesWrapper({ required bool subtractFeeFromAmount, required List serializedCoins, required int privateRecipientsCount, + required int utxoNum, + required int additionalTxSize, }) async { - return await computeWithLibSparkLogging( - _estSparkFeeComputeFunc, - ( - privateKeyHex: privateKeyHex, - index: index, - sendAmount: sendAmount, - subtractFeeFromAmount: subtractFeeFromAmount, - serializedCoins: serializedCoins, - privateRecipientsCount: privateRecipientsCount, - ), - ); + return await computeWithLibSparkLogging(_estSparkFeeComputeFunc, ( + privateKeyHex: privateKeyHex, + index: index, + sendAmount: sendAmount, + subtractFeeFromAmount: subtractFeeFromAmount, + serializedCoins: serializedCoins, + privateRecipientsCount: privateRecipientsCount, + utxoNum: utxoNum, + additionalTxSize: additionalTxSize, + )); } int _estSparkFeeComputeFunc( @@ -2236,7 +2483,10 @@ int _estSparkFeeComputeFunc( bool subtractFeeFromAmount, List serializedCoins, int privateRecipientsCount, - }) args, + int utxoNum, + int additionalTxSize, + }) + args, ) { final est = LibSpark.estimateSparkFee( privateKeyHex: args.privateKeyHex, @@ -2245,6 +2495,8 @@ int _estSparkFeeComputeFunc( subtractFeeFromAmount: args.subtractFeeFromAmount, serializedCoins: args.serializedCoins, privateRecipientsCount: args.privateRecipientsCount, + utxoNum: args.utxoNum, + additionalTxSize: args.additionalTxSize, ); return est; diff --git a/lib/widgets/coin_ticker_tag.dart b/lib/widgets/coin_ticker_tag.dart new file mode 100644 index 000000000..f15e6548d --- /dev/null +++ b/lib/widgets/coin_ticker_tag.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../themes/stack_colors.dart'; +import '../utilities/text_styles.dart'; +import 'rounded_container.dart'; + +class CoinTickerTag extends StatelessWidget { + const CoinTickerTag({super.key, required this.ticker}); + + final String ticker; + + @override + Widget build(BuildContext context) { + return RoundedContainer( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + radiusMultiplier: 0.25, + color: Theme.of(context).extension()!.ethTagBG, + child: Text( + ticker, + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context).extension()!.ethTagText, + ), + ), + ); + } +} diff --git a/lib/widgets/custom_buttons/draggable_switch_button.dart b/lib/widgets/custom_buttons/draggable_switch_button.dart index 064746c6f..e0ea21ebf 100644 --- a/lib/widgets/custom_buttons/draggable_switch_button.dart +++ b/lib/widgets/custom_buttons/draggable_switch_button.dart @@ -48,10 +48,9 @@ class DraggableSwitchButtonState extends State { Color _colorBG(bool isOn, bool enabled, double alpha) { if (enabled) { return Color.alphaBlend( - Theme.of(context) - .extension()! - .switchBGOn - .withOpacity(alpha), + Theme.of( + context, + ).extension()!.switchBGOn.withOpacity(alpha), Theme.of(context).extension()!.switchBGOff, ); } @@ -61,10 +60,9 @@ class DraggableSwitchButtonState extends State { Color _colorFG(bool isOn, bool enabled, double alpha) { if (enabled) { return Color.alphaBlend( - Theme.of(context) - .extension()! - .switchCircleOn - .withOpacity(alpha), + Theme.of( + context, + ).extension()!.switchCircleOn.withOpacity(alpha), Theme.of(context).extension()!.switchCircleOff, ); } @@ -190,15 +188,11 @@ class DraggableSwitchButtonState extends State { children: [ SizedBox( width: constraint.maxWidth / 2, - child: Center( - child: widget.onItem, - ), + child: Center(child: widget.onItem), ), SizedBox( width: constraint.maxWidth / 2, - child: Center( - child: widget.offItem, - ), + child: Center(child: widget.offItem), ), ], ), @@ -215,3 +209,85 @@ class DSBController { VoidCallback? activate; bool Function()? isOn; } + +// new and improved(ish) version +class DraggableSwitch extends StatefulWidget { + final bool value; + final ValueChanged onChanged; + + const DraggableSwitch({ + super.key, + required this.value, + required this.onChanged, + }); + + @override + State createState() => _DraggableSwitchState(); +} + +class _DraggableSwitchState extends State { + final _duration = const Duration(milliseconds: 150); + + double _dragOffset = 0.0; + + void _handleDragEnd(DragEndDetails details) { + if (_dragOffset.abs() > 10) { + final shouldTurnOn = _dragOffset > 0; + if (widget.value != shouldTurnOn) { + widget.onChanged(shouldTurnOn); + } + } + _dragOffset = 0.0; + } + + void _toggle() { + widget.onChanged(!widget.value); + } + + @override + Widget build(BuildContext context) { + final isOn = widget.value; + + final colors = Theme.of(context).extension()!; + + final colorBG = isOn ? colors.switchBGOn : colors.switchBGOff; + final colorFG = isOn ? colors.switchCircleOn : colors.switchCircleOff; + + return GestureDetector( + onTap: _toggle, + onHorizontalDragUpdate: (details) { + _dragOffset += details.primaryDelta!; + }, + onHorizontalDragEnd: _handleDragEnd, + child: LayoutBuilder( + builder: (context, constraints) { + return AnimatedContainer( + duration: _duration, + curve: Curves.easeInOut, + width: constraints.maxWidth, + height: constraints.maxHeight, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: colorBG, + borderRadius: BorderRadius.circular(constraints.maxHeight / 2), + ), + child: AnimatedAlign( + duration: _duration, + curve: Curves.easeInOut, + alignment: isOn ? Alignment.centerRight : Alignment.centerLeft, + child: AnimatedContainer( + duration: _duration, + height: constraints.maxHeight - 4, + width: constraints.maxWidth / 2 - 4, + decoration: BoxDecoration( + color: colorFG, + shape: BoxShape.circle, + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/desktop/desktop_fee_dialog.dart b/lib/widgets/desktop/desktop_fee_dialog.dart index 713aaa109..82cc82e0b 100644 --- a/lib/widgets/desktop/desktop_fee_dialog.dart +++ b/lib/widgets/desktop/desktop_fee_dialog.dart @@ -4,8 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/models.dart'; import '../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; -import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart'; import '../../providers/global/wallets_provider.dart'; +import '../../providers/ui/fee_rate_type_state_provider.dart'; +import '../../providers/wallet/desktop_fee_providers.dart'; import '../../providers/wallet/public_private_balance_state_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; @@ -43,7 +44,7 @@ class _DesktopFeeDialogState extends ConsumerState { Future feeFor({ required Amount amount, required FeeRateType feeRateType, - required int feeRate, + required BigInt feeRate, required CryptoCurrency coin, }) async { switch (feeRateType) { @@ -62,26 +63,26 @@ class _DesktopFeeDialogState extends ConsumerState { if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.high.value, + BigInt.from(lib_monero.TransactionPriority.high.value), ); ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + case BalanceType.private: + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); + case BalanceType.public: + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - await wallet.estimateFeeFor(amount, feeRate); + ref.read(feeSheetSessionCacheProvider).fast[amount] = await wallet + .estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -112,21 +113,21 @@ class _DesktopFeeDialogState extends ConsumerState { if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.medium.value, + BigInt.from(lib_monero.TransactionPriority.medium.value), ); ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + case BalanceType.private: + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); + case BalanceType.public: + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else { @@ -162,26 +163,26 @@ class _DesktopFeeDialogState extends ConsumerState { if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.normal.value, + BigInt.from(lib_monero.TransactionPriority.normal.value), ); ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + case BalanceType.private: + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); + case BalanceType.public: + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - await wallet.estimateFeeFor(amount, feeRate); + ref.read(feeSheetSessionCacheProvider).slow[amount] = await wallet + .estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -214,9 +215,7 @@ class _DesktopFeeDialogState extends ConsumerState { maxHeight: double.infinity, child: FutureBuilder( future: ref.watch( - pWallets.select( - (value) => value.getWallet(walletId).fees, - ), + pWallets.select((value) => value.getWallet(walletId).fees), ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -255,9 +254,7 @@ class _DesktopFeeDialogState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ); }, @@ -283,9 +280,10 @@ class DesktopFeeItem extends ConsumerStatefulWidget { final Future Function({ required Amount amount, required FeeRateType feeRateType, - required int feeRate, + required BigInt feeRate, required CryptoCurrency coin, - }) feeFor; + }) + feeFor; final bool isSelected; final bool isButton; @@ -337,59 +335,33 @@ class _DesktopFeeItemState extends ConsumerState { return ConditionalParent( condition: widget.isButton, - builder: (child) => MaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pop( - ( - widget.feeRateType, - feeString, - timeString, - ), - ); - }, - child: child, - ), + builder: + (child) => MaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + ref.read(feeRateTypeDesktopStateProvider.state).state = + widget.feeRateType; + Navigator.of( + context, + ).pop((widget.feeRateType, feeString, timeString)); + }, + child: child, + ), child: Builder( builder: (_) { - if (!widget.isButton) { - final coin = ref.watch( - pWallets.select( - (value) => value.getWallet(widget.walletId).info.coin, - ), - ); - if ((coin is Firo) && - ref.watch(publicPrivateBalanceStateProvider.state).state == - "Private") { - return Text( - "~${ref.watch(pAmountFormatter(coin)).format( - Amount( - rawValue: BigInt.parse("3794"), - fractionDigits: coin.fractionDigits, - ), - indicatePrecisionLoss: false, - )}", - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - textAlign: TextAlign.left, - ); - } - } - if (widget.feeRateType == FeeRateType.custom) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( widget.feeRateType.prettyName, - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), textAlign: TextAlign.left, ), @@ -405,9 +377,10 @@ class _DesktopFeeItemState extends ConsumerState { return AnimatedText( stringsToLoopThrough: stringsToLoopThrough, style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), ); } else { @@ -415,9 +388,10 @@ class _DesktopFeeItemState extends ConsumerState { future: widget.feeFor( coin: wallet.info.coin, feeRateType: widget.feeRateType, - feeRate: widget.feeRateType == FeeRateType.fast - ? widget.feeObject!.fast - : widget.feeRateType == FeeRateType.slow + feeRate: + widget.feeRateType == FeeRateType.fast + ? widget.feeObject!.fast + : widget.feeRateType == FeeRateType.slow ? widget.feeObject!.slow : widget.feeObject!.medium, amount: ref.watch(sendAmountProvider.state).state, @@ -425,44 +399,47 @@ class _DesktopFeeItemState extends ConsumerState { builder: (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { - feeString = "${widget.feeRateType.prettyName} " - "(~${ref.watch(pAmountFormatter(wallet.info.coin)).format( - snapshot.data!, - indicatePrecisionLoss: false, - )})"; - - timeString = wallet.info.coin is Ethereum - ? "" - : estimatedTimeToBeIncludedInNextBlock( - wallet.info.coin.targetBlockTimeSeconds, - widget.feeRateType == FeeRateType.fast - ? widget.feeObject!.numberOfBlocksFast - : widget.feeRateType == FeeRateType.slow - ? widget.feeObject!.numberOfBlocksSlow - : widget.feeObject!.numberOfBlocksAverage, - ); + feeString = + "${widget.feeRateType.prettyName} " + "(~${ref.watch(pAmountFormatter(wallet.info.coin)).format(snapshot.data!, indicatePrecisionLoss: false)})"; + + timeString = + wallet.info.coin is Ethereum + ? "" + : estimatedTimeToBeIncludedInNextBlock( + wallet.info.coin.targetBlockTimeSeconds, + widget.feeRateType == FeeRateType.fast + ? widget.feeObject!.numberOfBlocksFast + : widget.feeRateType == FeeRateType.slow + ? widget.feeObject!.numberOfBlocksSlow + : widget.feeObject!.numberOfBlocksAverage, + ); return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( feeString!, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), textAlign: TextAlign.left, ), if (widget.feeObject != null) Text( timeString!, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), ], @@ -470,11 +447,13 @@ class _DesktopFeeItemState extends ConsumerState { } else { return AnimatedText( stringsToLoopThrough: stringsToLoopThrough, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), ); } diff --git a/lib/widgets/desktop/desktop_scaffold.dart b/lib/widgets/desktop/desktop_scaffold.dart index 920c66306..805fc56a7 100644 --- a/lib/widgets/desktop/desktop_scaffold.dart +++ b/lib/widgets/desktop/desktop_scaffold.dart @@ -14,12 +14,7 @@ import '../../themes/stack_colors.dart'; import '../background.dart'; class DesktopScaffold extends StatelessWidget { - const DesktopScaffold({ - super.key, - this.background, - this.appBar, - this.body, - }); + const DesktopScaffold({super.key, this.background, this.appBar, this.body}); final Color? background; final Widget? appBar; @@ -35,10 +30,7 @@ class DesktopScaffold extends StatelessWidget { // crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (appBar != null) appBar!, - if (body != null) - Expanded( - child: body!, - ), + if (body != null) Expanded(child: body!), ], ), ), @@ -73,7 +65,7 @@ class MasterScaffold extends StatelessWidget { child: Scaffold( backgroundColor: background ?? Colors.transparent, appBar: appBar as PreferredSizeWidget?, - body: body, + body: SafeArea(child: body), ), ); } diff --git a/lib/widgets/eth_fee_form.dart b/lib/widgets/eth_fee_form.dart new file mode 100644 index 000000000..2f4e2889e --- /dev/null +++ b/lib/widgets/eth_fee_form.dart @@ -0,0 +1,310 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; + +import '../services/ethereum/ethereum_api.dart'; +import '../themes/stack_colors.dart'; +import '../utilities/constants.dart'; +import '../utilities/text_styles.dart'; +import '../utilities/util.dart'; +import 'stack_text_field.dart'; + +@immutable +class EthEIP1559Fee { + final Decimal maxBaseFeeGwei; + final Decimal priorityFeeGwei; + final int gasLimit; + + const EthEIP1559Fee({ + required this.maxBaseFeeGwei, + required this.priorityFeeGwei, + required this.gasLimit, + }); + + BigInt get maxBaseFeeWei => maxBaseFeeGwei.shift(9).toBigInt(); + BigInt get priorityFeeWei => priorityFeeGwei.shift(9).toBigInt(); + + @override + String toString() => + "EthEIP1559Fee(" + "maxBaseFeeGwei: $maxBaseFeeGwei, " + "priorityFeeGwei: $priorityFeeGwei, " + "maxBaseFeeWei: $maxBaseFeeWei, " + "priorityFeeWei: $priorityFeeWei, " + "gasLimit: $gasLimit)"; +} + +class EthFeeForm extends StatefulWidget { + EthFeeForm({ + super.key, + this.minGasLimit = 21000, + this.maxGasLimit = 30000000, + this.initialState, + required this.stateChanged, + }) : assert( + initialState == null || + (initialState.gasLimit >= minGasLimit && + initialState.gasLimit <= maxGasLimit), + ); + + final int minGasLimit; + final int maxGasLimit; + + final EthEIP1559Fee? initialState; + + final void Function(EthEIP1559Fee) stateChanged; + + @override + State createState() => _EthFeeFormState(); +} + +class _EthFeeFormState extends State { + static const _textFadeDuration = Duration(milliseconds: 300); + + final maxBaseController = TextEditingController(); + final priorityFeeController = TextEditingController(); + final gasLimitController = TextEditingController(); + final maxBaseFocus = FocusNode(); + final priorityFeeFocus = FocusNode(); + final gasLimitFocus = FocusNode(); + + late int _gasLimitCache; + + EthEIP1559Fee get _current => EthEIP1559Fee( + maxBaseFeeGwei: Decimal.tryParse(maxBaseController.text) ?? Decimal.zero, + priorityFeeGwei: + Decimal.tryParse(priorityFeeController.text) ?? Decimal.zero, + gasLimit: int.parse(gasLimitController.text), + ); + + String _currentBase = "Current: "; + String _currentPriority = "Current: "; + + void _checkNetworkGas() async { + final gas = await EthereumAPI.getGasOracle(); + + if (mounted) { + setState(() { + _currentBase = + "Current: ${gas.value!.suggestBaseFee.toStringAsFixed(3)} GWEI"; + _currentPriority = + "Current: ${gas.value!.lowPriority.toStringAsFixed(3)} - ${gas.value!.highPriority.toStringAsFixed(3)} GWEI"; + }); + } + } + + Timer? _gasTimer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkNetworkGas(); + _gasTimer = Timer.periodic( + const Duration(seconds: 5), + (_) => _checkNetworkGas(), + ); + }); + + maxBaseController.text = + widget.initialState?.maxBaseFeeGwei.toString() ?? ""; + priorityFeeController.text = + widget.initialState?.priorityFeeGwei.toString() ?? ""; + + _gasLimitCache = widget.initialState?.gasLimit ?? widget.minGasLimit; + gasLimitController.text = _gasLimitCache.toString(); + } + + @override + void dispose() { + _gasTimer?.cancel(); + _gasTimer = null; + maxBaseController.dispose(); + priorityFeeController.dispose(); + gasLimitController.dispose(); + maxBaseFocus.dispose(); + priorityFeeFocus.dispose(); + gasLimitFocus.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Max base fee (GWEI)", style: STextStyles.smallMed12(context)), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 1, + controller: maxBaseController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + focusNode: maxBaseFocus, + onChanged: (value) { + widget.stateChanged(_current); + }, + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + null, + maxBaseFocus, + context, + desktopMed: Util.isDesktop, + ).copyWith( + contentPadding: EdgeInsets.only( + left: 16, + top: Util.isDesktop ? 11 : 6, + bottom: Util.isDesktop ? 12 : 8, + right: 5, + ), + ), + ), + ), + const SizedBox(height: 6), + AnimatedSwitcher( + duration: _textFadeDuration, + transitionBuilder: + (child, animation) => + FadeTransition(opacity: animation, child: child), + child: Text( + _currentBase, + key: ValueKey( + _currentBase, + ), // Important: ensures AnimatedSwitcher sees the text change + style: STextStyles.smallMed12(context), + ), + ), + const SizedBox(height: 20), + Text("Priority fee (GWEI)", style: STextStyles.smallMed12(context)), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 1, + controller: priorityFeeController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + focusNode: priorityFeeFocus, + onChanged: (value) { + widget.stateChanged(_current); + }, + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + null, + priorityFeeFocus, + context, + desktopMed: Util.isDesktop, + ).copyWith( + contentPadding: EdgeInsets.only( + left: 16, + top: Util.isDesktop ? 11 : 6, + bottom: Util.isDesktop ? 12 : 8, + right: 5, + ), + ), + ), + ), + const SizedBox(height: 6), + AnimatedSwitcher( + duration: _textFadeDuration, + transitionBuilder: + (child, animation) => + FadeTransition(opacity: animation, child: child), + child: Text( + _currentPriority, + key: ValueKey( + _currentPriority, + ), // Important: ensures AnimatedSwitcher sees the text change + style: STextStyles.smallMed12(context), + ), + ), + const SizedBox(height: 20), + Text("Gas limit", style: STextStyles.smallMed12(context)), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 1, + controller: gasLimitController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + focusNode: gasLimitFocus, + onChanged: (value) { + final intValue = int.tryParse(value); + if (intValue == null || + intValue < widget.minGasLimit || + intValue > widget.maxGasLimit) { + gasLimitController.text = _gasLimitCache.toString(); + return; + } + + _gasLimitCache = intValue; + + widget.stateChanged(_current); + }, + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + null, + gasLimitFocus, + context, + desktopMed: Util.isDesktop, + ).copyWith( + contentPadding: EdgeInsets.only( + left: 16, + top: Util.isDesktop ? 11 : 6, + bottom: Util.isDesktop ? 12 : 8, + right: 5, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/icon_widgets/eth_token_icon.dart b/lib/widgets/icon_widgets/eth_token_icon.dart index 5908270f6..b837eeeb7 100644 --- a/lib/widgets/icon_widgets/eth_token_icon.dart +++ b/lib/widgets/icon_widgets/eth_token_icon.dart @@ -12,7 +12,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; + import '../../models/isar/exchange_cache/currency.dart'; +import '../../services/exchange/change_now/change_now_exchange.dart'; import '../../services/exchange/exchange_data_loading_service.dart'; import '../../themes/coin_icon_provider.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; @@ -32,17 +34,36 @@ class EthTokenIcon extends ConsumerStatefulWidget { } class _EthTokenIconState extends ConsumerState { - late final String? imageUrl; + String? imageUrl; @override void initState() { - imageUrl = ExchangeDataLoadingService.instance.isar.currencies - .where() - .filter() - .tokenContractEqualTo(widget.contractAddress, caseSensitive: false) - .findFirstSync() - ?.image; super.initState(); + + ExchangeDataLoadingService.instance.isar.then((isar) async { + final currency = + await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + widget.contractAddress, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirst(); + + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + imageUrl = currency?.image; + }); + } + }); + } + }); } @override diff --git a/lib/widgets/qr_scanner.dart b/lib/widgets/qr_scanner.dart new file mode 100644 index 000000000..66941ac9d --- /dev/null +++ b/lib/widgets/qr_scanner.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../themes/stack_colors.dart'; +import '../utilities/logger.dart'; +import '../utilities/text_styles.dart'; +import 'background.dart'; +import 'custom_buttons/app_bar_icon_button.dart'; + +class QrScanner extends ConsumerWidget { + const QrScanner({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.backgroundAppBar, + leading: const AppBarBackButton(), + title: Text("Scan QR code", style: STextStyles.navBarTitle(context)), + ), + body: MobileScanner( + onDetect: (capture) { + final data = + ((capture.raw as Map?)?["data"] as List?)?.firstOrNull as Map?; + + final value = + data?["rawValue"] as String? ?? + data?["displayValue"] as String?; + + Navigator.of(context).pop(value); + }, + onDetectError: (error, stackTrace) { + Logging.instance.w( + "Mobile scanner", + error: error, + stackTrace: stackTrace, + ); + Navigator.of(context).pop(); + }, + ), + ), + ); + } +} diff --git a/lib/widgets/static_overflow_row/measure_size.dart b/lib/widgets/static_overflow_row/measure_size.dart new file mode 100644 index 000000000..d6d9f9708 --- /dev/null +++ b/lib/widgets/static_overflow_row/measure_size.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class MeasureSize extends StatefulWidget { + const MeasureSize({super.key, required this.onChange, required this.child}); + + final ValueChanged onChange; + final Widget child; + + @override + State createState() => _MeasureSizeState(); +} + +class _MeasureSizeState extends State { + Size? previous; + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final size = context.size; + if (size != null && previous != size) { + previous = size; + widget.onChange(size); + } + }); + return widget.child; + } +} diff --git a/lib/widgets/static_overflow_row/static_overflow_row.dart b/lib/widgets/static_overflow_row/static_overflow_row.dart new file mode 100644 index 000000000..77cad9ada --- /dev/null +++ b/lib/widgets/static_overflow_row/static_overflow_row.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; + +import 'measure_size.dart'; + +class StaticOverflowRow extends StatefulWidget { + final Widget Function(int hiddenCount) overflowBuilder; + final MainAxisAlignment mainAxisAlignment; + final List children; + final bool forcedOverflow; + + const StaticOverflowRow({ + super.key, + required this.overflowBuilder, + this.mainAxisAlignment = MainAxisAlignment.end, + this.forcedOverflow = false, + required this.children, + }); + + @override + State createState() => _StaticOverflowRowState(); +} + +class _StaticOverflowRowState extends State { + final Map _itemSizes = {}; + Size? _overflowSize; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final childCount = widget.children.length; + + // Still measuring + if (_itemSizes.length < childCount || _overflowSize == null) { + return Row( + mainAxisAlignment: widget.mainAxisAlignment, + children: [ + ...List.generate(childCount, (i) { + return MeasureSize( + onChange: (size) { + if (_itemSizes[i] != size) { + setState(() { + _itemSizes[i] = size; + }); + } + }, + child: KeyedSubtree( + key: ValueKey("item-$i"), + child: widget.children[i], + ), + ); + }), + MeasureSize( + onChange: (size) { + if (_overflowSize != size) { + setState(() { + _overflowSize = size; + }); + } + }, + child: KeyedSubtree( + key: const ValueKey("overflow"), + child: widget.overflowBuilder(0), + ), + ), + ], + ); + } + + final List visible = []; + double usedWidth = (widget.forcedOverflow ? _overflowSize!.width : 0); + + bool firstPassFailed = false; + // Try first pass without overflow + for (int i = 0; i < childCount; i++) { + final itemSize = _itemSizes[i]!; + if (usedWidth + itemSize.width <= constraints.maxWidth) { + visible.add(widget.children[i]); + usedWidth += itemSize.width; + } else { + // Not all children fit. Overflow required + firstPassFailed = true; + break; + } + } + + if (firstPassFailed) { + visible.clear(); + usedWidth = 0; + int overflowCount = 0; + for (int i = 0; i < childCount; i++) { + final size = _itemSizes[i]!; + final needsOverflow = i < childCount - 1 || widget.forcedOverflow; + final canFit = + usedWidth + + size.width + + (needsOverflow ? _overflowSize!.width : 0) <= + constraints.maxWidth; + + if (canFit) { + visible.add(widget.children[i]); + usedWidth += size.width; + } else { + overflowCount = childCount - i; + break; + } + } + + // Add overflow + visible.add(widget.overflowBuilder(overflowCount)); + } else { + if (widget.forcedOverflow) { + // Add forced overflow + visible.add(widget.overflowBuilder(0)); + } + } + + return Row( + mainAxisAlignment: widget.mainAxisAlignment, + children: visible, + ); + }, + ); + } +} diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart index 3c86f1fed..f94fac2b4 100644 --- a/lib/widgets/textfields/frost_step_field.dart +++ b/lib/widgets/textfields/frost_step_field.dart @@ -1,10 +1,12 @@ import 'dart:io'; -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 '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; +import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; @@ -16,7 +18,7 @@ import '../icon_widgets/qrcode_icon.dart'; import '../icon_widgets/x_icon.dart'; import '../textfield_icon_button.dart'; -class FrostStepField extends StatefulWidget { +class FrostStepField extends ConsumerStatefulWidget { const FrostStepField({ super.key, required this.controller, @@ -35,10 +37,10 @@ class FrostStepField extends StatefulWidget { final bool showQrScanOption; @override - State createState() => _FrostStepFieldState(); + ConsumerState createState() => _FrostStepFieldState(); } -class _FrostStepFieldState extends State { +class _FrostStepFieldState extends ConsumerState { final _xKey = UniqueKey(); final _pasteKey = UniqueKey(); late final Key? _qrKey; @@ -46,13 +48,8 @@ class _FrostStepFieldState extends State { bool _isEmpty = true; final _inputBorder = OutlineInputBorder( - borderSide: const BorderSide( - width: 0, - color: Colors.transparent, - ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), + borderSide: const BorderSide(width: 0, color: Colors.transparent), + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ); late final void Function(String) _changed; @@ -79,12 +76,10 @@ class _FrostStepFieldState extends State { if (Platform.isAndroid || Platform.isIOS) { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); + await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await BarcodeScanner.scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); widget.controller.text = qrResult.rawContent; @@ -106,11 +101,26 @@ class _FrostStepFieldState extends State { } } } on PlatformException catch (e, s) { - Logging.instance.w( - "Failed to get camera permissions while trying to scan qr code: ", - error: e, - stackTrace: s, - ); + if (mounted) { + try { + await checkCamPermDeniedMobileAndOpenAppSettings( + context, + logging: Logging.instance, + ); + } catch (e, s) { + Logging.instance.e( + "Failed to check cam permissions", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w( + "Failed to get camera permissions while trying to scan qr code: ", + error: e, + stackTrace: s, + ); + } } } @@ -118,19 +128,15 @@ class _FrostStepFieldState extends State { 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), + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(widget.label!, style: STextStyles.w500_14(context)), + const SizedBox(height: 4), + child, + ], ), - const SizedBox( - height: 4, - ), - child, - ], - ), child: TextField( controller: widget.controller, focusNode: widget.focusNode, @@ -141,53 +147,60 @@ class _FrostStepFieldState extends State { onChanged: _changed, decoration: InputDecoration( hintText: widget.hint, - fillColor: widget.focusNode.hasFocus - ? Theme.of(context).extension()!.textFieldActiveBG - : Theme.of(context).extension()!.textFieldDefaultBG, - hintStyle: Util.isDesktop - ? STextStyles.desktopTextFieldLabel(context) - : STextStyles.fieldLabel(context), + 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), + padding: + _isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ !_isEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Frost Step Field Input.", - key: _xKey, - onTap: () { - widget.controller.text = ""; - - _changed(widget.controller.text); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Frost Step Field Input.", + key: _xKey, + onTap: () { + widget.controller.text = ""; + + _changed(widget.controller.text); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Frost Step Field Input.", - key: _pasteKey, - onTap: () async { - final ClipboardData? data = - await Clipboard.getData(Clipboard.kTextPlain); - if (data?.text != null && data!.text!.isNotEmpty) { - widget.controller.text = data.text!.trim(); - } - - _changed(widget.controller.text); - }, - child: - _isEmpty ? const ClipboardIcon() : const XIcon(), - ), + semanticsLabel: + "Paste Button. Pastes From Clipboard To Frost Step Field Input.", + key: _pasteKey, + onTap: () async { + final ClipboardData? data = await Clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && data!.text!.isNotEmpty) { + widget.controller.text = data.text!.trim(); + } + + _changed(widget.controller.text); + }, + child: _isEmpty ? const ClipboardIcon() : const XIcon(), + ), if (_isEmpty && widget.showQrScanOption) TextFieldIconButton( semanticsLabel: diff --git a/lib/widgets/trade_card.dart b/lib/widgets/trade_card.dart index 28a05f9ae..35c8e4578 100644 --- a/lib/widgets/trade_card.dart +++ b/lib/widgets/trade_card.dart @@ -15,7 +15,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import '../models/exchange/change_now/exchange_transaction_status.dart'; +import '../models/exchange/change_now/cn_exchange_transaction_status.dart'; import '../models/exchange/response_objects/trade.dart'; import '../models/isar/stack_theme.dart'; import '../themes/theme_providers.dart'; @@ -26,11 +26,7 @@ import 'conditional_parent.dart'; import 'rounded_white_container.dart'; class TradeCard extends ConsumerWidget { - const TradeCard({ - super.key, - required this.trade, - required this.onTap, - }); + const TradeCard({super.key, required this.trade, required this.onTap}); final Trade trade; final VoidCallback onTap; @@ -78,10 +74,9 @@ class TradeCard extends ConsumerWidget { return ConditionalParent( condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), child: GestureDetector( onTap: onTap, child: RoundedWhiteContainer( @@ -108,9 +103,7 @@ class TradeCard extends ConsumerWidget { ), ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: Column( children: [ @@ -127,9 +120,7 @@ class TradeCard extends ConsumerWidget { ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index cab5043d6..f4227005e 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -143,30 +143,35 @@ class _TransactionCardState extends ConsumerState { localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); - final price = ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => isTokenTx - ? value.getTokenPrice(_transaction.otherData!) - : value.getPrice(coin), - ), - ) - .item1; + final price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(_transaction.otherData!) + : value.getPrice(coin), + ), + ) + ?.value; final currentHeight = ref.watch( - pWallets - .select((value) => value.getWallet(walletId).info.cachedChainHeight), + pWallets.select( + (value) => value.getWallet(walletId).info.cachedChainHeight, + ), ); return Material( color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: Padding( padding: const EdgeInsets.all(6), @@ -191,25 +196,22 @@ class _TransactionCardState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TransactionDetailsView( - transaction: _transaction, - coin: coin, - walletId: walletId, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionDetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), + ), ); } else { unawaited( Navigator.of(context).pushNamed( TransactionDetailsView.routeName, - arguments: Tuple3( - _transaction, - coin, - walletId, - ), + arguments: Tuple3(_transaction, coin, walletId), ), ); } @@ -223,9 +225,7 @@ class _TransactionCardState extends ConsumerState { coin: coin, currentHeight: currentHeight, ), - const SizedBox( - width: 14, - ), + const SizedBox(width: 14), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -243,17 +243,15 @@ class _TransactionCardState extends ConsumerState { ? "Failed" : "Cancelled" : whatIsIt( - _transaction.type, - coin, - currentHeight, - ), + _transaction.type, + coin, + currentHeight, + ), style: STextStyles.itemSubtitle12(context), ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Flexible( child: FittedBox( fit: BoxFit.scaleDown, @@ -271,9 +269,7 @@ class _TransactionCardState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // crossAxisAlignment: CrossAxisAlignment.end, @@ -287,17 +283,19 @@ class _TransactionCardState extends ConsumerState { ), ), ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) - const SizedBox( - width: 10, - ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) + if (price != null && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + )) + const SizedBox(width: 10), + if (price != null && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + )) Flexible( child: FittedBox( fit: BoxFit.scaleDown, @@ -306,12 +304,7 @@ class _TransactionCardState extends ConsumerState { final amount = _transaction.realAmount; return Text( - "$prefix${Amount.fromDecimal( - amount.decimal * price, - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "$prefix${Amount.fromDecimal(amount.decimal * price, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.label(context), ); }, diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart index 0212d89d6..14783eb14 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart @@ -14,6 +14,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 '../../../models/isar/exchange_cache/currency.dart'; import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange_data_loading_service.dart'; @@ -22,7 +23,7 @@ import '../../../themes/theme_providers.dart'; import '../../../utilities/constants.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; -class WalletInfoCoinIcon extends ConsumerWidget { +class WalletInfoCoinIcon extends ConsumerStatefulWidget { const WalletInfoCoinIcon({ super.key, required this.coin, @@ -35,46 +36,65 @@ class WalletInfoCoinIcon extends ConsumerWidget { final double size; @override - Widget build(BuildContext context, WidgetRef ref) { - Currency? currency; - if (contractAddress != null) { - currency = ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - contractAddress!, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirstSync(); - } + ConsumerState createState() => _WalletInfoCoinIconState(); +} + +class _WalletInfoCoinIconState extends ConsumerState { + String? imageUrl; + + @override + void initState() { + super.initState(); + ExchangeDataLoadingService.instance.isar.then((isar) async { + if (widget.contractAddress != null) { + final currency = + await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + widget.contractAddress!, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirst(); + + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + imageUrl = currency?.image; + }); + } + }); + } + } + }); + } + + @override + Widget build(BuildContext context) { return Container( - width: size, - height: size, + width: widget.size, + height: widget.size, decoration: BoxDecoration( - color: ref.watch(pCoinColor(coin)).withOpacity(0.4), + color: ref.watch(pCoinColor(widget.coin)).withOpacity(0.4), borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), child: Padding( - padding: EdgeInsets.all(size / 5), - child: currency != null && currency.image.isNotEmpty - ? SvgPicture.network( - currency.image, - width: 20, - height: 20, - ) - : SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), + padding: EdgeInsets.all(widget.size / 5), + child: + imageUrl != null && imageUrl!.isNotEmpty + ? SvgPicture.network(imageUrl!, width: 20, height: 20) + : SvgPicture.file( + File(ref.watch(coinIconProvider(widget.coin))), + width: 20, + height: 20, ), - width: 20, - height: 20, - ), ), ); } diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index 2cc35b7aa..381d0e0d5 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -12,12 +12,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; -import '../../pages/token_view/sub_widgets/token_summary.dart'; -import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../coin_ticker_tag.dart'; import '../custom_buttons/blue_text_button.dart'; import 'sub_widgets/wallet_info_row_balance.dart'; import 'sub_widgets/wallet_info_row_coin_icon.dart'; @@ -43,8 +43,9 @@ class WalletInfoRow extends ConsumerWidget { EthContract? contract; if (contractAddress != null) { contract = ref.watch( - mainDBProvider - .select((value) => value.getEthContractSync(contractAddress!)), + mainDBProvider.select( + (value) => value.getEthContractSync(contractAddress!), + ), ); } @@ -63,39 +64,40 @@ class WalletInfoRow extends ConsumerWidget { coin: wallet.info.coin, contractAddress: contractAddress, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), contract != null ? Row( - children: [ - Text( - contract.name, - style: - STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - const SizedBox( - width: 4, + children: [ + Text( + contract.name, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), - CoinTickerTag( - walletId: walletId, + ), + const SizedBox(width: 4), + CoinTickerTag( + ticker: ref.watch( + pWalletCoin(walletId).select((s) => s.ticker), ), - ], - ) - : Text( - wallet.info.name, - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, ), + ], + ) + : Text( + wallet.info.name, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), + ), ], ), ), @@ -129,36 +131,33 @@ class WalletInfoRow extends ConsumerWidget { coin: wallet.info.coin, contractAddress: contractAddress, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - contract != null - ? Row( - children: [ - Text( - contract.name, - style: STextStyles.titleBold12(context), - ), - const SizedBox( - width: 4, - ), - CoinTickerTag( - walletId: walletId, - ), - ], - ) - : Text( - wallet.info.name, + if (contract != null) + Row( + children: [ + Text( + contract.name, style: STextStyles.titleBold12(context), ), - const SizedBox( - height: 2, - ), + const SizedBox(width: 4), + CoinTickerTag( + ticker: ref.watch( + pWalletCoin(walletId).select((s) => s.ticker), + ), + ), + ], + ) + else + Text( + wallet.info.name, + style: STextStyles.titleBold12(context), + ), + const SizedBox(height: 2), WalletInfoRowBalance( walletId: walletId, contractAddress: contractAddress, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 88c196c5e..02b019f4d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) cs_monero_flutter_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "CsMoneroFlutterLibsLinuxPlugin"); cs_monero_flutter_libs_linux_plugin_register_with_registrar(cs_monero_flutter_libs_linux_registrar); + g_autoptr(FlPluginRegistrar) cs_salvium_flutter_libs_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "CsSalviumFlutterLibsLinuxPlugin"); + cs_salvium_flutter_libs_linux_plugin_register_with_registrar(cs_salvium_flutter_libs_linux_registrar); g_autoptr(FlPluginRegistrar) desktop_drop_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index aa67f97ac..239136a98 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST cs_monero_flutter_libs_linux + cs_salvium_flutter_libs_linux desktop_drop devicelocale flutter_libepiccash @@ -19,6 +20,7 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST camera_linux coinlib_flutter flutter_libsparkmobile + flutter_mwebd frostdart tor_ffi_plugin xelis_flutter diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 245400e1a..b30b6bead 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import camera_macos import connectivity_plus import cs_monero_flutter_libs_macos +import cs_salvium_flutter_libs_macos import desktop_drop import device_info_plus import devicelocale @@ -16,8 +17,8 @@ import flutter_libepiccash import flutter_local_notifications import flutter_secure_storage_macos import isar_flutter_libs -import lelantus import local_auth_darwin +import mobile_scanner import package_info_plus import path_provider_foundation import share_plus @@ -31,6 +32,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { CameraMacosPlugin.register(with: registry.registrar(forPlugin: "CameraMacosPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) CsMoneroFlutterLibsMacosPlugin.register(with: registry.registrar(forPlugin: "CsMoneroFlutterLibsMacosPlugin")) + CsSalviumFlutterLibsMacosPlugin.register(with: registry.registrar(forPlugin: "CsSalviumFlutterLibsMacosPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) @@ -39,8 +41,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) - LelantusPlugin.register(with: registry.registrar(forPlugin: "LelantusPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index a51d63f71..64b655098 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -9,6 +9,8 @@ PODS: - ReachabilitySwift - cs_monero_flutter_libs_macos (0.0.1): - FlutterMacOS + - cs_salvium_flutter_libs_macos (0.0.1): + - FlutterMacOS - desktop_drop (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): @@ -30,8 +32,6 @@ PODS: - FlutterMacOS - isar_flutter_libs (1.0.0): - FlutterMacOS - - lelantus (0.0.1): - - FlutterMacOS - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS @@ -46,6 +46,8 @@ PODS: - "sqlite3 (3.46.0+1)": - "sqlite3/common (= 3.46.0+1)" - "sqlite3/common (3.46.0+1)" + - "sqlite3/dbstatvtab (3.46.0+1)": + - sqlite3/common - "sqlite3/fts5 (3.46.0+1)": - sqlite3/common - "sqlite3/perf-threadsafe (3.46.0+1)": @@ -54,7 +56,8 @@ PODS: - sqlite3/common - sqlite3_flutter_libs (0.0.1): - FlutterMacOS - - sqlite3 (~> 3.46.0) + - "sqlite3 (~> 3.46.0+1)" + - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree @@ -75,6 +78,7 @@ DEPENDENCIES: - coinlib_flutter (from `Flutter/ephemeral/.symlinks/plugins/coinlib_flutter/darwin`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - cs_monero_flutter_libs_macos (from `Flutter/ephemeral/.symlinks/plugins/cs_monero_flutter_libs_macos/macos`) + - cs_salvium_flutter_libs_macos (from `Flutter/ephemeral/.symlinks/plugins/cs_salvium_flutter_libs_macos/macos`) - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) @@ -86,7 +90,6 @@ DEPENDENCIES: - 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`) - local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) @@ -113,6 +116,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos cs_monero_flutter_libs_macos: :path: Flutter/ephemeral/.symlinks/plugins/cs_monero_flutter_libs_macos/macos + cs_salvium_flutter_libs_macos: + :path: Flutter/ephemeral/.symlinks/plugins/cs_salvium_flutter_libs_macos/macos desktop_drop: :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos device_info_plus: @@ -135,8 +140,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/frostdart/macos isar_flutter_libs: :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos - lelantus: - :path: Flutter/ephemeral/.symlinks/plugins/lelantus/macos local_auth_darwin: :path: Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin package_info_plus: @@ -165,6 +168,7 @@ SPEC CHECKSUMS: coinlib_flutter: 9275e8255ef67d3da33beb6e117d09ced4f46eb5 connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 cs_monero_flutter_libs_macos: b901f94d39d1338f706312b026aba928d23582d4 + cs_salvium_flutter_libs_macos: 3c7b30fb8c82ee0fb0195280ddcc10c65ab5e082 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 @@ -176,14 +180,13 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 frostdart: e6bf3119527ccfbcec1b8767da6ede5bb4c4f716 isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a - lelantus: 308e42c5a648598936a07a234471dd8cf8e687a0 local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630 - sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83 + sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b stack_wallet_backup: 6ebc60b1bdcf11cf1f1cbad9aa78332e1e15778c tor_ffi_plugin: 2566c1ed174688cca560fa0c64b7a799c66f07cb url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 diff --git a/pubspec.lock b/pubspec.lock index ffcc7e9a8..ab6a39710 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.11.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" another_flushbar: dependency: "direct main" description: @@ -62,14 +70,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" - barcode_scan2: - dependency: "direct main" - description: - name: barcode_scan2 - sha256: efbe38629e6df2200e4d60ebe252e8e041cd5ae7b50f194a20f01779ade9d1c3 - url: "https://pub.dev" - source: hosted - version: "4.5.0" basic_utils: dependency: "direct main" description: @@ -151,10 +151,10 @@ packages: dependency: "direct main" description: name: blockchain_utils - sha256: ebb6c336ba0120de0982c50d8bc597cb494a530bd22bd462895bb5cebde405af + sha256: "1e4f30b98d92f7ccf2eda009a23b53871a1c9b8b6dfa00bb1eb17ec00ae5eeeb" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.6.0" boolean_selector: dependency: transitive description: @@ -353,20 +353,20 @@ packages: dependency: "direct overridden" description: path: coinlib - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 - resolved-ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 + resolved-ref: da1b3659e296660ac2b36f81d155d2362a2b3195 url: "https://www.github.com/julian-CStack/coinlib" source: git - version: "3.1.0" + version: "4.1.0" coinlib_flutter: dependency: "direct main" description: path: coinlib_flutter - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 - resolved-ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 + resolved-ref: da1b3659e296660ac2b36f81d155d2362a2b3195 url: "https://www.github.com/julian-CStack/coinlib" source: git - version: "3.0.0" + version: "4.0.0" collection: dependency: transitive description: @@ -444,10 +444,10 @@ packages: dependency: "direct main" description: name: cs_monero - sha256: ed81d9e74ea71a8b8b0bfed07a284e14b6e5f4d0dbde774735f9f0a9ab60b7fb + sha256: f48495ed6744a47598b36eaf28adc1e9e55f0d4ea3c18fe42eda0d3d8f714206 url: "https://pub.dev" source: hosted - version: "1.0.0-pre.2" + version: "1.0.0-pre.3" cs_monero_flutter_libs: dependency: "direct main" description: @@ -528,6 +528,94 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0-pre" + cs_salvium: + dependency: "direct main" + description: + name: cs_salvium + sha256: "838a2f21b0ad567f68a5294360c4c96727b722037ae7bfdc26651c99d6c26bd3" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + cs_salvium_flutter_libs: + dependency: "direct main" + description: + name: cs_salvium_flutter_libs + sha256: d1e49ed67632f77d863ad3eafc78db8867f155cf9decf156345ec75c92e0d026 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + cs_salvium_flutter_libs_android: + dependency: transitive + description: + name: cs_salvium_flutter_libs_android + sha256: "63603fc4c94d609e13c8e8064c742ac628ef006d3af9990e2c585489bde9b96d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cs_salvium_flutter_libs_android_arm64_v8a: + dependency: transitive + description: + name: cs_salvium_flutter_libs_android_arm64_v8a + sha256: "5ced9fe6d71dd22f90865600b8dff1ed07ce480db6c9de1a8d56e63318000e97" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cs_salvium_flutter_libs_android_armeabi_v7a: + dependency: transitive + description: + name: cs_salvium_flutter_libs_android_armeabi_v7a + sha256: "2fb718dff22918e72b138c191dbd887d8d241f03a34add11e8d699c48b657b47" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cs_salvium_flutter_libs_android_x86_64: + dependency: transitive + description: + name: cs_salvium_flutter_libs_android_x86_64 + sha256: "85134ab635a4dddec5735fdd8f3971a1c33e3aaa1c8aa88e54f0b3d5e0d0caab" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cs_salvium_flutter_libs_ios: + dependency: transitive + description: + name: cs_salvium_flutter_libs_ios + sha256: "54d18fbac60c8a602e4d0f967ea7d02fab71bff3e032f9576e159229ce372534" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + cs_salvium_flutter_libs_linux: + dependency: transitive + description: + name: cs_salvium_flutter_libs_linux + sha256: "0cb2f545ea4aa45c819a0656540d022a9c73a43681e50f1d2a5e72eaf1bc500e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cs_salvium_flutter_libs_macos: + dependency: transitive + description: + name: cs_salvium_flutter_libs_macos + sha256: "9df0818299a5ddadd41eb4c94de2cd8e519d1e9f4aa6166657397200397f402f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cs_salvium_flutter_libs_platform_interface: + dependency: transitive + description: + name: cs_salvium_flutter_libs_platform_interface + sha256: "36ef1edd1481b92a95500fbdf397a371c1d624b58401a56638dc315f3c607dc0" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cs_salvium_flutter_libs_windows: + dependency: transitive + description: + name: cs_salvium_flutter_libs_windows + sha256: "824966223a32bfe4d99c634d3b8d81917806d06ad878007552597e2070c25a02" + url: "https://pub.dev" + source: hosted + version: "1.2.0" csslib: dependency: transitive description: @@ -665,6 +753,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + drift: + dependency: "direct main" + description: + name: drift + sha256: c2d073d35ad441730812f4ea05b5dd031fb81c5f9786a4f5fb77ecd6307b6f74 + url: "https://pub.dev" + source: hosted + version: "2.22.1" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: f4ab5d6976b1e31551ceb82ff597a505bda7818ff4f7be08a1da9d55eb6e730c + url: "https://pub.dev" + source: hosted + version: "2.22.1" + drift_flutter: + dependency: "direct main" + description: + name: drift_flutter + sha256: "9fd9b479c6187d6b3bbdfd2703df98010470a6c65c2a8c8c5a1034c620bd0a0e" + url: "https://pub.dev" + source: hosted + version: "0.2.3" dropdown_button2: dependency: "direct main" description: @@ -772,7 +884,7 @@ packages: source: git version: "8.3.1" fixnum: - dependency: transitive + dependency: "direct main" description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be @@ -832,11 +944,11 @@ packages: dependency: "direct main" description: path: "." - ref: e8c502652da7836cd1a22893339838553675b464 - resolved-ref: e8c502652da7836cd1a22893339838553675b464 + ref: f5fd2238fca4ffe82f7e14646a613a04d2c243d6 + resolved-ref: f5fd2238fca4ffe82f7e14646a613a04d2c243d6 url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git - version: "0.0.2" + version: "0.1.0" flutter_lints: dependency: "direct dev" description: @@ -869,6 +981,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.2.0" + flutter_mwebd: + dependency: "direct main" + description: + name: flutter_mwebd + sha256: "73b35b6eaccb6e1be7eb37e04bcc94f091244fa31b8aedc17e4119c580a7a747" + url: "https://pub.dev" + source: hosted + version: "0.0.1-pre.7" flutter_native_splash: dependency: "direct main" description: @@ -999,8 +1119,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9dc883f4432c8db4ec44cb8cc836963295d63952" - resolved-ref: "9dc883f4432c8db4ec44cb8cc836963295d63952" + ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 + resolved-ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 url: "https://github.com/cypherstack/fusiondart.git" source: git version: "1.0.0" @@ -1020,6 +1140,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.5" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db + url: "https://pub.dev" + source: hosted + version: "2.0.0" graphs: dependency: transitive description: @@ -1028,6 +1164,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + grpc: + dependency: transitive + description: + name: grpc + sha256: "30e1edae6846b163a64f6d8716e3443980fe1f7d2d1f086f011d24ea186f2582" + url: "https://pub.dev" + source: hosted + version: "4.0.4" hex: dependency: "direct main" description: @@ -1084,6 +1228,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.6" + http2: + dependency: transitive + description: + name: http2 + sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" + url: "https://pub.dev" + source: hosted + version: "2.3.1" http_multi_server: dependency: transitive description: @@ -1109,7 +1261,7 @@ packages: source: hosted version: "1.0.3" image: - dependency: transitive + dependency: "direct main" description: name: image sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d @@ -1241,13 +1393,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" - lelantus: - dependency: "direct main" - description: - path: "crypto_plugins/flutter_liblelantus" - relative: true - source: path - version: "0.0.3" lints: dependency: transitive description: @@ -1369,6 +1514,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" + url: "https://pub.dev" + source: hosted + version: "7.0.1" mockingjay: dependency: "direct dev" description: @@ -1409,6 +1562,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + mweb_client: + dependency: "direct main" + description: + name: mweb_client + sha256: "263ba560dab7e63a1d03875d455a19cc4a1ab9720786cd9d6ffcc42127d06732" + url: "https://pub.dev" + source: hosted + version: "0.2.0" namecoin: dependency: "direct main" description: @@ -1475,7 +1636,7 @@ packages: source: hosted version: "3.2.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" @@ -1542,18 +1703,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f" url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "12.0.0+1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "13.0.1" permission_handler_apple: dependency: transitive description: @@ -1654,10 +1815,10 @@ packages: dependency: transitive description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: "579fe5557eae58e3adca2e999e38f02441d8aa908703854a9e0a0f47fa857731" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.1.0" pub_semver: dependency: transitive description: @@ -1706,6 +1867,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" retry: dependency: transitive description: @@ -1722,14 +1891,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" - rxdart: - dependency: "direct main" - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" sec: dependency: transitive description: @@ -1861,18 +2022,26 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295 + sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.5" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "1e62698dc1ab396152ccaf3b3990d826244e9f3c8c39b51805f209adcd6dbea3" + sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + url: "https://pub.dev" + source: hosted + version: "0.5.24" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "4cad4b2c5f63dc9ea1a8dcffb58cf762322bea5dd8836870164a65e913bdae41" url: "https://pub.dev" source: hosted - version: "0.5.22" + version: "0.40.0" stack_trace: dependency: transitive description: @@ -2384,5 +2553,5 @@ packages: source: hosted version: "0.2.3" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.7.2 <4.0.0" flutter: ">=3.29.0" diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index 5438234ea..d791b933b 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -7,8 +7,6 @@ mkdir -p build PLUGINS_DIR=../../crypto_plugins -(cd "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android && ./build_all.sh ) - # libepiccash requires old rust source ../rust_version.sh set_rust_version_for_libepiccash diff --git a/scripts/android/build_all_campfire.sh b/scripts/android/build_all_campfire.sh index 5438234ea..d791b933b 100755 --- a/scripts/android/build_all_campfire.sh +++ b/scripts/android/build_all_campfire.sh @@ -7,8 +7,6 @@ mkdir -p build PLUGINS_DIR=../../crypto_plugins -(cd "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android && ./build_all.sh ) - # libepiccash requires old rust source ../rust_version.sh set_rust_version_for_libepiccash diff --git a/scripts/android/build_all_duo.sh b/scripts/android/build_all_duo.sh index 40be4bee4..39579d238 100755 --- a/scripts/android/build_all_duo.sh +++ b/scripts/android/build_all_duo.sh @@ -9,8 +9,6 @@ mkdir -p build PLUGINS_DIR=../../crypto_plugins -(cd "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android && ./build_all.sh ) - # libepiccash requires old rust source ../rust_version.sh set_rust_version_for_libepiccash diff --git a/scripts/app_config/configure_campfire.sh b/scripts/app_config/configure_campfire.sh index 883d67fa0..258058aae 100755 --- a/scripts/app_config/configure_campfire.sh +++ b/scripts/app_config/configure_campfire.sh @@ -62,6 +62,12 @@ final List _supportedCoins = List.unmodifiable([ Firo(CryptoCurrencyNetwork.main), ]); -final ({String from, String to}) _swapDefaults = (from: "BTC", to: "FIRO"); +final ({String from, String fromFuzzyNet, String to, String toFuzzyNet}) +_swapDefaults = ( + from: "BTC", + fromFuzzyNet: "btc", + to: "FIRO", + toFuzzyNet: "firo", +); EOF \ No newline at end of file diff --git a/scripts/app_config/configure_stack_duo.sh b/scripts/app_config/configure_stack_duo.sh index 7d1a7665a..4c871fff3 100755 --- a/scripts/app_config/configure_stack_duo.sh +++ b/scripts/app_config/configure_stack_duo.sh @@ -64,6 +64,12 @@ final List _supportedCoins = List.unmodifiable([ BitcoinFrost(CryptoCurrencyNetwork.test4), ]); -final ({String from, String to}) _swapDefaults = (from: "BTC", to: "XMR"); +final ({String from, String fromFuzzyNet, String to, String toFuzzyNet}) +_swapDefaults = ( + from: "BTC", + fromFuzzyNet: "btc", + to: "XMR", + toFuzzyNet: "xmr", +); EOF \ No newline at end of file diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index e46420fa0..5aa170a98 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -63,12 +63,14 @@ final List _supportedCoins = List.unmodifiable([ Ecash(CryptoCurrencyNetwork.main), Epiccash(CryptoCurrencyNetwork.main), Ethereum(CryptoCurrencyNetwork.main), + Fact0rn(CryptoCurrencyNetwork.main), Firo(CryptoCurrencyNetwork.main), Litecoin(CryptoCurrencyNetwork.main), Nano(CryptoCurrencyNetwork.main), Namecoin(CryptoCurrencyNetwork.main), Particl(CryptoCurrencyNetwork.main), Peercoin(CryptoCurrencyNetwork.main), + Salvium(CryptoCurrencyNetwork.main), Solana(CryptoCurrencyNetwork.main), Stellar(CryptoCurrencyNetwork.main), Tezos(CryptoCurrencyNetwork.main), @@ -87,6 +89,12 @@ final List _supportedCoins = List.unmodifiable([ Xelis(CryptoCurrencyNetwork.test), ]); -final ({String from, String to}) _swapDefaults = (from: "BTC", to: "XMR"); +final ({String from, String fromFuzzyNet, String to, String toFuzzyNet}) +_swapDefaults = ( + from: "BTC", + fromFuzzyNet: "btc", + to: "XMR", + toFuzzyNet: "xmr", +); EOF \ No newline at end of file diff --git a/scripts/app_config/templates/android/app/src/main/AndroidManifest.xml b/scripts/app_config/templates/android/app/src/main/AndroidManifest.xml index 07f7a3ef0..d10488cc9 100644 --- a/scripts/app_config/templates/android/app/src/main/AndroidManifest.xml +++ b/scripts/app_config/templates/android/app/src/main/AndroidManifest.xml @@ -5,12 +5,6 @@ android:name="android.permission.INTERNET"/> - - - list = [ "hello", @@ -92,7 +103,10 @@ void main() { test("build a uri string with empty params", () { expect( AddressUtils.buildUriString( - Firo(CryptoCurrencyNetwork.main).uriScheme, firoAddress, {}), + Firo(CryptoCurrencyNetwork.main).uriScheme, + firoAddress, + {}, + ), "firo:$firoAddress", ); }); diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 4ac0b9849..22ab2a712 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -531,6 +531,67 @@ class MockElectrumXClient extends _i1.Mock implements _i6.ElectrumXClient { returnValue: _i9.Future>>.value(>[]), ) as _i9.Future>>); + @override + _i9.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i9.Future>.value( + <({String address, String name})>[]), + ) as _i9.Future>); + + @override + _i9.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i9.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i8.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i8.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i9.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i9.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, @@ -850,6 +911,36 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), diff --git a/test/hive/db_test.dart b/test/hive/db_test.dart index 2f5130837..fb4e71434 100644 --- a/test/hive/db_test.dart +++ b/test/hive/db_test.dart @@ -6,31 +6,39 @@ void main() { group("DB box names", () { test("address book", () => expect(DB.boxNameAddressBook, "addressBook")); test("nodes", () => expect(DB.boxNameNodeModels, "nodeModels")); - test("primary nodes", () => expect(DB.boxNamePrimaryNodes, "primaryNodes")); test("wallets info", () => expect(DB.boxNameAllWalletsData, "wallets")); - test("notifications", - () => expect(DB.boxNameNotifications, "notificationModels")); test( - "watched transactions", - () => expect( - DB.boxNameWatchedTransactions, "watchedTxNotificationModels")); + "notifications", + () => expect(DB.boxNameNotifications, "notificationModels"), + ); test( - "watched trades", - () => - expect(DB.boxNameWatchedTrades, "watchedTradesNotificationModels")); + "watched transactions", + () => + expect(DB.boxNameWatchedTransactions, "watchedTxNotificationModels"), + ); + test( + "watched trades", + () => expect(DB.boxNameWatchedTrades, "watchedTradesNotificationModels"), + ); test("trades", () => expect(DB.boxNameTrades, "exchangeTransactionsBox")); test("trade notes", () => expect(DB.boxNameTradeNotes, "tradeNotesBox")); - test("tx <> trade lookup table", - () => expect(DB.boxNameTradeLookup, "tradeToTxidLookUpBox")); - test("favorite wallets", - () => expect(DB.boxNameFavoriteWallets, "favoriteWallets")); + test( + "tx <> trade lookup table", + () => expect(DB.boxNameTradeLookup, "tradeToTxidLookUpBox"), + ); + test( + "favorite wallets", + () => expect(DB.boxNameFavoriteWallets, "favoriteWallets"), + ); test("preferences", () => expect(DB.boxNamePrefs, "prefs")); test( - "deleted wallets to clear out on start", - () => - expect(DB.boxNameWalletsToDeleteOnStart, "walletsToDeleteOnStart")); - test("price cache", - () => expect(DB.boxNamePriceCache, "priceAPIPrice24hCache")); + "deleted wallets to clear out on start", + () => expect(DB.boxNameWalletsToDeleteOnStart, "walletsToDeleteOnStart"), + ); + test( + "price cache", + () => expect(DB.boxNamePriceCache, "priceAPIPrice24hCache"), + ); }); group("tests requiring test hive environment", () { diff --git a/test/models/fee_object_model_test.dart b/test/models/fee_object_model_test.dart index 52045d662..1f108d531 100644 --- a/test/models/fee_object_model_test.dart +++ b/test/models/fee_object_model_test.dart @@ -4,27 +4,16 @@ import 'package:stackwallet/models/models.dart'; void main() { test("FeeObject constructor", () { final feeObject = FeeObject( - fast: 3, - medium: 2, - slow: 1, + fast: BigInt.from(3), + medium: BigInt.from(2), + slow: BigInt.from(1), numberOfBlocksFast: 4, numberOfBlocksSlow: 5, numberOfBlocksAverage: 10, ); - expect(feeObject.toString(), - "{fast: 3, medium: 2, slow: 1, numberOfBlocksFast: 4, numberOfBlocksAverage: 10, numberOfBlocksSlow: 5}"); - }); - - test("FeeObject.fromJson factory", () { - final feeObject = FeeObject.fromJson({ - "fast": 3, - "average": 2, - "slow": 1, - "numberOfBlocksFast": 4, - "numberOfBlocksSlow": 5, - "numberOfBlocksAverage": 6, - }); - expect(feeObject.toString(), - "{fast: 3, medium: 2, slow: 1, numberOfBlocksFast: 4, numberOfBlocksAverage: 6, numberOfBlocksSlow: 5}"); + expect( + feeObject.toString(), + "{fast: 3, medium: 2, slow: 1, numberOfBlocksFast: 4, numberOfBlocksAverage: 10, numberOfBlocksSlow: 5}", + ); }); } diff --git a/test/models/lelantus_fee_data_test.dart b/test/models/lelantus_fee_data_test.dart deleted file mode 100644 index a6e2ebb9b..000000000 --- a/test/models/lelantus_fee_data_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:stackwallet/models/lelantus_fee_data.dart'; - -void main() { - test("LelantusFeeData constructor", () { - final lfData = LelantusFeeData(10000, 3794, [1, 2, 1, 0, 1]); - expect(lfData.toString(), - "{changeToMint: 10000, fee: 3794, spendCoinIndexes: [1, 2, 1, 0, 1]}"); - }); -} diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 476ab3883..7ea2055fc 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -202,6 +202,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -209,6 +210,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i10.Future.value(), @@ -339,14 +341,14 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as List<_i13.NodeModel>); @override - _i10.Future add( + _i10.Future save( _i13.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -393,25 +395,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i13.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( @@ -848,6 +831,36 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), diff --git a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart index 25bfa08e4..192ce703a 100644 --- a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart @@ -3,14 +3,14 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i4; import 'dart:ui' as _i7; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i3; import 'package:stackwallet/services/address_book_service.dart' as _i6; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i4; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -49,29 +49,28 @@ class _FakeContactEntry_1 extends _i1.SmartFake implements _i3.ContactEntry { /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i4.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i4.Future<_i2.ScanResult> scan({required _i5.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i5.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i5.Future<_i2.ScanResult>); + ) as _i4.Future<_i2.ScanResult>); } /// A class which mocks [AddressBookService]. @@ -111,15 +110,15 @@ class MockAddressBookService extends _i1.Mock ) as _i3.ContactEntry); @override - _i5.Future> search(String? text) => + _i4.Future> search(String? text) => (super.noSuchMethod( Invocation.method( #search, [text], ), returnValue: - _i5.Future>.value(<_i3.ContactEntry>[]), - ) as _i5.Future>); + _i4.Future>.value(<_i3.ContactEntry>[]), + ) as _i4.Future>); @override bool matches( @@ -138,33 +137,33 @@ class MockAddressBookService extends _i1.Mock ) as bool); @override - _i5.Future addContact(_i3.ContactEntry? contact) => (super.noSuchMethod( + _i4.Future addContact(_i3.ContactEntry? contact) => (super.noSuchMethod( Invocation.method( #addContact, [contact], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future editContact(_i3.ContactEntry? editedContact) => + _i4.Future editContact(_i3.ContactEntry? editedContact) => (super.noSuchMethod( Invocation.method( #editContact, [editedContact], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future removeContact(String? id) => (super.noSuchMethod( + _i4.Future removeContact(String? id) => (super.noSuchMethod( Invocation.method( #removeContact, [id], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void addListener(_i7.VoidCallback? listener) => super.noSuchMethod( diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 565f05f82..643d6cc6b 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -10,22 +10,17 @@ import 'package:decimal/decimal.dart' as _i19; import 'package:logger/logger.dart' as _i9; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i7; -import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart' +import 'package:stackwallet/models/exchange/change_now/cn_exchange_transaction.dart' as _i22; -import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart' - as _i24; -import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart' - as _i25; +import 'package:stackwallet/models/exchange/change_now/cn_exchange_transaction_status.dart' + as _i23; import 'package:stackwallet/models/exchange/response_objects/estimate.dart' as _i21; -import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart' - as _i23; import 'package:stackwallet/models/exchange/response_objects/range.dart' as _i20; import 'package:stackwallet/models/exchange/response_objects/trade.dart' as _i15; import 'package:stackwallet/models/isar/exchange_cache/currency.dart' as _i18; -import 'package:stackwallet/models/isar/exchange_cache/pair.dart' as _i26; import 'package:stackwallet/networking/http.dart' as _i3; import 'package:stackwallet/services/exchange/change_now/change_now_api.dart' as _i17; @@ -277,6 +272,36 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), @@ -1031,16 +1056,22 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { @override _i10.Future<_i4.ExchangeResponse>> getAvailableCurrencies({ - bool? fixedRate, bool? active, + bool? buy, + bool? sell, + _i17.CNFlow? flow = _i17.CNFlow.standard, + String? apiKey, }) => (super.noSuchMethod( Invocation.method( #getAvailableCurrencies, [], { - #fixedRate: fixedRate, #active: active, + #buy: buy, + #sell: sell, + #flow: flow, + #apiKey: apiKey, }, ), returnValue: @@ -1051,64 +1082,23 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getAvailableCurrencies, [], { - #fixedRate: fixedRate, #active: active, + #buy: buy, + #sell: sell, + #flow: flow, + #apiKey: apiKey, }, ), )), ) as _i10.Future<_i4.ExchangeResponse>>); - @override - _i10.Future<_i4.ExchangeResponse>> getCurrenciesV2() => - (super.noSuchMethod( - Invocation.method( - #getCurrenciesV2, - [], - ), - returnValue: - _i10.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getCurrenciesV2, - [], - ), - )), - ) as _i10.Future<_i4.ExchangeResponse>>); - - @override - _i10.Future<_i4.ExchangeResponse>> getPairedCurrencies({ - required String? ticker, - bool? fixedRate, - }) => - (super.noSuchMethod( - Invocation.method( - #getPairedCurrencies, - [], - { - #ticker: ticker, - #fixedRate: fixedRate, - }, - ), - returnValue: - _i10.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getPairedCurrencies, - [], - { - #ticker: ticker, - #fixedRate: fixedRate, - }, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse>>); - @override _i10.Future<_i4.ExchangeResponse<_i19.Decimal>> getMinimalExchangeAmount({ - required String? fromTicker, - required String? toTicker, + required String? fromCurrency, + required String? toCurrency, + String? fromNetwork, + String? toNetwork, + _i17.CNFlow? flow = _i17.CNFlow.standard, String? apiKey, }) => (super.noSuchMethod( @@ -1116,8 +1106,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getMinimalExchangeAmount, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, #apiKey: apiKey, }, ), @@ -1128,8 +1121,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getMinimalExchangeAmount, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, #apiKey: apiKey, }, ), @@ -1138,9 +1134,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { @override _i10.Future<_i4.ExchangeResponse<_i20.Range>> getRange({ - required String? fromTicker, - required String? toTicker, - required bool? isFixedRate, + required String? fromCurrency, + required String? toCurrency, + String? fromNetwork, + String? toNetwork, + _i17.CNFlow? flow = _i17.CNFlow.standard, String? apiKey, }) => (super.noSuchMethod( @@ -1148,9 +1146,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getRange, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #isFixedRate: isFixedRate, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, #apiKey: apiKey, }, ), @@ -1161,9 +1161,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getRange, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #isFixedRate: isFixedRate, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, #apiKey: apiKey, }, ), @@ -1172,9 +1174,16 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { @override _i10.Future<_i4.ExchangeResponse<_i21.Estimate>> getEstimatedExchangeAmount({ - required String? fromTicker, - required String? toTicker, - required _i19.Decimal? fromAmount, + required String? fromCurrency, + required String? toCurrency, + _i19.Decimal? fromAmount, + _i19.Decimal? toAmount, + String? fromNetwork, + String? toNetwork, + _i17.CNFlow? flow = _i17.CNFlow.standard, + _i17.CNExchangeType? type = _i17.CNExchangeType.direct, + bool? useRateId, + bool? isTopUp, String? apiKey, }) => (super.noSuchMethod( @@ -1182,9 +1191,16 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getEstimatedExchangeAmount, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, #fromAmount: fromAmount, + #toAmount: toAmount, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, + #type: type, + #useRateId: useRateId, + #isTopUp: isTopUp, #apiKey: apiKey, }, ), @@ -1195,9 +1211,16 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getEstimatedExchangeAmount, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, #fromAmount: fromAmount, + #toAmount: toAmount, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, + #type: type, + #useRateId: useRateId, + #isTopUp: isTopUp, #apiKey: apiKey, }, ), @@ -1205,277 +1228,109 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { ) as _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>); @override - _i10.Future<_i4.ExchangeResponse<_i21.Estimate>> - getEstimatedExchangeAmountFixedRate({ - required String? fromTicker, - required String? toTicker, - required _i19.Decimal? fromAmount, - required bool? reversed, - bool? useRateId = true, - String? apiKey, - }) => - (super.noSuchMethod( - Invocation.method( - #getEstimatedExchangeAmountFixedRate, - [], - { - #fromTicker: fromTicker, - #toTicker: toTicker, - #fromAmount: fromAmount, - #reversed: reversed, - #useRateId: useRateId, - #apiKey: apiKey, - }, - ), - returnValue: _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>.value( - _FakeExchangeResponse_2<_i21.Estimate>( - this, - Invocation.method( - #getEstimatedExchangeAmountFixedRate, - [], - { - #fromTicker: fromTicker, - #toTicker: toTicker, - #fromAmount: fromAmount, - #reversed: reversed, - #useRateId: useRateId, - #apiKey: apiKey, - }, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>); - - @override - _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>> - getEstimatedExchangeAmountV2({ - required String? fromTicker, - required String? toTicker, - required _i22.CNEstimateType? fromOrTo, - required _i19.Decimal? amount, - String? fromNetwork, - String? toNetwork, - _i22.CNFlowType? flow = _i22.CNFlowType.standard, + _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeTransaction>> + createExchangeTransaction({ + required String? fromCurrency, + required String? fromNetwork, + required String? toCurrency, + required String? toNetwork, + _i19.Decimal? fromAmount, + _i19.Decimal? toAmount, + _i17.CNFlow? flow = _i17.CNFlow.standard, + _i17.CNExchangeType? type = _i17.CNExchangeType.direct, + required String? address, + String? extraId, + String? refundAddress, + String? refundExtraId, + String? userId, + String? payload, + String? contactEmail, + required String? rateId, String? apiKey, }) => (super.noSuchMethod( Invocation.method( - #getEstimatedExchangeAmountV2, + #createExchangeTransaction, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #fromOrTo: fromOrTo, - #amount: amount, + #fromCurrency: fromCurrency, #fromNetwork: fromNetwork, + #toCurrency: toCurrency, #toNetwork: toNetwork, + #fromAmount: fromAmount, + #toAmount: toAmount, #flow: flow, - #apiKey: apiKey, - }, - ), - returnValue: _i10 - .Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>>.value( - _FakeExchangeResponse_2<_i22.CNExchangeEstimate>( - this, - Invocation.method( - #getEstimatedExchangeAmountV2, - [], - { - #fromTicker: fromTicker, - #toTicker: toTicker, - #fromOrTo: fromOrTo, - #amount: amount, - #fromNetwork: fromNetwork, - #toNetwork: toNetwork, - #flow: flow, - #apiKey: apiKey, - }, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>>); - - @override - _i10.Future<_i4.ExchangeResponse>> - getAvailableFixedRateMarkets({String? apiKey}) => (super.noSuchMethod( - Invocation.method( - #getAvailableFixedRateMarkets, - [], - {#apiKey: apiKey}, - ), - returnValue: _i10 - .Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getAvailableFixedRateMarkets, - [], - {#apiKey: apiKey}, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse>>); - - @override - _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>> - createStandardExchangeTransaction({ - required String? fromTicker, - required String? toTicker, - required String? receivingAddress, - required _i19.Decimal? amount, - String? extraId = r'', - String? userId = r'', - String? contactEmail = r'', - String? refundAddress = r'', - String? refundExtraId = r'', - String? apiKey, - }) => - (super.noSuchMethod( - Invocation.method( - #createStandardExchangeTransaction, - [], - { - #fromTicker: fromTicker, - #toTicker: toTicker, - #receivingAddress: receivingAddress, - #amount: amount, + #type: type, + #address: address, #extraId: extraId, - #userId: userId, - #contactEmail: contactEmail, #refundAddress: refundAddress, #refundExtraId: refundExtraId, + #userId: userId, + #payload: payload, + #contactEmail: contactEmail, + #rateId: rateId, #apiKey: apiKey, }, ), returnValue: _i10 - .Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>.value( - _FakeExchangeResponse_2<_i24.ExchangeTransaction>( + .Future<_i4.ExchangeResponse<_i22.CNExchangeTransaction>>.value( + _FakeExchangeResponse_2<_i22.CNExchangeTransaction>( this, Invocation.method( - #createStandardExchangeTransaction, + #createExchangeTransaction, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #receivingAddress: receivingAddress, - #amount: amount, + #fromCurrency: fromCurrency, + #fromNetwork: fromNetwork, + #toCurrency: toCurrency, + #toNetwork: toNetwork, + #fromAmount: fromAmount, + #toAmount: toAmount, + #flow: flow, + #type: type, + #address: address, #extraId: extraId, - #userId: userId, - #contactEmail: contactEmail, #refundAddress: refundAddress, #refundExtraId: refundExtraId, + #userId: userId, + #payload: payload, + #contactEmail: contactEmail, + #rateId: rateId, #apiKey: apiKey, }, ), )), - ) as _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>); + ) as _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeTransaction>>); @override - _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>> - createFixedRateExchangeTransaction({ - required String? fromTicker, - required String? toTicker, - required String? receivingAddress, - required _i19.Decimal? amount, - required String? rateId, - required bool? reversed, - String? extraId = r'', - String? userId = r'', - String? contactEmail = r'', - String? refundAddress = r'', - String? refundExtraId = r'', + _i10.Future<_i4.ExchangeResponse<_i23.CNExchangeTransactionStatus>> + getTransactionStatus({ + required String? id, String? apiKey, }) => (super.noSuchMethod( Invocation.method( - #createFixedRateExchangeTransaction, + #getTransactionStatus, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #receivingAddress: receivingAddress, - #amount: amount, - #rateId: rateId, - #reversed: reversed, - #extraId: extraId, - #userId: userId, - #contactEmail: contactEmail, - #refundAddress: refundAddress, - #refundExtraId: refundExtraId, + #id: id, #apiKey: apiKey, }, ), - returnValue: _i10 - .Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>.value( - _FakeExchangeResponse_2<_i24.ExchangeTransaction>( + returnValue: _i10.Future< + _i4 + .ExchangeResponse<_i23.CNExchangeTransactionStatus>>.value( + _FakeExchangeResponse_2<_i23.CNExchangeTransactionStatus>( this, Invocation.method( - #createFixedRateExchangeTransaction, + #getTransactionStatus, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #receivingAddress: receivingAddress, - #amount: amount, - #rateId: rateId, - #reversed: reversed, - #extraId: extraId, - #userId: userId, - #contactEmail: contactEmail, - #refundAddress: refundAddress, - #refundExtraId: refundExtraId, + #id: id, #apiKey: apiKey, }, ), )), - ) as _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>); - - @override - _i10.Future< - _i4 - .ExchangeResponse<_i25.ExchangeTransactionStatus>> getTransactionStatus({ - required String? id, - String? apiKey, - }) => - (super.noSuchMethod( - Invocation.method( - #getTransactionStatus, - [], - { - #id: id, - #apiKey: apiKey, - }, - ), - returnValue: _i10 - .Future<_i4.ExchangeResponse<_i25.ExchangeTransactionStatus>>.value( - _FakeExchangeResponse_2<_i25.ExchangeTransactionStatus>( - this, - Invocation.method( - #getTransactionStatus, - [], - { - #id: id, - #apiKey: apiKey, - }, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse<_i25.ExchangeTransactionStatus>>); - - @override - _i10.Future<_i4.ExchangeResponse>> - getAvailableFloatingRatePairs({bool? includePartners = false}) => - (super.noSuchMethod( - Invocation.method( - #getAvailableFloatingRatePairs, - [], - {#includePartners: includePartners}, - ), - returnValue: - _i10.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getAvailableFloatingRatePairs, - [], - {#includePartners: includePartners}, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse>>); + ) as _i10 + .Future<_i4.ExchangeResponse<_i23.CNExchangeTransactionStatus>>); } diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index a3523a093..804721d4c 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -202,14 +202,14 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { ) as List<_i7.NodeModel>); @override - _i4.Future add( + _i4.Future save( _i7.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -256,25 +256,6 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override - _i4.Future edit( - _i7.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 4f9addbc7..9218bd83e 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -202,14 +202,14 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { ) as List<_i7.NodeModel>); @override - _i4.Future add( + _i4.Future save( _i7.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -256,25 +256,6 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override - _i4.Future edit( - _i7.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index 0603d61be..68ac1be3e 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -3,15 +3,15 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i4; import 'dart:ui' as _i7; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/models/node_model.dart' as _i9; import 'package:stackwallet/services/node_service.dart' as _i8; import 'package:stackwallet/services/wallets_service.dart' as _i6; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i4; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' as _i3; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' @@ -55,29 +55,28 @@ class _FakeSecureStorageInterface_1 extends _i1.SmartFake /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i4.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i4.Future<_i2.ScanResult> scan({required _i5.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i5.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i5.Future<_i2.ScanResult>); + ) as _i4.Future<_i2.ScanResult>); } /// A class which mocks [WalletsService]. @@ -89,12 +88,12 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { } @override - _i5.Future> get walletNames => + _i4.Future> get walletNames => (super.noSuchMethod( Invocation.getter(#walletNames), - returnValue: _i5.Future>.value( + returnValue: _i4.Future>.value( {}), - ) as _i5.Future>); + ) as _i4.Future>); @override bool get hasListeners => (super.noSuchMethod( @@ -175,17 +174,17 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { ) as bool); @override - _i5.Future updateDefaults() => (super.noSuchMethod( + _i4.Future updateDefaults() => (super.noSuchMethod( Invocation.method( #updateDefaults, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future setPrimaryNodeFor({ + _i4.Future setPrimaryNodeFor({ required _i10.CryptoCurrency? coin, required _i9.NodeModel? node, bool? shouldNotifyListeners = false, @@ -200,9 +199,9 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override _i9.NodeModel? getPrimaryNodeFor({required _i10.CryptoCurrency? currency}) => @@ -243,26 +242,26 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { ) as List<_i9.NodeModel>); @override - _i5.Future add( + _i4.Future save( _i9.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future delete( + _i4.Future delete( String? id, bool? shouldNotifyListeners, ) => @@ -274,12 +273,12 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future setEnabledState( + _i4.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, @@ -293,38 +292,19 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future edit( - _i9.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - - @override - _i5.Future updateCommunityNodes() => (super.noSuchMethod( + _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( #updateCommunityNodes, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void addListener(_i7.VoidCallback? listener) => super.noSuchMethod( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index 75a10f43e..b4eb99213 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -142,14 +142,14 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) as List<_i4.NodeModel>); @override - _i5.Future add( + _i5.Future save( _i4.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index fb159cea3..787f1f801 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -142,14 +142,14 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) as List<_i4.NodeModel>); @override - _i5.Future add( + _i5.Future save( _i4.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart index c60df280d..6a2a923e0 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart @@ -142,14 +142,14 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) as List<_i4.NodeModel>); @override - _i5.Future add( + _i5.Future save( _i4.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( 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 40a295640..d3a16fd4f 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,7 +3,7 @@ // 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 'dart:ui' as _i13; import 'package:local_auth/local_auth.dart' as _i7; @@ -11,13 +11,13 @@ import 'package:local_auth_android/local_auth_android.dart' as _i8; import 'package:local_auth_darwin/local_auth_darwin.dart' as _i9; import 'package:local_auth_windows/local_auth_windows.dart' as _i10; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i6; +import 'package:mockito/src/dummies.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 _i12; import 'package:stackwallet/utilities/biometrics.dart' as _i11; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' - as _i5; + as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -61,33 +61,13 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i2.ElectrumXClient); - @override - _i4.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i5.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, [source], ), - returnValue: _i6.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #base64ToHex, @@ -102,7 +82,7 @@ class MockCachedElectrumXClient extends _i1.Mock #base64ToReverseHex, [source], ), - returnValue: _i6.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #base64ToReverseHex, @@ -112,9 +92,9 @@ class MockCachedElectrumXClient extends _i1.Mock ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i5.CryptoCurrency? cryptoCurrency, + required _i6.CryptoCurrency? cryptoCurrency, bool? verbose = true, }) => (super.noSuchMethod( @@ -128,38 +108,21 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); - - @override - _i4.Future> getUsedCoinSerials({ - required _i5.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache( - {required _i5.CryptoCurrency? cryptoCurrency}) => + _i5.Future clearSharedTransactionCache( + {required _i6.CryptoCurrency? cryptoCurrency}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#cryptoCurrency: cryptoCurrency}, ), - 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]. @@ -172,13 +135,13 @@ class MockLocalAuthentication extends _i1.Mock } @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 authenticate({ + _i5.Future authenticate({ required String? localizedReason, Iterable<_i8.AuthMessages>? authMessages = const [ _i9.IOSAuthMessages(), @@ -197,37 +160,37 @@ class MockLocalAuthentication extends _i1.Mock #options: options, }, ), - 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(<_i8.BiometricType>[]), - ) as _i4.Future>); + _i5.Future>.value(<_i8.BiometricType>[]), + ) as _i5.Future>); } /// A class which mocks [Biometrics]. @@ -239,7 +202,7 @@ class MockBiometrics extends _i1.Mock implements _i11.Biometrics { } @override - _i4.Future authenticate({ + _i5.Future authenticate({ required String? cancelButtonText, required String? localizedReason, required String? title, @@ -254,8 +217,8 @@ class MockBiometrics extends _i1.Mock implements _i11.Biometrics { #title: title, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); } /// A class which mocks [WalletsService]. @@ -267,12 +230,12 @@ class MockWalletsService extends _i1.Mock implements _i12.WalletsService { } @override - _i4.Future> get walletNames => + _i5.Future> get walletNames => (super.noSuchMethod( Invocation.getter(#walletNames), - returnValue: _i4.Future>.value( + returnValue: _i5.Future>.value( {}), - ) as _i4.Future>); + ) as _i5.Future>); @override bool get hasListeners => (super.noSuchMethod( diff --git a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart index a7a3ad695..cd4db9558 100644 --- a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart @@ -3,11 +3,11 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i3; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i3; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -36,27 +36,26 @@ class _FakeScanResult_0 extends _i1.SmartFake implements _i2.ScanResult { /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i3.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i4.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i3.Future<_i2.ScanResult> scan({required _i4.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i3.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i4.Future<_i2.ScanResult>); + ) as _i3.Future<_i2.ScanResult>); } diff --git a/test/services/change_now/change_now_test.dart b/test/services/change_now/change_now_test.dart index f49c52064..f922bc3ed 100644 --- a/test/services/change_now/change_now_test.dart +++ b/test/services/change_now/change_now_test.dart @@ -8,7 +8,6 @@ import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart'; import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; -import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; import 'package:stackwallet/networking/http.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_api.dart'; @@ -23,12 +22,16 @@ void main() { final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode(jsonEncode(availableCurrenciesJSON)), 200)); + when( + client.get( + url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => + Response(utf8.encode(jsonEncode(availableCurrenciesJSON)), 200), + ); final result = await instance.getAvailableCurrencies(); @@ -41,12 +44,18 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies?active=true"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(availableCurrenciesJSONActive)), 200)); + when( + client.get( + url: Uri.parse("https://api.ChangeNow.io/v1/currencies?active=true"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode(jsonEncode(availableCurrenciesJSONActive)), + 200, + ), + ); final result = await instance.getAvailableCurrencies(active: true); @@ -59,36 +68,24 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies?fixedRate=true"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(availableCurrenciesJSONFixedRate)), 200)); - - final result = await instance.getAvailableCurrencies(fixedRate: true); - - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value!.length, 410); - }); - - test("getAvailableCurrencies succeeds with fixedRate and active options", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/currencies?fixedRate=true&active=true"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(availableCurrenciesJSONActiveFixedRate)), - 200)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/currencies?fixedRate=true", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode(jsonEncode(availableCurrenciesJSONFixedRate)), + 200, + ), + ); - final result = - await instance.getAvailableCurrencies(active: true, fixedRate: true); + final result = await instance.getAvailableCurrencies( + flow: CNFlow.fixedRate, + ); expect(result.exception, null); expect(result.value == null, false); @@ -96,116 +93,84 @@ void main() { }); test( - "getAvailableCurrencies fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode('{"some unexpected": "but valid json data"}'), 200)); - - final result = await instance.getAvailableCurrencies(); + "getAvailableCurrencies succeeds with fixedRate and active options", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/currencies?fixedRate=true&active=true", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode(jsonEncode(availableCurrenciesJSONActiveFixedRate)), + 200, + ), + ); + + final result = await instance.getAvailableCurrencies( + active: true, + flow: CNFlow.fixedRate, + ); + + expect(result.exception, null); + expect(result.value == null, false); + expect(result.value!.length, 410); + }, + ); - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); + test( + "getAvailableCurrencies fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode('{"some unexpected": "but valid json data"}'), + 200, + ), + ); + + final result = await instance.getAvailableCurrencies(); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); test("getAvailableCurrencies fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(""), 400)); + when( + client.get( + url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(""), 400)); final result = await instance.getAvailableCurrencies(); expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - }); - - group("getPairedCurrencies", () { - test("getPairedCurrencies succeeds without fixedRate option", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies-to/XMR"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode(jsonEncode(getPairedCurrenciesJSON)), 200)); - - final result = await instance.getPairedCurrencies(ticker: "XMR"); - - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value!.length, 537); - }); - - test("getPairedCurrencies succeeds with fixedRate option", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/currencies-to/XMR?fixedRate=true"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(getPairedCurrenciesJSONFixedRate)), 200)); - - final result = - await instance.getPairedCurrencies(ticker: "XMR", fixedRate: true); - - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value!.length, 410); - }); - - test( - "getPairedCurrencies fails with ChangeNowExceptionType.serializeResponseError A", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies-to/XMR"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode('[{"some unexpected": "but valid json data"}]'), 200)); - - final result = await instance.getPairedCurrencies(ticker: "XMR"); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - - test("getPairedCurrencies fails for any other reason", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(""), 400)); - - final result = - await instance.getPairedCurrencies(ticker: "XMR", fixedRate: true); - - expect(result.exception!.type, ExchangeExceptionType.generic); + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); expect(result.value == null, true); }); }); @@ -215,17 +180,22 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"minAmount": 42}'), 200)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => + Response(utf8.encode('{"minAmount": 42}'), 200), + ); final result = await instance.getMinimalExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", + fromCurrency: "xmr", + toCurrency: "btc", apiKey: "testAPIKEY", ); @@ -235,49 +205,61 @@ void main() { }); test( - "getMinimalExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getMinimalExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); + "getMinimalExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response(utf8.encode('{"error": 42}'), 200), + ); + + final result = await instance.getMinimalExchangeAmount( + fromCurrency: "xmr", + toCurrency: "btc", + apiKey: "testAPIKEY", + ); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); test("getMinimalExchangeAmount fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); final result = await instance.getMinimalExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", + fromCurrency: "xmr", + toCurrency: "btc", apiKey: "testAPIKEY", ); expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); expect(result.value == null, true); }); }); @@ -287,19 +269,26 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( utf8.encode( - '{"estimatedAmount": 58.4142873, "transactionSpeedForecast": "10-60", "warningMessage": null}'), - 200)); + '{"estimatedAmount": 58.4142873, "transactionSpeedForecast": "10-60", "warningMessage": null}', + ), + 200, + ), + ); final result = await instance.getEstimatedExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", + fromCurrency: "xmr", + toCurrency: "btc", fromAmount: Decimal.fromInt(42), apiKey: "testAPIKEY", ); @@ -310,45 +299,55 @@ void main() { }); test( - "getEstimatedExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getEstimatedExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", - fromAmount: Decimal.fromInt(42), - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); + "getEstimatedExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response(utf8.encode('{"error": 42}'), 200), + ); + + final result = await instance.getEstimatedExchangeAmount( + fromCurrency: "xmr", + toCurrency: "btc", + fromAmount: Decimal.fromInt(42), + apiKey: "testAPIKEY", + ); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); test("getEstimatedExchangeAmount fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); final result = await instance.getEstimatedExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", + fromCurrency: "xmr", + toCurrency: "btc", fromAmount: Decimal.fromInt(42), apiKey: "testAPIKEY", ); @@ -373,8 +372,8 @@ void main() { // // final result = // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( - // fromTicker: "xmr", - // toTicker: "btc", + // fromCurrency: "xmr", + // toCurrency: "btc", // fromAmount: Decimal.fromInt(10), // apiKey: "testAPIKEY", // ); @@ -400,8 +399,8 @@ void main() { // // final result = // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( - // fromTicker: "xmr", - // toTicker: "btc", + // fromCurrency: "xmr", + // toCurrency: "btc", // fromAmount: Decimal.fromInt(10), // apiKey: "testAPIKEY", // ); @@ -425,8 +424,8 @@ void main() { // // final result = // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( - // fromTicker: "xmr", - // toTicker: "btc", + // fromCurrency: "xmr", + // toCurrency: "btc", // fromAmount: Decimal.fromInt(10), // apiKey: "testAPIKEY", // ); @@ -436,95 +435,38 @@ void main() { // }); // }); - group("getAvailableFixedRateMarkets", () { - test("getAvailableFixedRateMarkets succeeds", () async { + group("createExchangeTransaction", () { + test("createExchangeTransaction succeeds", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode(jsonEncode(fixedRateMarketsJSON)), 200)); - - final result = await instance.getAvailableFixedRateMarkets( - apiKey: "testAPIKEY", + when( + client.post( + url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', + encoding: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode(jsonEncode(createStandardTransactionResponse)), + 200, + ), ); - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value!.length, 237); - }); - - test( - "getAvailableFixedRateMarkets fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getAvailableFixedRateMarkets( - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - - test("getAvailableFixedRateMarkets fails for any other reason", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); - - final result = await instance.getAvailableFixedRateMarkets( - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - }); - - group("createStandardExchangeTransaction", () { - test("createStandardExchangeTransaction succeeds", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.post( - url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', - encoding: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(createStandardTransactionResponse)), 200)); - - final result = await instance.createStandardExchangeTransaction( - fromTicker: "xmr", - toTicker: "btc", - receivingAddress: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", - amount: Decimal.parse("0.3"), + final result = await instance.createExchangeTransaction( + fromCurrency: "xmr", + toCurrency: "btc", + address: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", + fromAmount: Decimal.parse("0.3"), refundAddress: "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", apiKey: "testAPIKEY", + fromNetwork: 'xmr', + toNetwork: '', + rateId: '', ); expect(result.exception, null); @@ -533,58 +475,73 @@ void main() { }); test( - "createStandardExchangeTransaction fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.post( - url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', - encoding: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.createStandardExchangeTransaction( - fromTicker: "xmr", - toTicker: "btc", - receivingAddress: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", - amount: Decimal.parse("0.3"), - refundAddress: - "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - - test("createStandardExchangeTransaction fails for any other reason", - () async { + "createExchangeTransaction fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.post( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', + encoding: null, + ), + ).thenAnswer( + (realInvocation) async => Response(utf8.encode('{"error": 42}'), 200), + ); + + final result = await instance.createExchangeTransaction( + fromCurrency: "xmr", + toCurrency: "btc", + address: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", + fromAmount: Decimal.parse("0.3"), + refundAddress: + "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", + apiKey: "testAPIKEY", + fromNetwork: 'xmr', + toNetwork: '', + rateId: '', + ); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); + + test("createExchangeTransaction fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.post( - url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', - encoding: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); - - final result = await instance.createStandardExchangeTransaction( - fromTicker: "xmr", - toTicker: "btc", - receivingAddress: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", - amount: Decimal.parse("0.3"), + when( + client.post( + url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', + encoding: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + + final result = await instance.createExchangeTransaction( + fromCurrency: "xmr", + toCurrency: "btc", + address: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", + fromAmount: Decimal.parse("0.3"), refundAddress: "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", apiKey: "testAPIKEY", + fromNetwork: 'xmr', + toNetwork: '', + rateId: '', ); expect(result.exception!.type, ExchangeExceptionType.generic); @@ -592,37 +549,46 @@ void main() { }); }); - group("createFixedRateExchangeTransaction", () { - test("createFixedRateExchangeTransaction succeeds", () async { + group("createExchangeTransaction", () { + test("createExchangeTransaction succeeds", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.post( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"btc","to":"eth","address":"0x57f31ad4b64095347F87eDB1675566DAfF5EC886","flow":"fixed-rate","extraId":"","userId":"","contactEmail":"","refundAddress":"","refundExtraId":"","rateId":"","amount":"0.3"}', - encoding: null, - )).thenAnswer((realInvocation) async => Response( + when( + client.post( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"btc","to":"eth","address":"0x57f31ad4b64095347F87eDB1675566DAfF5EC886","flow":"fixed-rate","extraId":"","userId":"","contactEmail":"","refundAddress":"","refundExtraId":"","rateId":"","amount":"0.3"}', + encoding: null, + ), + ).thenAnswer( + (realInvocation) async => Response( utf8.encode( - '{"payinAddress": "33eFX2jfeWbXMSmRe9ewUUTrmSVSxZi5cj", "payoutAddress":' - ' "0x57f31ad4b64095347F87eDB1675566DAfF5EC886","payoutExtraId": "",' - ' "fromCurrency": "btc", "toCurrency": "eth", "refundAddress": "",' - '"refundExtraId": "","validUntil": "2019-09-09T14:01:04.921Z","id":' - ' "a5c73e2603f40d","amount": 62.9737711}'), - 200)); - - final result = await instance.createFixedRateExchangeTransaction( - fromTicker: "btc", - toTicker: "eth", - receivingAddress: "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", - amount: Decimal.parse("0.3"), + '{"payinAddress": "33eFX2jfeWbXMSmRe9ewUUTrmSVSxZi5cj", "payoutAddress":' + ' "0x57f31ad4b64095347F87eDB1675566DAfF5EC886","payoutExtraId": "",' + ' "fromCurrency": "btc", "toCurrency": "eth", "refundAddress": "",' + '"refundExtraId": "","validUntil": "2019-09-09T14:01:04.921Z","id":' + ' "a5c73e2603f40d","amount": 62.9737711}', + ), + 200, + ), + ); + + final result = await instance.createExchangeTransaction( + fromCurrency: "btc", + toCurrency: "eth", + address: "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", + fromAmount: Decimal.parse("0.3"), refundAddress: "", apiKey: "testAPIKEY", rateId: '', - reversed: false, + type: CNExchangeType.direct, + fromNetwork: 'xmr', + toNetwork: '', ); expect(result.exception, null); @@ -631,62 +597,76 @@ void main() { }); test( - "createFixedRateExchangeTransaction fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.post( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"btc","to":"eth","address":"0x57f31ad4b64095347F87eDB1675566DAfF5EC886","amount":"0.3","flow":"fixed-rate","extraId":"","userId":"","contactEmail":"","refundAddress":"","refundExtraId":"","rateId":""}', - encoding: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode('{"id": "a5c73e2603f40d","amount": 62.9737711}'), 200)); - - final result = await instance.createFixedRateExchangeTransaction( - fromTicker: "btc", - toTicker: "eth", - receivingAddress: "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", - amount: Decimal.parse("0.3"), - refundAddress: "", - apiKey: "testAPIKEY", - rateId: '', - reversed: false, - ); - - expect(result.exception!.type, ExchangeExceptionType.generic); - expect(result.value == null, true); - }); - - test("createFixedRateExchangeTransaction fails for any other reason", - () async { + "createExchangeTransaction fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.post( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"btc","to":"eth","address":"0x57f31ad4b64095347F87eDB1675566DAfF5EC886","amount":"0.3","flow":"fixed-rate","extraId":"","userId":"","contactEmail":"","refundAddress":"","refundExtraId":"","rateId":""}', + encoding: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode('{"id": "a5c73e2603f40d","amount": 62.9737711}'), + 200, + ), + ); + + final result = await instance.createExchangeTransaction( + fromCurrency: "btc", + toCurrency: "eth", + address: "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", + fromAmount: Decimal.parse("0.3"), + refundAddress: "", + apiKey: "testAPIKEY", + rateId: '', + type: CNExchangeType.direct, + fromNetwork: 'xmr', + toNetwork: '', + ); + + expect(result.exception!.type, ExchangeExceptionType.generic); + expect(result.value == null, true); + }, + ); + + test("createExchangeTransaction fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.post( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from": "btc","to": "eth","address": "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", "amount": "1.12345","extraId": "", "userId": "","contactEmail": "","refundAddress": "", "refundExtraId": "", "rateId": "" }', - encoding: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); - - final result = await instance.createFixedRateExchangeTransaction( - fromTicker: "xmr", - toTicker: "btc", - receivingAddress: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", - amount: Decimal.parse("0.3"), + when( + client.post( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from": "btc","to": "eth","address": "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", "amount": "1.12345","extraId": "", "userId": "","contactEmail": "","refundAddress": "", "refundExtraId": "", "rateId": "" }', + encoding: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + + final result = await instance.createExchangeTransaction( + fromCurrency: "xmr", + toCurrency: "btc", + address: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", + fromAmount: Decimal.parse("0.3"), refundAddress: "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", apiKey: "testAPIKEY", rateId: '', - reversed: false, + type: CNExchangeType.direct, + fromNetwork: 'xmr', + toNetwork: '', ); expect(result.exception!.type, ExchangeExceptionType.generic); @@ -699,20 +679,27 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( utf8.encode( - '{"status": "waiting", "payinAddress": "32Ge2ci26rj1sRGw2NjiQa9L7Xvxtgzhrj", ' - '"payoutAddress": "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", ' - '"fromCurrency": "btc", "toCurrency": "eth", "id": "50727663e5d9a4", ' - '"updatedAt": "2019-08-22T14:47:49.943Z", "expectedSendAmount": 1, ' - '"expectedReceiveAmount": 52.31667, "createdAt": "2019-08-22T14:47:49.943Z",' - ' "isPartner": false}'), - 200)); + '{"status": "waiting", "payinAddress": "32Ge2ci26rj1sRGw2NjiQa9L7Xvxtgzhrj", ' + '"payoutAddress": "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", ' + '"fromCurrency": "btc", "toCurrency": "eth", "id": "50727663e5d9a4", ' + '"updatedAt": "2019-08-22T14:47:49.943Z", "expectedSendAmount": 1, ' + '"expectedReceiveAmount": 52.31667, "createdAt": "2019-08-22T14:47:49.943Z",' + ' "isPartner": false}', + ), + 200, + ), + ); final result = await instance.getTransactionStatus( id: "47F87eDB1675566DAfF5EC886", @@ -725,39 +712,49 @@ void main() { }); test( - "getTransactionStatus fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getTransactionStatus( - id: "47F87eDB1675566DAfF5EC886", - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); + "getTransactionStatus fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response(utf8.encode('{"error": 42}'), 200), + ); + + final result = await instance.getTransactionStatus( + id: "47F87eDB1675566DAfF5EC886", + apiKey: "testAPIKEY", + ); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); test("getTransactionStatus fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); final result = await instance.getTransactionStatus( id: "47F87eDB1675566DAfF5EC886", @@ -765,67 +762,9 @@ void main() { ); expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - }); - - group("getAvailableFloatingRatePairs", () { - test("getAvailableFloatingRatePairs succeeds", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/available-pairs?includePartners=false"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode('["btc_xmr","btc_firo","btc_doge","eth_ltc"]'), 200)); - - final result = await instance.getAvailableFloatingRatePairs(); - - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value, isA>()); - }); - - test( - "getAvailableFloatingRatePairs fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/available-pairs?includePartners=false"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getAvailableFloatingRatePairs(); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - - test("getAvailableFloatingRatePairs fails for any other reason", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/available-pairs?includePartners=false"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); - - final result = await instance.getAvailableFloatingRatePairs(); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); expect(result.value == null, true); }); }); diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index a36aefef0..35fbc67a1 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, @@ -672,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -742,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index c853a3f37..7c455f73d 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, @@ -672,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -742,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 08f884b57..86090d418 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, @@ -672,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -742,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index cd27b6654..a397b611a 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, @@ -672,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -742,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 1360bd6db..861fa4327 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, @@ -672,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -742,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/node_service_test.dart b/test/services/node_service_test.dart index 2fac42b96..cb0acd6de 100644 --- a/test/services/node_service_test.dart +++ b/test/services/node_service_test.dart @@ -17,7 +17,7 @@ void main() { Hive.registerAdapter(NodeModelAdapter()); } await Hive.openBox(DB.boxNameNodeModels); - await Hive.openBox(DB.boxNamePrimaryNodes); + // await Hive.openBox(DB.boxNamePrimaryNodes); }); group("Empty nodes DB tests", () { @@ -50,6 +50,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ); await service.setPrimaryNodeFor( coin: Bitcoin(CryptoCurrencyNetwork.main), @@ -133,6 +134,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ); final nodeB = NodeModel( host: "host2", @@ -146,6 +148,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ); final nodeC = NodeModel( host: "host3", @@ -159,11 +162,13 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ); setUp(() async { - await NodeService(secureStorageInterface: FakeSecureStorage()) - .updateDefaults(); + await NodeService( + secureStorageInterface: FakeSecureStorage(), + ).updateDefaults(); }); test("setPrimaryNodeFor and getPrimaryNodeFor", () async { @@ -177,7 +182,7 @@ void main() { ); await service.setPrimaryNodeFor( coin: Bitcoin(CryptoCurrencyNetwork.main), - node: Bitcoin(CryptoCurrencyNetwork.main).defaultNode, + node: Bitcoin(CryptoCurrencyNetwork.main).defaultNode(isPrimary: true), ); expect( service @@ -193,17 +198,17 @@ void main() { final service = NodeService(secureStorageInterface: fakeStore); await service.setPrimaryNodeFor( coin: Bitcoin(CryptoCurrencyNetwork.main), - node: Bitcoin(CryptoCurrencyNetwork.main).defaultNode, + node: Bitcoin(CryptoCurrencyNetwork.main).defaultNode(isPrimary: true), ); await service.setPrimaryNodeFor( coin: Monero(CryptoCurrencyNetwork.main), - node: Monero(CryptoCurrencyNetwork.main).defaultNode, + node: Monero(CryptoCurrencyNetwork.main).defaultNode(isPrimary: true), ); expect( service.primaryNodes.toString(), [ - Bitcoin(CryptoCurrencyNetwork.main).defaultNode, - Monero(CryptoCurrencyNetwork.main).defaultNode, + Bitcoin(CryptoCurrencyNetwork.main).defaultNode(isPrimary: true), + Monero(CryptoCurrencyNetwork.main).defaultNode(isPrimary: true), ].toString(), ); expect(fakeStore.interactions, 0); @@ -213,7 +218,8 @@ void main() { final fakeStore = FakeSecureStorage(); final service = NodeService(secureStorageInterface: fakeStore); final nodes = service.nodes; - final defaults = AppConfig.coins.map((e) => e.defaultNode).toList(); + final defaults = + AppConfig.coins.map((e) => e.defaultNode(isPrimary: true)).toList(); nodes.sort((a, b) => a.id.compareTo(b.id)); defaults.sort((a, b) => a.id.compareTo(b.id)); @@ -226,7 +232,7 @@ void main() { test("add a node without a password", () async { final fakeStore = FakeSecureStorage(); final service = NodeService(secureStorageInterface: fakeStore); - await service.add(nodeA, null, true); + await service.save(nodeA, null, true); expect( service.nodes.length, AppConfig.coins.map((e) => e.defaultNode).length + 1, @@ -237,7 +243,7 @@ void main() { test("add a node with a password", () async { final fakeStore = FakeSecureStorage(); final service = NodeService(secureStorageInterface: fakeStore); - await service.add(nodeA, "some password", true); + await service.save(nodeA, "some password", true); expect( service.nodes.length, AppConfig.coins.map((e) => e.defaultNode).length + 1, @@ -276,7 +282,7 @@ void main() { trusted: null, ); - await service.edit(editedNode, "123456", true); + await service.save(editedNode, "123456", true); expect(service.nodes.length, currentLength); diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 218b9c0c5..410094cdf 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -202,6 +202,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -209,6 +210,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i10.Future.value(), @@ -553,6 +555,36 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), @@ -1224,14 +1256,14 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as List<_i23.NodeModel>); @override - _i10.Future add( + _i10.Future save( _i23.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -1278,25 +1310,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i23.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/node_card_test.dart b/test/widget_tests/node_card_test.dart index cc706824c..71a08f851 100644 --- a/test/widget_tests/node_card_test.dart +++ b/test/widget_tests/node_card_test.dart @@ -39,6 +39,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); @@ -55,6 +56,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); @@ -118,6 +120,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); @@ -134,6 +137,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); @@ -198,6 +202,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); @@ -214,6 +219,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); diff --git a/test/widget_tests/node_card_test.mocks.dart b/test/widget_tests/node_card_test.mocks.dart index 441edcdbf..19c8c5166 100644 --- a/test/widget_tests/node_card_test.mocks.dart +++ b/test/widget_tests/node_card_test.mocks.dart @@ -142,14 +142,14 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) as List<_i4.NodeModel>); @override - _i5.Future add( + _i5.Future save( _i4.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/node_options_sheet_test.dart b/test/widget_tests/node_options_sheet_test.dart index 998f6322a..cedc9158b 100644 --- a/test/widget_tests/node_options_sheet_test.dart +++ b/test/widget_tests/node_options_sheet_test.dart @@ -38,6 +38,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, )); when(mockNodeService.getPrimaryNodeFor( @@ -53,7 +54,8 @@ void main() { isFailover: false, torEnabled: true, clearnetEnabled: true, - isDown: false)); + isDown: false, + isPrimary: true)); await tester.pumpWidget( ProviderScope( @@ -116,6 +118,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); @@ -134,6 +137,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); @@ -197,6 +201,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); @@ -215,6 +220,7 @@ void main() { isDown: false, torEnabled: true, clearnetEnabled: true, + isPrimary: true, ), ); diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index cf7098de6..9b7d99828 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -203,6 +203,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { _i10.Future load( _i12.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -210,6 +211,7 @@ class MockWallets extends _i1.Mock implements _i9.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i10.Future.value(), @@ -428,6 +430,36 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), @@ -1028,14 +1060,14 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as List<_i19.NodeModel>); @override - _i10.Future add( + _i10.Future save( _i19.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -1082,25 +1114,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i19.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index 9393a5730..4d7adcad3 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -171,6 +171,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { _i8.Future load( _i11.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -178,6 +179,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i8.Future.value(), diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index a146a9abf..cd8edb2ab 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -3,41 +3,41 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i11; -import 'dart:typed_data' as _i26; -import 'dart:ui' as _i17; +import 'dart:async' as _i10; +import 'dart:typed_data' as _i25; +import 'dart:ui' as _i16; -import 'package:decimal/decimal.dart' as _i23; -import 'package:isar/isar.dart' as _i9; -import 'package:logger/logger.dart' as _i20; +import 'package:decimal/decimal.dart' as _i22; +import 'package:isar/isar.dart' as _i8; +import 'package:logger/logger.dart' as _i19; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i16; +import 'package:mockito/src/dummies.dart' as _i15; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i28; +import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i27; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart' as _i30; -import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i27; -import 'package:stackwallet/models/isar/models/isar_models.dart' as _i29; -import 'package:stackwallet/models/isar/stack_theme.dart' as _i25; -import 'package:stackwallet/networking/http.dart' as _i8; -import 'package:stackwallet/services/locale_service.dart' as _i15; +import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i26; +import 'package:stackwallet/models/isar/models/isar_models.dart' as _i28; +import 'package:stackwallet/models/isar/stack_theme.dart' as _i24; +import 'package:stackwallet/networking/http.dart' as _i7; +import 'package:stackwallet/services/locale_service.dart' as _i14; import 'package:stackwallet/services/node_service.dart' as _i2; -import 'package:stackwallet/services/price_service.dart' as _i22; -import 'package:stackwallet/services/wallets.dart' as _i10; -import 'package:stackwallet/themes/theme_service.dart' as _i24; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i21; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i19; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i18; +import 'package:stackwallet/services/price_service.dart' as _i21; +import 'package:stackwallet/services/wallets.dart' as _i9; +import 'package:stackwallet/themes/theme_service.dart' as _i23; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i20; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i18; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i17; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' - as _i13; -import 'package:stackwallet/utilities/prefs.dart' as _i14; + as _i12; +import 'package:stackwallet/utilities/prefs.dart' as _i13; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' as _i4; -import 'package:stackwallet/wallets/isar/models/wallet_info.dart' as _i12; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart' as _i11; import 'package:stackwallet/wallets/wallet/wallet.dart' as _i5; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' as _i6; -import 'package:tuple/tuple.dart' as _i7; +import 'package:tuple/tuple.dart' as _i29; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -103,9 +103,8 @@ class _FakeDuration_4 extends _i1.SmartFake implements Duration { ); } -class _FakeTuple2_5 extends _i1.SmartFake - implements _i7.Tuple2 { - _FakeTuple2_5( +class _FakeHTTP_5 extends _i1.SmartFake implements _i7.HTTP { + _FakeHTTP_5( Object parent, Invocation parentInvocation, ) : super( @@ -114,8 +113,8 @@ class _FakeTuple2_5 extends _i1.SmartFake ); } -class _FakeHTTP_6 extends _i1.SmartFake implements _i8.HTTP { - _FakeHTTP_6( +class _FakeIsar_6 extends _i1.SmartFake implements _i8.Isar { + _FakeIsar_6( Object parent, Invocation parentInvocation, ) : super( @@ -124,19 +123,9 @@ class _FakeHTTP_6 extends _i1.SmartFake implements _i8.HTTP { ); } -class _FakeIsar_7 extends _i1.SmartFake implements _i9.Isar { - _FakeIsar_7( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeQueryBuilder_8 extends _i1.SmartFake - implements _i9.QueryBuilder { - _FakeQueryBuilder_8( +class _FakeQueryBuilder_7 extends _i1.SmartFake + implements _i8.QueryBuilder { + _FakeQueryBuilder_7( Object parent, Invocation parentInvocation, ) : super( @@ -148,7 +137,7 @@ class _FakeQueryBuilder_8 extends _i1.SmartFake /// A class which mocks [Wallets]. /// /// See the documentation for Mockito's code generation for more information. -class MockWallets extends _i1.Mock implements _i10.Wallets { +class MockWallets extends _i1.Mock implements _i9.Wallets { MockWallets() { _i1.throwOnMissingStub(this); } @@ -221,9 +210,9 @@ class MockWallets extends _i1.Mock implements _i10.Wallets { ); @override - _i11.Future deleteWallet( - _i12.WalletInfo? info, - _i13.SecureStorageInterface? secureStorage, + _i10.Future deleteWallet( + _i11.WalletInfo? info, + _i12.SecureStorageInterface? secureStorage, ) => (super.noSuchMethod( Invocation.method( @@ -233,14 +222,15 @@ class MockWallets extends _i1.Mock implements _i10.Wallets { secureStorage, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future load( - _i14.Prefs? prefs, + _i10.Future load( + _i13.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -248,15 +238,16 @@ class MockWallets extends _i1.Mock implements _i10.Wallets { [ prefs, mainDB, + isDuress, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future loadAfterStackRestore( - _i14.Prefs? prefs, + _i10.Future loadAfterStackRestore( + _i13.Prefs? prefs, List<_i5.Wallet<_i4.CryptoCurrency>>? wallets, bool? isDesktop, ) => @@ -269,15 +260,15 @@ class MockWallets extends _i1.Mock implements _i10.Wallets { isDesktop, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); } /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i15.LocaleService { +class MockLocaleService extends _i1.Mock implements _i14.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } @@ -285,7 +276,7 @@ class MockLocaleService extends _i1.Mock implements _i15.LocaleService { @override String get locale => (super.noSuchMethod( Invocation.getter(#locale), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#locale), ), @@ -298,18 +289,18 @@ class MockLocaleService extends _i1.Mock implements _i15.LocaleService { ) as bool); @override - _i11.Future loadLocale({bool? notify = true}) => (super.noSuchMethod( + _i10.Future loadLocale({bool? notify = true}) => (super.noSuchMethod( Invocation.method( #loadLocale, [], {#notify: notify}, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -318,7 +309,7 @@ class MockLocaleService extends _i1.Mock implements _i15.LocaleService { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -348,7 +339,7 @@ class MockLocaleService extends _i1.Mock implements _i15.LocaleService { /// A class which mocks [Prefs]. /// /// See the documentation for Mockito's code generation for more information. -class MockPrefs extends _i1.Mock implements _i14.Prefs { +class MockPrefs extends _i1.Mock implements _i13.Prefs { MockPrefs() { _i1.throwOnMissingStub(this); } @@ -412,13 +403,13 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - _i18.SyncingType get syncType => (super.noSuchMethod( + _i17.SyncingType get syncType => (super.noSuchMethod( Invocation.getter(#syncType), - returnValue: _i18.SyncingType.currentWalletOnly, - ) as _i18.SyncingType); + returnValue: _i17.SyncingType.currentWalletOnly, + ) as _i17.SyncingType); @override - set syncType(_i18.SyncingType? syncType) => super.noSuchMethod( + set syncType(_i17.SyncingType? syncType) => super.noSuchMethod( Invocation.setter( #syncType, syncType, @@ -459,7 +450,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get language => (super.noSuchMethod( Invocation.getter(#language), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#language), ), @@ -477,7 +468,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get currency => (super.noSuchMethod( Invocation.getter(#currency), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#currency), ), @@ -537,6 +528,36 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { returnValueForMissingStub: null, ); + @override + bool get hasDuressPin => (super.noSuchMethod( + Invocation.getter(#hasDuressPin), + returnValue: false, + ) as bool); + + @override + set hasDuressPin(bool? hasDuressPin) => super.noSuchMethod( + Invocation.setter( + #hasDuressPin, + hasDuressPin, + ), + returnValueForMissingStub: null, + ); + + @override + bool get biometricsDuress => (super.noSuchMethod( + Invocation.getter(#biometricsDuress), + returnValue: false, + ) as bool); + + @override + set biometricsDuress(bool? biometricsDuress) => super.noSuchMethod( + Invocation.setter( + #biometricsDuress, + biometricsDuress, + ), + returnValueForMissingStub: null, + ); + @override int get familiarity => (super.noSuchMethod( Invocation.getter(#familiarity), @@ -607,13 +628,13 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - _i19.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + _i18.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( Invocation.getter(#backupFrequencyType), - returnValue: _i19.BackupFrequencyType.everyTenMinutes, - ) as _i19.BackupFrequencyType); + returnValue: _i18.BackupFrequencyType.everyTenMinutes, + ) as _i18.BackupFrequencyType); @override - set backupFrequencyType(_i19.BackupFrequencyType? backupFrequencyType) => + set backupFrequencyType(_i18.BackupFrequencyType? backupFrequencyType) => super.noSuchMethod( Invocation.setter( #backupFrequencyType, @@ -720,7 +741,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get themeId => (super.noSuchMethod( Invocation.getter(#themeId), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#themeId), ), @@ -738,7 +759,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get systemBrightnessLightThemeId => (super.noSuchMethod( Invocation.getter(#systemBrightnessLightThemeId), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#systemBrightnessLightThemeId), ), @@ -757,7 +778,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get systemBrightnessDarkThemeId => (super.noSuchMethod( Invocation.getter(#systemBrightnessDarkThemeId), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#systemBrightnessDarkThemeId), ), @@ -843,13 +864,13 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - _i20.Level get logLevel => (super.noSuchMethod( + _i19.Level get logLevel => (super.noSuchMethod( Invocation.getter(#logLevel), - returnValue: _i20.Level.all, - ) as _i20.Level); + returnValue: _i19.Level.all, + ) as _i19.Level); @override - set logLevel(_i20.Level? logLevel) => super.noSuchMethod( + set logLevel(_i19.Level? logLevel) => super.noSuchMethod( Invocation.setter( #logLevel, logLevel, @@ -864,67 +885,67 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ) as bool); @override - _i11.Future init() => (super.noSuchMethod( + _i10.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + _i10.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( Invocation.method( #incrementCurrentNotificationIndex, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future isExternalCallsSet() => (super.noSuchMethod( + _i10.Future isExternalCallsSet() => (super.noSuchMethod( Invocation.method( #isExternalCallsSet, [], ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Future saveUserID(String? userId) => (super.noSuchMethod( + _i10.Future saveUserID(String? userId) => (super.noSuchMethod( Invocation.method( #saveUserID, [userId], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + _i10.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( Invocation.method( #saveSignupEpoch, [signupEpoch], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i21.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i20.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i21.AmountUnit.normal, - ) as _i21.AmountUnit); + returnValue: _i20.AmountUnit.normal, + ) as _i20.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i21.AmountUnit? amountUnit, + required _i20.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -997,7 +1018,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1006,7 +1027,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1036,7 +1057,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { /// A class which mocks [PriceService]. /// /// See the documentation for Mockito's code generation for more information. -class MockPriceService extends _i1.Mock implements _i22.PriceService { +class MockPriceService extends _i1.Mock implements _i21.PriceService { MockPriceService() { _i1.throwOnMissingStub(this); } @@ -1044,7 +1065,7 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { @override String get baseTicker => (super.noSuchMethod( Invocation.getter(#baseTicker), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#baseTicker), ), @@ -1069,11 +1090,11 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { ) as Duration); @override - _i11.Future> get tokenContractAddressesToCheck => + _i10.Future> get tokenContractAddressesToCheck => (super.noSuchMethod( Invocation.getter(#tokenContractAddressesToCheck), - returnValue: _i11.Future>.value({}), - ) as _i11.Future>); + returnValue: _i10.Future>.value({}), + ) as _i10.Future>); @override bool get hasListeners => (super.noSuchMethod( @@ -1082,46 +1103,30 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { ) as bool); @override - _i7.Tuple2<_i23.Decimal, double> getPrice(_i4.CryptoCurrency? coin) => - (super.noSuchMethod( - Invocation.method( - #getPrice, - [coin], - ), - returnValue: _FakeTuple2_5<_i23.Decimal, double>( - this, - Invocation.method( - #getPrice, - [coin], - ), - ), - ) as _i7.Tuple2<_i23.Decimal, double>); + ({double change24h, _i22.Decimal value})? getPrice( + _i4.CryptoCurrency? coin) => + (super.noSuchMethod(Invocation.method( + #getPrice, + [coin], + )) as ({double change24h, _i22.Decimal value})?); @override - _i7.Tuple2<_i23.Decimal, double> getTokenPrice(String? contractAddress) => - (super.noSuchMethod( - Invocation.method( - #getTokenPrice, - [contractAddress], - ), - returnValue: _FakeTuple2_5<_i23.Decimal, double>( - this, - Invocation.method( - #getTokenPrice, - [contractAddress], - ), - ), - ) as _i7.Tuple2<_i23.Decimal, double>); + ({double change24h, _i22.Decimal value})? getTokenPrice( + String? contractAddress) => + (super.noSuchMethod(Invocation.method( + #getTokenPrice, + [contractAddress], + )) as ({double change24h, _i22.Decimal value})?); @override - _i11.Future updatePrice() => (super.noSuchMethod( + _i10.Future updatePrice() => (super.noSuchMethod( Invocation.method( #updatePrice, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override void cancel() => super.noSuchMethod( @@ -1151,7 +1156,7 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { ); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1160,7 +1165,7 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1181,22 +1186,22 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { /// A class which mocks [ThemeService]. /// /// See the documentation for Mockito's code generation for more information. -class MockThemeService extends _i1.Mock implements _i24.ThemeService { +class MockThemeService extends _i1.Mock implements _i23.ThemeService { MockThemeService() { _i1.throwOnMissingStub(this); } @override - _i8.HTTP get client => (super.noSuchMethod( + _i7.HTTP get client => (super.noSuchMethod( Invocation.getter(#client), - returnValue: _FakeHTTP_6( + returnValue: _FakeHTTP_5( this, Invocation.getter(#client), ), - ) as _i8.HTTP); + ) as _i7.HTTP); @override - set client(_i8.HTTP? _client) => super.noSuchMethod( + set client(_i7.HTTP? _client) => super.noSuchMethod( Invocation.setter( #client, _client, @@ -1214,10 +1219,10 @@ class MockThemeService extends _i1.Mock implements _i24.ThemeService { ) as _i3.MainDB); @override - List<_i25.StackTheme> get installedThemes => (super.noSuchMethod( + List<_i24.StackTheme> get installedThemes => (super.noSuchMethod( Invocation.getter(#installedThemes), - returnValue: <_i25.StackTheme>[], - ) as List<_i25.StackTheme>); + returnValue: <_i24.StackTheme>[], + ) as List<_i24.StackTheme>); @override void init(_i3.MainDB? db) => super.noSuchMethod( @@ -1229,79 +1234,79 @@ class MockThemeService extends _i1.Mock implements _i24.ThemeService { ); @override - _i11.Future install({required _i26.Uint8List? themeArchiveData}) => + _i10.Future install({required _i25.Uint8List? themeArchiveData}) => (super.noSuchMethod( Invocation.method( #install, [], {#themeArchiveData: themeArchiveData}, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future remove({required String? themeId}) => (super.noSuchMethod( + _i10.Future remove({required String? themeId}) => (super.noSuchMethod( Invocation.method( #remove, [], {#themeId: themeId}, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future checkDefaultThemesOnStartup() => (super.noSuchMethod( + _i10.Future checkDefaultThemesOnStartup() => (super.noSuchMethod( Invocation.method( #checkDefaultThemesOnStartup, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future verifyInstalled({required String? themeId}) => + _i10.Future verifyInstalled({required String? themeId}) => (super.noSuchMethod( Invocation.method( #verifyInstalled, [], {#themeId: themeId}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Future> fetchThemes() => + _i10.Future> fetchThemes() => (super.noSuchMethod( Invocation.method( #fetchThemes, [], ), - returnValue: _i11.Future>.value( - <_i24.StackThemeMetaData>[]), - ) as _i11.Future>); + returnValue: _i10.Future>.value( + <_i23.StackThemeMetaData>[]), + ) as _i10.Future>); @override - _i11.Future<_i26.Uint8List> fetchTheme( - {required _i24.StackThemeMetaData? themeMetaData}) => + _i10.Future<_i25.Uint8List> fetchTheme( + {required _i23.StackThemeMetaData? themeMetaData}) => (super.noSuchMethod( Invocation.method( #fetchTheme, [], {#themeMetaData: themeMetaData}, ), - returnValue: _i11.Future<_i26.Uint8List>.value(_i26.Uint8List(0)), - ) as _i11.Future<_i26.Uint8List>); + returnValue: _i10.Future<_i25.Uint8List>.value(_i25.Uint8List(0)), + ) as _i10.Future<_i25.Uint8List>); @override - _i25.StackTheme? getTheme({required String? themeId}) => + _i24.StackTheme? getTheme({required String? themeId}) => (super.noSuchMethod(Invocation.method( #getTheme, [], {#themeId: themeId}, - )) as _i25.StackTheme?); + )) as _i24.StackTheme?); } /// A class which mocks [MainDB]. @@ -1313,166 +1318,166 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { } @override - _i9.Isar get isar => (super.noSuchMethod( + _i8.Isar get isar => (super.noSuchMethod( Invocation.getter(#isar), - returnValue: _FakeIsar_7( + returnValue: _FakeIsar_6( this, Invocation.getter(#isar), ), - ) as _i9.Isar); + ) as _i8.Isar); @override - _i11.Future initMainDB({_i9.Isar? mock}) => (super.noSuchMethod( + _i10.Future initMainDB({_i8.Isar? mock}) => (super.noSuchMethod( Invocation.method( #initMainDB, [], {#mock: mock}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Future putWalletInfo(_i12.WalletInfo? walletInfo) => + _i10.Future putWalletInfo(_i11.WalletInfo? walletInfo) => (super.noSuchMethod( Invocation.method( #putWalletInfo, [walletInfo], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future updateWalletInfo(_i12.WalletInfo? walletInfo) => + _i10.Future updateWalletInfo(_i11.WalletInfo? walletInfo) => (super.noSuchMethod( Invocation.method( #updateWalletInfo, [walletInfo], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - List<_i27.ContactEntry> getContactEntries() => (super.noSuchMethod( + List<_i26.ContactEntry> getContactEntries() => (super.noSuchMethod( Invocation.method( #getContactEntries, [], ), - returnValue: <_i27.ContactEntry>[], - ) as List<_i27.ContactEntry>); + returnValue: <_i26.ContactEntry>[], + ) as List<_i26.ContactEntry>); @override - _i11.Future deleteContactEntry({required String? id}) => + _i10.Future deleteContactEntry({required String? id}) => (super.noSuchMethod( Invocation.method( #deleteContactEntry, [], {#id: id}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Future isContactEntryExists({required String? id}) => + _i10.Future isContactEntryExists({required String? id}) => (super.noSuchMethod( Invocation.method( #isContactEntryExists, [], {#id: id}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i27.ContactEntry? getContactEntry({required String? id}) => + _i26.ContactEntry? getContactEntry({required String? id}) => (super.noSuchMethod(Invocation.method( #getContactEntry, [], {#id: id}, - )) as _i27.ContactEntry?); + )) as _i26.ContactEntry?); @override - _i11.Future putContactEntry( - {required _i27.ContactEntry? contactEntry}) => + _i10.Future putContactEntry( + {required _i26.ContactEntry? contactEntry}) => (super.noSuchMethod( Invocation.method( #putContactEntry, [], {#contactEntry: contactEntry}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i28.TransactionBlockExplorer? getTransactionBlockExplorer( + _i27.TransactionBlockExplorer? getTransactionBlockExplorer( {required _i4.CryptoCurrency? cryptoCurrency}) => (super.noSuchMethod(Invocation.method( #getTransactionBlockExplorer, [], {#cryptoCurrency: cryptoCurrency}, - )) as _i28.TransactionBlockExplorer?); + )) as _i27.TransactionBlockExplorer?); @override - _i11.Future putTransactionBlockExplorer( - _i28.TransactionBlockExplorer? explorer) => + _i10.Future putTransactionBlockExplorer( + _i27.TransactionBlockExplorer? explorer) => (super.noSuchMethod( Invocation.method( #putTransactionBlockExplorer, [explorer], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i9.QueryBuilder<_i29.Address, _i29.Address, _i9.QAfterWhereClause> + _i8.QueryBuilder<_i28.Address, _i28.Address, _i8.QAfterWhereClause> getAddresses(String? walletId) => (super.noSuchMethod( Invocation.method( #getAddresses, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i29.Address, _i29.Address, - _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_7<_i28.Address, _i28.Address, + _i8.QAfterWhereClause>( this, Invocation.method( #getAddresses, [walletId], ), ), - ) as _i9 - .QueryBuilder<_i29.Address, _i29.Address, _i9.QAfterWhereClause>); + ) as _i8 + .QueryBuilder<_i28.Address, _i28.Address, _i8.QAfterWhereClause>); @override - _i11.Future putAddress(_i29.Address? address) => (super.noSuchMethod( + _i10.Future putAddress(_i28.Address? address) => (super.noSuchMethod( Invocation.method( #putAddress, [address], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i11.Future> putAddresses(List<_i29.Address>? addresses) => + _i10.Future> putAddresses(List<_i28.Address>? addresses) => (super.noSuchMethod( Invocation.method( #putAddresses, [addresses], ), - returnValue: _i11.Future>.value([]), - ) as _i11.Future>); + returnValue: _i10.Future>.value([]), + ) as _i10.Future>); @override - _i11.Future> updateOrPutAddresses(List<_i29.Address>? addresses) => + _i10.Future> updateOrPutAddresses(List<_i28.Address>? addresses) => (super.noSuchMethod( Invocation.method( #updateOrPutAddresses, [addresses], ), - returnValue: _i11.Future>.value([]), - ) as _i11.Future>); + returnValue: _i10.Future>.value([]), + ) as _i10.Future>); @override - _i11.Future<_i29.Address?> getAddress( + _i10.Future<_i28.Address?> getAddress( String? walletId, String? address, ) => @@ -1484,13 +1489,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { address, ], ), - returnValue: _i11.Future<_i29.Address?>.value(), - ) as _i11.Future<_i29.Address?>); + returnValue: _i10.Future<_i28.Address?>.value(), + ) as _i10.Future<_i28.Address?>); @override - _i11.Future updateAddress( - _i29.Address? oldAddress, - _i29.Address? newAddress, + _i10.Future updateAddress( + _i28.Address? oldAddress, + _i28.Address? newAddress, ) => (super.noSuchMethod( Invocation.method( @@ -1500,50 +1505,50 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { newAddress, ], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i9.QueryBuilder<_i29.Transaction, _i29.Transaction, _i9.QAfterWhereClause> + _i8.QueryBuilder<_i28.Transaction, _i28.Transaction, _i8.QAfterWhereClause> getTransactions(String? walletId) => (super.noSuchMethod( Invocation.method( #getTransactions, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i29.Transaction, _i29.Transaction, - _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_7<_i28.Transaction, _i28.Transaction, + _i8.QAfterWhereClause>( this, Invocation.method( #getTransactions, [walletId], ), ), - ) as _i9.QueryBuilder<_i29.Transaction, _i29.Transaction, - _i9.QAfterWhereClause>); + ) as _i8.QueryBuilder<_i28.Transaction, _i28.Transaction, + _i8.QAfterWhereClause>); @override - _i11.Future putTransaction(_i29.Transaction? transaction) => + _i10.Future putTransaction(_i28.Transaction? transaction) => (super.noSuchMethod( Invocation.method( #putTransaction, [transaction], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i11.Future> putTransactions( - List<_i29.Transaction>? transactions) => + _i10.Future> putTransactions( + List<_i28.Transaction>? transactions) => (super.noSuchMethod( Invocation.method( #putTransactions, [transactions], ), - returnValue: _i11.Future>.value([]), - ) as _i11.Future>); + returnValue: _i10.Future>.value([]), + ) as _i10.Future>); @override - _i11.Future<_i29.Transaction?> getTransaction( + _i10.Future<_i28.Transaction?> getTransaction( String? walletId, String? txid, ) => @@ -1555,11 +1560,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { txid, ], ), - returnValue: _i11.Future<_i29.Transaction?>.value(), - ) as _i11.Future<_i29.Transaction?>); + returnValue: _i10.Future<_i28.Transaction?>.value(), + ) as _i10.Future<_i28.Transaction?>); @override - _i11.Stream<_i29.Transaction?> watchTransaction({ + _i10.Stream<_i28.Transaction?> watchTransaction({ required int? id, bool? fireImmediately = false, }) => @@ -1572,11 +1577,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i29.Transaction?>.empty(), - ) as _i11.Stream<_i29.Transaction?>); + returnValue: _i10.Stream<_i28.Transaction?>.empty(), + ) as _i10.Stream<_i28.Transaction?>); @override - _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause> getUTXOs( + _i8.QueryBuilder<_i28.UTXO, _i28.UTXO, _i8.QAfterWhereClause> getUTXOs( String? walletId) => (super.noSuchMethod( Invocation.method( @@ -1584,17 +1589,17 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { [walletId], ), returnValue: - _FakeQueryBuilder_8<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause>( + _FakeQueryBuilder_7<_i28.UTXO, _i28.UTXO, _i8.QAfterWhereClause>( this, Invocation.method( #getUTXOs, [walletId], ), ), - ) as _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause>); + ) as _i8.QueryBuilder<_i28.UTXO, _i28.UTXO, _i8.QAfterWhereClause>); @override - _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition> + _i8.QueryBuilder<_i28.UTXO, _i28.UTXO, _i8.QAfterFilterCondition> getUTXOsByAddress( String? walletId, String? address, @@ -1607,8 +1612,8 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { address, ], ), - returnValue: _FakeQueryBuilder_8<_i29.UTXO, _i29.UTXO, - _i9.QAfterFilterCondition>( + returnValue: _FakeQueryBuilder_7<_i28.UTXO, _i28.UTXO, + _i8.QAfterFilterCondition>( this, Invocation.method( #getUTXOsByAddress, @@ -1618,33 +1623,33 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ], ), ), - ) as _i9 - .QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition>); + ) as _i8 + .QueryBuilder<_i28.UTXO, _i28.UTXO, _i8.QAfterFilterCondition>); @override - _i11.Future putUTXO(_i29.UTXO? utxo) => (super.noSuchMethod( + _i10.Future putUTXO(_i28.UTXO? utxo) => (super.noSuchMethod( Invocation.method( #putUTXO, [utxo], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future putUTXOs(List<_i29.UTXO>? utxos) => (super.noSuchMethod( + _i10.Future putUTXOs(List<_i28.UTXO>? utxos) => (super.noSuchMethod( Invocation.method( #putUTXOs, [utxos], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future updateUTXOs( + _i10.Future updateUTXOs( String? walletId, - List<_i29.UTXO>? utxos, + List<_i28.UTXO>? utxos, ) => (super.noSuchMethod( Invocation.method( @@ -1654,11 +1659,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { utxos, ], ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Stream<_i29.UTXO?> watchUTXO({ + _i10.Stream<_i28.UTXO?> watchUTXO({ required int? id, bool? fireImmediately = false, }) => @@ -1671,54 +1676,54 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i29.UTXO?>.empty(), - ) as _i11.Stream<_i29.UTXO?>); + returnValue: _i10.Stream<_i28.UTXO?>.empty(), + ) as _i10.Stream<_i28.UTXO?>); @override - _i9.QueryBuilder<_i29.TransactionNote, _i29.TransactionNote, - _i9.QAfterWhereClause> getTransactionNotes( + _i8.QueryBuilder<_i28.TransactionNote, _i28.TransactionNote, + _i8.QAfterWhereClause> getTransactionNotes( String? walletId) => (super.noSuchMethod( Invocation.method( #getTransactionNotes, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i29.TransactionNote, - _i29.TransactionNote, _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_7<_i28.TransactionNote, + _i28.TransactionNote, _i8.QAfterWhereClause>( this, Invocation.method( #getTransactionNotes, [walletId], ), ), - ) as _i9.QueryBuilder<_i29.TransactionNote, _i29.TransactionNote, - _i9.QAfterWhereClause>); + ) as _i8.QueryBuilder<_i28.TransactionNote, _i28.TransactionNote, + _i8.QAfterWhereClause>); @override - _i11.Future putTransactionNote(_i29.TransactionNote? transactionNote) => + _i10.Future putTransactionNote(_i28.TransactionNote? transactionNote) => (super.noSuchMethod( Invocation.method( #putTransactionNote, [transactionNote], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future putTransactionNotes( - List<_i29.TransactionNote>? transactionNotes) => + _i10.Future putTransactionNotes( + List<_i28.TransactionNote>? transactionNotes) => (super.noSuchMethod( Invocation.method( #putTransactionNotes, [transactionNotes], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future<_i29.TransactionNote?> getTransactionNote( + _i10.Future<_i28.TransactionNote?> getTransactionNote( String? walletId, String? txid, ) => @@ -1730,11 +1735,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { txid, ], ), - returnValue: _i11.Future<_i29.TransactionNote?>.value(), - ) as _i11.Future<_i29.TransactionNote?>); + returnValue: _i10.Future<_i28.TransactionNote?>.value(), + ) as _i10.Future<_i28.TransactionNote?>); @override - _i11.Stream<_i29.TransactionNote?> watchTransactionNote({ + _i10.Stream<_i28.TransactionNote?> watchTransactionNote({ required int? id, bool? fireImmediately = false, }) => @@ -1747,39 +1752,39 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i29.TransactionNote?>.empty(), - ) as _i11.Stream<_i29.TransactionNote?>); + returnValue: _i10.Stream<_i28.TransactionNote?>.empty(), + ) as _i10.Stream<_i28.TransactionNote?>); @override - _i9.QueryBuilder<_i29.AddressLabel, _i29.AddressLabel, _i9.QAfterWhereClause> + _i8.QueryBuilder<_i28.AddressLabel, _i28.AddressLabel, _i8.QAfterWhereClause> getAddressLabels(String? walletId) => (super.noSuchMethod( Invocation.method( #getAddressLabels, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i29.AddressLabel, - _i29.AddressLabel, _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_7<_i28.AddressLabel, + _i28.AddressLabel, _i8.QAfterWhereClause>( this, Invocation.method( #getAddressLabels, [walletId], ), ), - ) as _i9.QueryBuilder<_i29.AddressLabel, _i29.AddressLabel, - _i9.QAfterWhereClause>); + ) as _i8.QueryBuilder<_i28.AddressLabel, _i28.AddressLabel, + _i8.QAfterWhereClause>); @override - _i11.Future putAddressLabel(_i29.AddressLabel? addressLabel) => + _i10.Future putAddressLabel(_i28.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #putAddressLabel, [addressLabel], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - int putAddressLabelSync(_i29.AddressLabel? addressLabel) => + int putAddressLabelSync(_i28.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #putAddressLabelSync, @@ -1789,18 +1794,18 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as int); @override - _i11.Future putAddressLabels(List<_i29.AddressLabel>? addressLabels) => + _i10.Future putAddressLabels(List<_i28.AddressLabel>? addressLabels) => (super.noSuchMethod( Invocation.method( #putAddressLabels, [addressLabels], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future<_i29.AddressLabel?> getAddressLabel( + _i10.Future<_i28.AddressLabel?> getAddressLabel( String? walletId, String? addressString, ) => @@ -1812,11 +1817,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { addressString, ], ), - returnValue: _i11.Future<_i29.AddressLabel?>.value(), - ) as _i11.Future<_i29.AddressLabel?>); + returnValue: _i10.Future<_i28.AddressLabel?>.value(), + ) as _i10.Future<_i28.AddressLabel?>); @override - _i29.AddressLabel? getAddressLabelSync( + _i28.AddressLabel? getAddressLabelSync( String? walletId, String? addressString, ) => @@ -1826,10 +1831,10 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { walletId, addressString, ], - )) as _i29.AddressLabel?); + )) as _i28.AddressLabel?); @override - _i11.Stream<_i29.AddressLabel?> watchAddressLabel({ + _i10.Stream<_i28.AddressLabel?> watchAddressLabel({ required int? id, bool? fireImmediately = false, }) => @@ -1842,55 +1847,55 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i29.AddressLabel?>.empty(), - ) as _i11.Stream<_i29.AddressLabel?>); + returnValue: _i10.Stream<_i28.AddressLabel?>.empty(), + ) as _i10.Stream<_i28.AddressLabel?>); @override - _i11.Future updateAddressLabel(_i29.AddressLabel? addressLabel) => + _i10.Future updateAddressLabel(_i28.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #updateAddressLabel, [addressLabel], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i11.Future deleteWalletBlockchainData(String? walletId) => + _i10.Future deleteWalletBlockchainData(String? walletId) => (super.noSuchMethod( Invocation.method( #deleteWalletBlockchainData, [walletId], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future deleteAddressLabels(String? walletId) => + _i10.Future deleteAddressLabels(String? walletId) => (super.noSuchMethod( Invocation.method( #deleteAddressLabels, [walletId], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future deleteTransactionNotes(String? walletId) => + _i10.Future deleteTransactionNotes(String? walletId) => (super.noSuchMethod( Invocation.method( #deleteTransactionNotes, [walletId], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future addNewTransactionData( - List<_i7.Tuple2<_i29.Transaction, _i29.Address?>>? transactionsData, + _i10.Future addNewTransactionData( + List<_i29.Tuple2<_i28.Transaction, _i28.Address?>>? transactionsData, String? walletId, ) => (super.noSuchMethod( @@ -1901,93 +1906,82 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { walletId, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future> updateOrPutTransactionV2s( + _i10.Future> updateOrPutTransactionV2s( List<_i30.TransactionV2>? transactions) => (super.noSuchMethod( Invocation.method( #updateOrPutTransactionV2s, [transactions], ), - returnValue: _i11.Future>.value([]), - ) as _i11.Future>); + returnValue: _i10.Future>.value([]), + ) as _i10.Future>); @override - _i9.QueryBuilder<_i29.EthContract, _i29.EthContract, _i9.QWhere> + _i8.QueryBuilder<_i28.EthContract, _i28.EthContract, _i8.QWhere> getEthContracts() => (super.noSuchMethod( Invocation.method( #getEthContracts, [], ), - returnValue: _FakeQueryBuilder_8<_i29.EthContract, _i29.EthContract, - _i9.QWhere>( + returnValue: _FakeQueryBuilder_7<_i28.EthContract, _i28.EthContract, + _i8.QWhere>( this, Invocation.method( #getEthContracts, [], ), ), - ) as _i9 - .QueryBuilder<_i29.EthContract, _i29.EthContract, _i9.QWhere>); + ) as _i8 + .QueryBuilder<_i28.EthContract, _i28.EthContract, _i8.QWhere>); @override - _i11.Future<_i29.EthContract?> getEthContract(String? contractAddress) => + _i10.Future<_i28.EthContract?> getEthContract(String? contractAddress) => (super.noSuchMethod( Invocation.method( #getEthContract, [contractAddress], ), - returnValue: _i11.Future<_i29.EthContract?>.value(), - ) as _i11.Future<_i29.EthContract?>); + returnValue: _i10.Future<_i28.EthContract?>.value(), + ) as _i10.Future<_i28.EthContract?>); @override - _i29.EthContract? getEthContractSync(String? contractAddress) => + _i28.EthContract? getEthContractSync(String? contractAddress) => (super.noSuchMethod(Invocation.method( #getEthContractSync, [contractAddress], - )) as _i29.EthContract?); + )) as _i28.EthContract?); @override - _i11.Future putEthContract(_i29.EthContract? contract) => + _i10.Future putEthContract(_i28.EthContract? contract) => (super.noSuchMethod( Invocation.method( #putEthContract, [contract], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i11.Future putEthContracts(List<_i29.EthContract>? contracts) => + _i10.Future putEthContracts(List<_i28.EthContract>? contracts) => (super.noSuchMethod( Invocation.method( #putEthContracts, [contracts], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); - - @override - _i11.Future getHighestUsedMintIndex({required String? walletId}) => - (super.noSuchMethod( - Invocation.method( - #getHighestUsedMintIndex, - [], - {#walletId: walletId}, - ), - returnValue: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); } /// A class which mocks [IThemeAssets]. /// /// See the documentation for Mockito's code generation for more information. -class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { +class MockIThemeAssets extends _i1.Mock implements _i24.IThemeAssets { MockIThemeAssets() { _i1.throwOnMissingStub(this); } @@ -1995,7 +1989,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get bellNew => (super.noSuchMethod( Invocation.getter(#bellNew), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#bellNew), ), @@ -2004,7 +1998,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get buy => (super.noSuchMethod( Invocation.getter(#buy), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#buy), ), @@ -2013,7 +2007,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get exchange => (super.noSuchMethod( Invocation.getter(#exchange), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#exchange), ), @@ -2022,7 +2016,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get personaIncognito => (super.noSuchMethod( Invocation.getter(#personaIncognito), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#personaIncognito), ), @@ -2031,7 +2025,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get personaEasy => (super.noSuchMethod( Invocation.getter(#personaEasy), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#personaEasy), ), @@ -2040,7 +2034,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get stack => (super.noSuchMethod( Invocation.getter(#stack), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#stack), ), @@ -2049,7 +2043,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get stackIcon => (super.noSuchMethod( Invocation.getter(#stackIcon), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#stackIcon), ), @@ -2058,7 +2052,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get receive => (super.noSuchMethod( Invocation.getter(#receive), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#receive), ), @@ -2067,7 +2061,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get receivePending => (super.noSuchMethod( Invocation.getter(#receivePending), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#receivePending), ), @@ -2076,7 +2070,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get receiveCancelled => (super.noSuchMethod( Invocation.getter(#receiveCancelled), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#receiveCancelled), ), @@ -2085,7 +2079,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get send => (super.noSuchMethod( Invocation.getter(#send), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#send), ), @@ -2094,7 +2088,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get sendPending => (super.noSuchMethod( Invocation.getter(#sendPending), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#sendPending), ), @@ -2103,7 +2097,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get sendCancelled => (super.noSuchMethod( Invocation.getter(#sendCancelled), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#sendCancelled), ), @@ -2112,7 +2106,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get themeSelector => (super.noSuchMethod( Invocation.getter(#themeSelector), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#themeSelector), ), @@ -2121,7 +2115,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get themePreview => (super.noSuchMethod( Invocation.getter(#themePreview), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#themePreview), ), @@ -2130,7 +2124,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get txExchange => (super.noSuchMethod( Invocation.getter(#txExchange), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#txExchange), ), @@ -2139,7 +2133,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get txExchangePending => (super.noSuchMethod( Invocation.getter(#txExchangePending), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#txExchangePending), ), @@ -2148,7 +2142,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get txExchangeFailed => (super.noSuchMethod( Invocation.getter(#txExchangeFailed), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#txExchangeFailed), ), diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index 80c03af35..1ba8ca158 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -174,6 +174,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { _i8.Future load( _i11.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -181,6 +182,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i8.Future.value(), diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index d7fea45d4..d406a0c35 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -170,6 +170,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { _i8.Future load( _i10.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -177,6 +178,7 @@ class MockWallets extends _i1.Mock implements _i7.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i8.Future.value(), @@ -307,14 +309,14 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as List<_i11.NodeModel>); @override - _i8.Future add( + _i8.Future save( _i11.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -361,25 +363,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); - @override - _i8.Future edit( - _i11.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); - @override _i8.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 88f854589..6018c1f93 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -184,6 +184,7 @@ class MockWallets extends _i1.Mock implements _i8.Wallets { _i9.Future load( _i11.Prefs? prefs, _i3.MainDB? mainDB, + bool? isDuress, ) => (super.noSuchMethod( Invocation.method( @@ -191,6 +192,7 @@ class MockWallets extends _i1.Mock implements _i8.Wallets { [ prefs, mainDB, + isDuress, ], ), returnValue: _i9.Future.value(), @@ -447,14 +449,14 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as List<_i15.NodeModel>); @override - _i9.Future add( + _i9.Future save( _i15.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -501,25 +503,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); - @override - _i9.Future edit( - _i15.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - @override _i9.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 74ef33c4e..63c954ce1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); CsMoneroFlutterLibsWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("CsMoneroFlutterLibsWindowsPluginCApi")); + CsSalviumFlutterLibsWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CsSalviumFlutterLibsWindowsPluginCApi")); DesktopDropPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopDropPlugin")); FlutterLibepiccashPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 55c2cc622..ac0ec291d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST camera_windows connectivity_plus cs_monero_flutter_libs_windows + cs_salvium_flutter_libs_windows desktop_drop flutter_libepiccash flutter_secure_storage_windows @@ -22,6 +23,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST coinlib_flutter flutter_libsparkmobile + flutter_mwebd frostdart tor_ffi_plugin xelis_flutter