From 1fdc848b3a6b3e8dd9c85728fa445968ef28060c Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 9 May 2024 12:03:30 +0100 Subject: [PATCH 01/25] fix: Modify tron fee to eliminate spike due to first tx (#1439) --- cw_tron/lib/tron_client.dart | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index 73812f14c..25edb0f9a 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -130,11 +130,6 @@ class TronClient { final energyInSun = chainParams.getEnergyFee!; log('Energy In Sun: $energyInSun'); - log( - 'Create Account Fee In System Contract for Chain: ${chainParams.getCreateNewAccountFeeInSystemContract!}', - ); - log('Create Account Fee for Chain: ${chainParams.getCreateAccountFee}'); - final fakeTransaction = Transaction( rawData: rawTransaction, signature: [Uint8List(65)], @@ -185,17 +180,6 @@ class TronClient { totalBurn += chainParams.getMemoFee!; } - // Check if receiver's account is active - final receiverAccountInfo = - await _provider!.request(TronRequestGetAccount(address: receiverAddress)); - - /// Calculate the resources required to create a new account. - if (receiverAccountInfo == null) { - totalBurn += chainParams.getCreateNewAccountFeeInSystemContract!; - - totalBurn += (chainParams.getCreateAccountFee! * bandWidthInSun); - } - log('Final total burn: $totalBurn'); return totalBurn; From 8325181253a877b2493056a4022d7d66d539f489 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 9 May 2024 16:00:26 +0300 Subject: [PATCH 02/25] Revert flutter secure storage update (#1440) * Revert flutter secure storage update * Fix app start slowness --- lib/di.dart | 28 ---------------------------- pubspec_base.yaml | 1 + tool/configure.dart | 31 +++++++------------------------ 3 files changed, 8 insertions(+), 52 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 3ee57cb53..92bf25c9a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -124,7 +124,6 @@ import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; -import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; @@ -223,7 +222,6 @@ import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -231,37 +229,11 @@ import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/core/secure_storage.dart'; -import 'package:cake_wallet/core/wallet_creation_service.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/wallet_new_vm.dart'; -import 'package:cake_wallet/store/authentication_store.dart'; -import 'package:cake_wallet/store/dashboard/trades_store.dart'; -import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; -import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -import 'package:cake_wallet/store/templates/send_template_store.dart'; -import 'package:cake_wallet/store/templates/exchange_template_store.dart'; -import 'package:cake_wallet/entities/template.dart'; -import 'package:cake_wallet/exchange/exchange_template.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/src/screens/dashboard/pages/address_page.dart'; -import 'package:cake_wallet/anypay/anypay_api.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; -import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; -import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; -import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; -import 'package:cake_wallet/core/wallet_loading_service.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/entities/qr_view_data.dart'; import 'buy/dfx/dfx_buy_provider.dart'; import 'core/totp_request_details.dart'; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index a501b9af9..a2be113e4 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -138,6 +138,7 @@ dependency_overrides: git: url: https://github.com/cake-tech/web3dart.git ref: cake + flutter_secure_storage_platform_interface: 1.0.2 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/tool/configure.dart b/tool/configure.dart index 959809b07..126851997 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1175,8 +1175,8 @@ Future generatePubspec( git: url: https://github.com/cake-tech/flutter_secure_storage.git path: flutter_secure_storage - ref: cake-9.0.0 - version: 9.0.0 + ref: cake-8.0.0 + version: 8.0.0 """; const cwEthereum = """ cw_ethereum: @@ -1392,29 +1392,12 @@ class DefaultSecureStorage extends SecureStorage { @override Future readNoIOptions({required String key}) async => await _readInternal(key, true); - + Future _readInternal(String key, bool useNoIOptions) async { - String? result; - - const maxWait = Duration(seconds: 3); - const checkInterval = Duration(milliseconds: 200); - - DateTime start = DateTime.now(); - - while (result == null && DateTime.now().difference(start) < maxWait) { - result = await _secureStorage.read( - key: key, - iOptions: useNoIOptions ? IOSOptions() : null, - ); - - if (result != null) { - break; - } - - await Future.delayed(checkInterval); - } - - return result; + return await _secureStorage.read( + key: key, + iOptions: useNoIOptions ? IOSOptions() : null, + ); } }"""; const fakeSecureStorage = """ From cfe4401ccbbfeb34e61c58aefb82c71af2a2c009 Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 9 May 2024 15:10:49 +0100 Subject: [PATCH 03/25] Desktop-refresh-error-fix (#1442) * fix: Desktop refresh issue * chore: Removing comments --- lib/src/screens/dashboard/dashboard_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 52a4d8f61..8d9dc9829 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -53,7 +53,7 @@ class DashboardPage extends StatelessWidget { Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; return Scaffold( - body: Observer( + body: Builder( builder: (_) { final dashboardPageView = RefreshIndicator( displacement: screenHeight * 0.1, From 678dc3b13570f9b5a07b70f839f006d56252b021 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 10 May 2024 21:28:49 +0300 Subject: [PATCH 04/25] update build numbers (#1441) * update build numbers * fix UI issue * Add Nano to secret generation properly [skip ci] * remove to utc from expiration time --- cw_tron/lib/tron_client.dart | 4 ++-- lib/src/screens/dashboard/pages/balance_page.dart | 6 +++--- .../xcshareddata/xcschemes/Runner.xcscheme | 8 ++++---- scripts/android/app_env.sh | 4 ++-- scripts/ios/app_env.sh | 4 ++-- scripts/macos/app_env.sh | 4 ++-- tool/import_secrets_config.dart | 14 ++++++++++++++ 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index 25edb0f9a..e25887002 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -208,7 +208,7 @@ class TronClient { TransactionContract(type: contract.contractType, parameter: parameter); // Set the transaction expiration time (maximum 24 hours) - final expireTime = DateTime.now().toUtc().add(const Duration(hours: 24)); + final expireTime = DateTime.now().add(const Duration(hours: 24)); // Create a raw transaction TransactionRaw rawTransaction = TransactionRaw( @@ -369,7 +369,7 @@ class TronClient { TransactionContract(type: contract.contractType, parameter: parameter); // Set the transaction expiration time (maximum 24 hours) - final expireTime = DateTime.now().toUtc().add(const Duration(hours: 24)); + final expireTime = DateTime.now().add(const Duration(hours: 24)); // Create a raw transaction TransactionRaw rawTransaction = TransactionRaw( diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 83a57060d..7f9256c51 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -69,14 +69,14 @@ class BalancePage extends StatelessWidget { ), labelColor: Theme.of(context).extension()!.pageTitleTextColor, - dividerColor: - Theme.of(context).extension()!.pageTitleTextColor, + dividerColor: Colors.transparent, indicatorColor: Theme.of(context).extension()!.pageTitleTextColor, unselectedLabelColor: Theme.of(context) .extension()! .pageTitleTextColor .withOpacity(0.5), + tabAlignment: TabAlignment.start, tabs: [ Tab(text: 'My Crypto'), Tab(text: 'My NFTs'), @@ -351,7 +351,7 @@ class BalanceRowWidget extends StatelessWidget { style: TextStyle( fontSize: 16, fontFamily: 'Lato', - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w500, color: Theme.of(context).extension()!.textColor, height: 1)), ], diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fd9942aa8..f28097b88 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 77ee02b9c..519d62ade 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -16,14 +16,14 @@ APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.13.0" -MONERO_COM_BUILD_NUMBER=85 +MONERO_COM_BUILD_NUMBER=86 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.16.0" -CAKEWALLET_BUILD_NUMBER=208 +CAKEWALLET_BUILD_NUMBER=209 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index e772ea128..f7887e4bf 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -14,12 +14,12 @@ APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.13.0" -MONERO_COM_BUILD_NUMBER=83 +MONERO_COM_BUILD_NUMBER=84 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.16.0" -CAKEWALLET_BUILD_NUMBER=234 +CAKEWALLET_BUILD_NUMBER=236 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 95201dc02..2a937f4f9 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -17,12 +17,12 @@ fi MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.3.0" -MONERO_COM_BUILD_NUMBER=17 +MONERO_COM_BUILD_NUMBER=18 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.9.0" -CAKEWALLET_BUILD_NUMBER=69 +CAKEWALLET_BUILD_NUMBER=70 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index b2f3ca691..72a019636 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -13,6 +13,9 @@ const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart'; const tronConfigPath = 'tool/.tron-secrets-config.json'; const tronOutputPath = 'cw_tron/lib/.secrets.g.dart'; + +const nanoConfigPath = 'tool/.nano-secrets-config.json'; +const nanoOutputPath = 'cw_nano/lib/.secrets.g.dart'; Future main(List args) async => importSecretsConfig(); Future importSecretsConfig() async { @@ -37,6 +40,11 @@ Future importSecretsConfig() async { final tronOutput = tronInput.keys.fold('', (String acc, String val) => acc + generateConst(val, tronInput)); + final nanoOutputFile = File(nanoOutputPath); + final nanoInput = json.decode(File(nanoConfigPath).readAsStringSync()) as Map; + final nanoOutput = + nanoInput.keys.fold('', (String acc, String val) => acc + generateConst(val, nanoInput)); + if (outputFile.existsSync()) { await outputFile.delete(); } @@ -60,4 +68,10 @@ Future importSecretsConfig() async { } await tronOutputFile.writeAsString(tronOutput); + + if (nanoOutputFile.existsSync()) { + await nanoOutputFile.delete(); + } + + await nanoOutputFile.writeAsString(nanoOutput); } From 97555f2ce12419187bdf004aa8af799bf0682bca Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 10 May 2024 21:34:28 +0300 Subject: [PATCH 05/25] V4.16.0 (#1445) * update build numbers * fix UI issue * Add Nano to secret generation properly [skip ci] * remove to utc from expiration time * update app version Remove tron --- assets/text/Release_Notes.txt | 1 - cw_tron/lib/tron_client.dart | 4 ++-- lib/view_model/send/send_view_model.dart | 2 +- scripts/android/app_env.sh | 2 +- scripts/android/pubspec_gen.sh | 2 +- scripts/ios/app_config.sh | 2 +- scripts/macos/app_config.sh | 2 +- scripts/macos/app_env.sh | 4 ++-- 8 files changed, 9 insertions(+), 10 deletions(-) diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 3ff7aa3b4..f9b05cea2 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,4 +1,3 @@ Hardware wallets support for Bitcoin, Ethereum and Polygon -Add Tron wallet Security enhancements Bug fixes and generic enhancements \ No newline at end of file diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index e25887002..92684cebc 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -387,7 +387,7 @@ class TronClient { if (feeLimit > tronBalanceInt) { final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString())); throw Exception( - 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.\nTransaction fee: $feeInTrx TRX', + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up.\nTransaction fee: $feeInTrx TRX', ); } @@ -444,7 +444,7 @@ class TronClient { if (feeLimit > tronBalanceInt) { final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString())); throw Exception( - 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up. Transaction fee: $feeInTrx TRX', + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up. Transaction fee: $feeInTrx TRX', ); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 0d53c59cc..a633d3982 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -580,7 +580,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } if (errorMessage.contains('Transaction expired')) { - return 'An error occurred while processing the transaction. Kindly retry the transaction'; + return 'An error occurred while processing the transaction. Please retry the transaction'; } } diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 519d62ade..c671a013f 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -23,7 +23,7 @@ MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.16.0" -CAKEWALLET_BUILD_NUMBER=209 +CAKEWALLET_BUILD_NUMBER=210 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index bc7985506..d238052fe 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index ab7fbd422..9f59d6632 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -28,7 +28,7 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" ;; $HAVEN) diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index a1143bb12..bd1417c4b 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -31,7 +31,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 2a937f4f9..e16df1e61 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -17,12 +17,12 @@ fi MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.3.0" -MONERO_COM_BUILD_NUMBER=18 +MONERO_COM_BUILD_NUMBER=17 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.9.0" -CAKEWALLET_BUILD_NUMBER=70 +CAKEWALLET_BUILD_NUMBER=71 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From e92e8df3aaab119679f94d4561a0931d0a305a82 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 13 May 2024 23:59:11 +0300 Subject: [PATCH 06/25] Generic enhancements (#1447) * Disable hardware wallets for Monero.com * disable ble permission on Monero.com * code cleanup [skip ci] * Fix missing Trade bytes issue --- assets/text/Monerocom_Release_Notes.txt | 2 +- assets/text/Release_Notes.txt | 2 - cw_bitcoin/pubspec.lock | 72 ++++++--- cw_core/pubspec.lock | 72 ++++++--- cw_haven/pubspec.lock | 74 ++++++---- cw_monero/example/pubspec.lock | 66 ++++++--- cw_monero/pubspec.lock | 74 ++++++---- cw_nano/pubspec.lock | 72 ++++++--- ios/Podfile.lock | 2 +- lib/exchange/trade.dart | 138 ++++++++++++++++-- lib/main.dart | 86 ++++------- .../screens/restore/restore_options_page.dart | 20 ++- lib/src/screens/root/root.dart | 4 - .../hardware_wallet/ledger_view_model.dart | 19 ++- scripts/android/app_env.sh | 8 +- scripts/ios/app_env.sh | 8 +- scripts/macos/app_env.sh | 8 +- 17 files changed, 499 insertions(+), 228 deletions(-) diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index d5297ebe1..faad67777 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1 +1 @@ -Generic bug fixes and enhancements \ No newline at end of file +Bug fixes and generic enhancements \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index f9b05cea2..faad67777 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,3 +1 @@ -Hardware wallets support for Bitcoin, Ethereum and Polygon -Security enhancements Bug fixes and generic enhancements \ No newline at end of file diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index a398a11e0..e6f0b34dd 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -217,10 +217,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -446,6 +446,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" ledger_bitcoin: dependency: "direct main" description: @@ -483,26 +507,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -547,10 +571,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -736,26 +760,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -784,10 +808,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" timing: dependency: transitive description: @@ -820,8 +844,16 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: "direct overridden" description: name: watcher sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" @@ -861,5 +893,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.6 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.10.0" diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 678e57b54..abfdbfc58 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -331,6 +331,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" logging: dependency: transitive description: @@ -343,26 +367,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -407,10 +431,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -564,26 +588,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -612,10 +636,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" timing: dependency: transitive description: @@ -640,8 +664,16 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: "direct overridden" description: name: watcher sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" @@ -681,5 +713,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.10.0" diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index d84523539..8aeb70a97 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -338,6 +338,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" logging: dependency: transitive description: @@ -350,26 +374,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -406,10 +430,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -571,18 +595,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -611,10 +635,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" timing: dependency: transitive description: @@ -639,6 +663,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" watcher: dependency: "direct overridden" description: @@ -647,14 +679,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 - url: "https://pub.dev" - source: hosted - version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -688,5 +712,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/cw_monero/example/pubspec.lock b/cw_monero/example/pubspec.lock index ece0d4395..7816a7fad 100644 --- a/cw_monero/example/pubspec.lock +++ b/cw_monero/example/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -201,6 +201,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -213,26 +237,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mobx: dependency: transitive description: @@ -245,10 +269,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: transitive description: @@ -362,18 +386,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -394,10 +418,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" typed_data: dependency: transitive description: @@ -414,14 +438,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - web: + vm_service: dependency: transitive description: - name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "13.0.0" win32: dependency: transitive description: @@ -439,5 +463,5 @@ packages: source: hosted version: "0.2.0+3" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index b736f80cb..adb50bd02 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -354,6 +354,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" logging: dependency: transitive description: @@ -366,26 +390,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -422,10 +446,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -595,18 +619,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -635,10 +659,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" timing: dependency: transitive description: @@ -663,6 +687,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" watcher: dependency: "direct overridden" description: @@ -671,14 +703,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 - url: "https://pub.dev" - source: hosted - version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -712,5 +736,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index cbcf3d38d..2c2a342ca 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" convert: dependency: transitive description: @@ -399,6 +399,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" libcrypto: dependency: "direct main" description: @@ -419,26 +443,26 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: @@ -492,10 +516,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: transitive description: @@ -713,26 +737,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -761,10 +785,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" timing: dependency: transitive description: @@ -789,8 +813,16 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: "direct overridden" description: name: watcher sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" @@ -830,5 +862,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0cc57e075..c4ee98c37 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -307,7 +307,7 @@ SPEC CHECKSUMS: Toast: ec33c32b8688982cecc6348adeae667c1b9938da uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 - url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index 6cc3fddbe..aeb544ece 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -5,9 +5,6 @@ import 'package:cw_core/format_amount.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; -part 'trade.g.dart'; - -@HiveType(typeId: Trade.typeId) class Trade extends HiveObject { Trade({ required this.id, @@ -32,6 +29,7 @@ class Trade extends HiveObject { this.txId, this.isRefund, this.isSendAll, + this.router, }) { if (provider != null) providerRaw = provider.raw; @@ -121,21 +119,26 @@ class Trade extends HiveObject { @HiveField(21) bool? isSendAll; + @HiveField(22) + String? router; + static Trade fromMap(Map map) { return Trade( - id: map['id'] as String, - provider: ExchangeProviderDescription.deserialize(raw: map['provider'] as int), - from: CryptoCurrency.deserialize(raw: map['input'] as int), - to: CryptoCurrency.deserialize(raw: map['output'] as int), - createdAt: - map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, - amount: map['amount'] as String, - walletId: map['wallet_id'] as String, - fromWalletAddress: map['from_wallet_address'] as String?, - memo: map['memo'] as String?, - txId: map['tx_id'] as String?, - isRefund: map['isRefund'] as bool?, - isSendAll: map['isSendAll'] as bool?); + id: map['id'] as String, + provider: ExchangeProviderDescription.deserialize(raw: map['provider'] as int), + from: CryptoCurrency.deserialize(raw: map['input'] as int), + to: CryptoCurrency.deserialize(raw: map['output'] as int), + createdAt: + map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, + amount: map['amount'] as String, + walletId: map['wallet_id'] as String, + fromWalletAddress: map['from_wallet_address'] as String?, + memo: map['memo'] as String?, + txId: map['tx_id'] as String?, + isRefund: map['isRefund'] as bool?, + isSendAll: map['isSendAll'] as bool?, + router: map['router'] as String?, + ); } Map toMap() { @@ -152,8 +155,111 @@ class Trade extends HiveObject { 'tx_id': txId, 'isRefund': isRefund, 'isSendAll': isSendAll, + 'router': router, }; } String amountFormatted() => formatAmount(amount); } + +class TradeAdapter extends TypeAdapter { + @override + final int typeId = Trade.typeId; + + @override + Trade read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = {}; + for (int i = 0; i < numOfFields; i++) { + try { + fields[reader.readByte()] = reader.read(); + } catch (_) {} + } + + return Trade( + id: fields[0] == null ? '' : fields[0] as String, + amount: fields[7] == null ? '' : fields[7] as String, + createdAt: fields[5] as DateTime?, + expiredAt: fields[6] as DateTime?, + inputAddress: fields[8] as String?, + extraId: fields[9] as String?, + outputTransaction: fields[10] as String?, + refundAddress: fields[11] as String?, + walletId: fields[12] as String?, + payoutAddress: fields[13] as String?, + password: fields[14] as String?, + providerId: fields[15] as String?, + providerName: fields[16] as String?, + fromWalletAddress: fields[17] as String?, + memo: fields[18] as String?, + txId: fields[19] as String?, + isRefund: fields[20] as bool?, + isSendAll: fields[21] as bool?, + router: fields[22] as String?, + ) + ..providerRaw = fields[1] == null ? 0 : fields[1] as int + ..fromRaw = fields[2] == null ? 0 : fields[2] as int + ..toRaw = fields[3] == null ? 0 : fields[3] as int + ..stateRaw = fields[4] == null ? '' : fields[4] as String; + } + + @override + void write(BinaryWriter writer, Trade obj) { + writer + ..writeByte(23) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.providerRaw) + ..writeByte(2) + ..write(obj.fromRaw) + ..writeByte(3) + ..write(obj.toRaw) + ..writeByte(4) + ..write(obj.stateRaw) + ..writeByte(5) + ..write(obj.createdAt) + ..writeByte(6) + ..write(obj.expiredAt) + ..writeByte(7) + ..write(obj.amount) + ..writeByte(8) + ..write(obj.inputAddress) + ..writeByte(9) + ..write(obj.extraId) + ..writeByte(10) + ..write(obj.outputTransaction) + ..writeByte(11) + ..write(obj.refundAddress) + ..writeByte(12) + ..write(obj.walletId) + ..writeByte(13) + ..write(obj.payoutAddress) + ..writeByte(14) + ..write(obj.password) + ..writeByte(15) + ..write(obj.providerId) + ..writeByte(16) + ..write(obj.providerName) + ..writeByte(17) + ..write(obj.fromWalletAddress) + ..writeByte(18) + ..write(obj.memo) + ..writeByte(19) + ..write(obj.txId) + ..writeByte(20) + ..write(obj.isRefund) + ..writeByte(21) + ..write(obj.isSendAll) + ..writeByte(22) + ..write(obj.router); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TradeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; +} diff --git a/lib/main.dart b/lib/main.dart index fa71da31d..f043b0cd2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,6 @@ import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/locales/locale.dart'; -import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; @@ -38,7 +37,6 @@ import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/src/screens/root/root.dart'; -import 'package:uni_links/uni_links.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cw_core/cake_hive.dart'; @@ -46,7 +44,7 @@ import 'package:cw_core/window_size.dart'; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); -final RouteObserver routeObserver = RouteObserver(); +final RouteObserver> routeObserver = RouteObserver>(); Future main() async { await runZonedGuarded(() async { @@ -63,13 +61,38 @@ Future main() async { }; await setDefaultMinimumWindowSize(); - + await CakeHive.close(); await initializeAppConfigs(); runApp(App()); }, (error, stackTrace) async { + runApp( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: SingleChildScrollView( + child: Container( + margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Column( + children: [ + Text( + 'Error:\n${error.toString()}', + style: TextStyle(fontSize: 22), + ), + Text( + 'Stack trace:\n${stackTrace.toString()}', + style: TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ), + ), + ); + ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace)); }); } @@ -229,61 +252,6 @@ class App extends StatefulWidget { } class AppState extends State with SingleTickerProviderStateMixin { - AppState() : yatStore = getIt.get(); - - YatStore yatStore; - StreamSubscription? stream; - - @override - void initState() { - super.initState(); - //_handleIncomingLinks(); - //_handleInitialUri(); - } - - Future _handleInitialUri() async { - try { - final uri = await getInitialUri(); - print('uri: $uri'); - if (uri == null) { - return; - } - if (!mounted) return; - //_fetchEmojiFromUri(uri); - } catch (e) { - if (!mounted) return; - print(e.toString()); - } - } - - void _handleIncomingLinks() { - if (!kIsWeb) { - stream = getUriLinksStream().listen((Uri? uri) { - print('uri: $uri'); - if (!mounted) return; - //_fetchEmojiFromUri(uri); - }, onError: (Object error) { - if (!mounted) return; - print('Error: $error'); - }); - } - } - - void _fetchEmojiFromUri(Uri uri) { - //final queryParameters = uri.queryParameters; - //if (queryParameters?.isEmpty ?? true) { - // return; - //} - //final emoji = queryParameters['eid']; - //final refreshToken = queryParameters['refresh_token']; - //if ((emoji?.isEmpty ?? true)||(refreshToken?.isEmpty ?? true)) { - // return; - //} - //yatStore.emoji = emoji; - //yatStore.refreshToken = refreshToken; - //yatStore.emojiIncommingSC.add(emoji); - } - @override Widget build(BuildContext context) { return Observer(builder: (BuildContext context) { diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 454d124da..a703c9f9e 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -13,6 +15,9 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/hardware/device_connection_type.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -24,6 +29,19 @@ class RestoreOptionsPage extends BasePage { final bool isNewInstall; + bool get _doesSupportHardwareWallets { + if (!DeviceInfo.instance.isMobile) { + return false; + } + + if (isMoneroOnly) { + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + .isNotEmpty; + } + + return true; + } + @override Widget body(BuildContext context) { final imageColor = Theme.of(context).extension()!.titleColor; @@ -57,7 +75,7 @@ class RestoreOptionsPage extends BasePage { description: S.of(context).restore_description_from_backup, ), ), - if (DeviceInfo.instance.isMobile) + if (_doesSupportHardwareWallets) Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index afdd14865..b6406dfbd 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -1,10 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; @@ -13,7 +10,6 @@ import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:mobx/mobx.dart'; import 'package:uni_links/uni_links.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart'; diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 06ddaf275..f05b1c805 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -1,8 +1,12 @@ +import 'dart:io'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:ledger_flutter/ledger_flutter.dart'; @@ -11,8 +15,21 @@ import 'package:permission_handler/permission_handler.dart'; class LedgerViewModel { late final Ledger ledger; + bool get _doesSupportHardwareWallets { + if (!DeviceInfo.instance.isMobile) { + return false; + } + + if (isMoneroOnly) { + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + .isNotEmpty; + } + + return true; + } + LedgerViewModel() { - if (DeviceInfo.instance.isMobile) { + if (_doesSupportHardwareWallets) { ledger = Ledger( options: LedgerOptions( scanMode: ScanMode.balanced, diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index c671a013f..3e5db68d2 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.13.0" -MONERO_COM_BUILD_NUMBER=86 +MONERO_COM_VERSION="1.13.1" +MONERO_COM_BUILD_NUMBER=87 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.16.0" -CAKEWALLET_BUILD_NUMBER=210 +CAKEWALLET_VERSION="4.16.1" +CAKEWALLET_BUILD_NUMBER=211 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index f7887e4bf..c5a2e6306 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.13.0" -MONERO_COM_BUILD_NUMBER=84 +MONERO_COM_VERSION="1.13.1" +MONERO_COM_BUILD_NUMBER=85 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.16.0" -CAKEWALLET_BUILD_NUMBER=236 +CAKEWALLET_VERSION="4.16.1" +CAKEWALLET_BUILD_NUMBER=239 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index e16df1e61..ebe3115be 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.3.0" -MONERO_COM_BUILD_NUMBER=17 +MONERO_COM_VERSION="1.3.1" +MONERO_COM_BUILD_NUMBER=18 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.9.0" -CAKEWALLET_BUILD_NUMBER=71 +CAKEWALLET_VERSION="1.9.1" +CAKEWALLET_BUILD_NUMBER=72 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From c12b4f5ff611810edf8e3668c0c26b25049ecada Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 14 May 2024 04:27:16 +0300 Subject: [PATCH 07/25] improve error handling (#1451) --- lib/main.dart | 45 +++++++++++++++++++++----------------- scripts/android/app_env.sh | 8 +++---- scripts/ios/app_env.sh | 8 +++---- scripts/macos/app_env.sh | 8 +++---- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f043b0cd2..eeee4fbc3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,6 +47,7 @@ final rootKey = GlobalKey(); final RouteObserver> routeObserver = RouteObserver>(); Future main() async { + bool isAppRunning = false; await runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); @@ -67,31 +68,35 @@ Future main() async { await initializeAppConfigs(); runApp(App()); + + isAppRunning = true; }, (error, stackTrace) async { - runApp( - MaterialApp( - debugShowCheckedModeBanner: false, - home: Scaffold( - body: SingleChildScrollView( - child: Container( - margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), - child: Column( - children: [ - Text( - 'Error:\n${error.toString()}', - style: TextStyle(fontSize: 22), - ), - Text( - 'Stack trace:\n${stackTrace.toString()}', - style: TextStyle(fontSize: 16), - ), - ], + if (!isAppRunning) { + runApp( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: SingleChildScrollView( + child: Container( + margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Column( + children: [ + Text( + 'Error:\n${error.toString()}', + style: TextStyle(fontSize: 22), + ), + Text( + 'Stack trace:\n${stackTrace.toString()}', + style: TextStyle(fontSize: 16), + ), + ], + ), ), ), ), ), - ), - ); + ); + } ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace)); }); diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 3e5db68d2..b520a3179 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.13.1" -MONERO_COM_BUILD_NUMBER=87 +MONERO_COM_VERSION="1.13.2" +MONERO_COM_BUILD_NUMBER=88 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.16.1" -CAKEWALLET_BUILD_NUMBER=211 +CAKEWALLET_VERSION="4.16.2" +CAKEWALLET_BUILD_NUMBER=212 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index c5a2e6306..f70963745 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.13.1" -MONERO_COM_BUILD_NUMBER=85 +MONERO_COM_VERSION="1.13.2" +MONERO_COM_BUILD_NUMBER=86 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.16.1" -CAKEWALLET_BUILD_NUMBER=239 +CAKEWALLET_VERSION="4.16.2" +CAKEWALLET_BUILD_NUMBER=240 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index ebe3115be..afdac3e6c 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.3.1" -MONERO_COM_BUILD_NUMBER=18 +MONERO_COM_VERSION="1.3.2" +MONERO_COM_BUILD_NUMBER=19 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.9.1" -CAKEWALLET_BUILD_NUMBER=72 +CAKEWALLET_VERSION="1.9.2" +CAKEWALLET_BUILD_NUMBER=73 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 4947e231e9afcd38267bab82617130067b1e857b Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Mon, 13 May 2024 19:07:16 -0700 Subject: [PATCH 08/25] Cw 613 quantex (#1377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * save progress * [skip ci] * forgot to add [skip ci] * not sure what exactly I changed but it just works now! ¯\_(ツ)_/¯ * status updates * minor cleanup * minor fix (toUppercase needed) * remove unnecessary apikey + keep original raw values * fix track url for quantex * only increment raw values --------- Co-authored-by: Omar Hatem --- .github/workflows/pr_test_build.yml | 1 + assets/images/quantex.png | Bin 0 -> 16353 bytes .../exchange_provider_description.dart | 13 +- .../provider/quantex_exchange_provider.dart | 252 ++++++++++++++++++ lib/exchange/trade_state.dart | 27 ++ .../exchange/exchange_trade_view_model.dart | 4 + .../exchange/exchange_view_model.dart | 2 + lib/view_model/trade_details_view_model.dart | 6 + tool/utils/secret_key.dart | 1 + 9 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 assets/images/quantex.png create mode 100644 lib/exchange/provider/quantex_exchange_provider.dart diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 69c632967..acaa12fe0 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -151,6 +151,7 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart diff --git a/assets/images/quantex.png b/assets/images/quantex.png new file mode 100644 index 0000000000000000000000000000000000000000..cfa32b3825a29161e38e708955bc96de3671098e GIT binary patch literal 16353 zcmbtbWm83GVKTyR$+6`BgpN;CV53 z>fWlEsat2}O!w*TC>13cbQB^K004k4Co8G;udn;BAtC&G*0$^!{p*mOWp&*E095S% z8Vn#KivR#12gpf^X?SOyb@^mjT6$c(8`@vGDQVuTpd+Cxk-NxJt0J?n8pX{>)?U-n z5+x(xa7lByM2>ceaziqM0eNBZg|w| z5!t-IaI5IB^{qJJ*SzXxe)8S@K=f>lX~^oV)f@lv-FvQ;|IPp7!ck z$qb&8p~?p8xbJk0prAP92Y}vy>3!{ae6pj z5|RwNX$c)GE(QF!BKVDEw%`O{?YbYN8pD4iC(OThM`QRBp(nM!6#Z%FIS4GU?(JFd zQ9~5D#IzH4yWmN`z_Wq)66jo*gkq5-PHX1l-!pYzJ)u8hn-hB`jN&`ZYvdO}Obk98 z%Ewh{RUPX2+t&Hf9SeF2ZF{gaX(<0Xz7gPn>h^w;buvPYVg-_)4uh;ik&1YYl1WvD z?pJzCah>xHrS+d`wSzi+I(_6@W0Re;q+rpP&JH29OK^hgsq!uoliMgW)Pu3fvBtx{K?<$~R{b)!}wHsa(wK!~UsLR4-Ci;>87vWLyb$IxpaNootSeSOG2emPT zMMp4{5JK`^k-1P{=OS3}5HQ^Y^3+^)P@g@SYi-*dZg^tWzgmZ#o!uWoQJqA1WBV}S zUq#+^J^64P_r1n&w?QeR9_AVLs2=n}NvbxmbTBq#zIYWVL%VF(31;8^yqzGFZjmDz zorcsbunE!`pWL;=wZRktX6;+C#<^`vQ)IV6h8u>#Tl;mG%URy|uYLZNWsU_-oj-}; z|9HnC@)#YlIwF)2TN3F20^*6iU@qYX;xcmjteBS5p!NPHrZ?v40G`57$obG#QptA_ z1c_uu9&0I%@pEEUu7+Qs>18QC5Agw`F6^8=6!GYRvA-PFsd#9@R*eqWji{g-B5(C*L*`;qOUk?b4fkzWmFgW!_u03ZWv zon&RaegyuRhZWws22nhdA)n#fR^l~6^H;^Oq2Z--%vwCF3iLaRUFngoT$c>P|W=BxTBci5kX5&!L zku)N-jNTqH0+VC^0i)CXoHBNgHwDJn8WZE71R)?F?FW)|;F+&PjOH%AloB8j0iIcR$f*%cG_V$>+hEQ-;Ra&p2fukb zt++F?e0@Oo{@lCLTLEozi5XPFJK*t7GJhdz2!u~3bHlLZd2{!cpn1#YHJyz;3%|3K zWJDN#ew0-m3m_+~DFaQRe_11VQSxfYDQkw%bjT$af@8phpvjGV7lHfo^KU_9J45`c zf!O3M2Da;F2X*?H;T!!t(2RRtU;nL%#pn~wV;%Y9@h%a!%%f+w{8S-k6lbJ5rDQrW zNy>%aim2WuEz6%w1VBpNdsf5IC)t3;5Uoa2?U#E2_eOv6ut4)ro+D$NZK_@ch&AE~ z`7Noi^m0dN`gdz{WHKvw+WI~)*N-l=eq?S`e@B_`!}=9P`s6RoAlicV1#iwDhy9&I zPCwEFi2}0%r#gbMZGk~FBO=~AO3aR)cAZ{F(yySzKfm3Mi7$!Q*`7jI;)2GuC9oQ< z{0Y9=K=m3rQ}Qy_6+SYy)DY9rzSqMWbKZB%As>tfb+X5KEi(U>zkcGXd6;!iGrd~-@1@P~JTl2!b z2Gz1z1G@`%h6@hzgz`u>qXj7ba>Ce9T!=DVb8qsYzKtjTJO~I=hM2bYNa&Gh4d)y3 z+S9~cT*W?alhVK3hrU57!G*z>Fy3+$jt(++xcaY_3OSijTmXh=!>#0f%Wepx>hCy8 zLV(}T!wbgd!zs9K{TzI)R7v*xfJ>PO5Tq>2JCN(&%^LD1e&aCqS*oGj8o={szV>a|n{sb>!CG<;ExjlZQKwDY5mD}G(jMe~`9iI0 zD>#p3b%cla+k4pPPrvs@oRP2Xd2KH(7(dExGeP!&Y|QqxD^m1@atZ-i8D_EVx-MB> zTw6n*fpzq=)qyG5@ALfcj!EmmLM=Eco)B8qco-H~*Ul|3_d~&tuBn~1ANR{m4#C1W z;l9T8mH$?@D(T)G1~uc|-AGSygj+Mmz(TvW=eCC!w{ICzCI-aO)UB->>1iw4d*kqD zFWTfh(G;c7wW4lRc)MX9XzDcYoOuS~$A9}2B+BK*04pW`QaXRn6rPV`EWWb|^yZlo z{L&ESpMcj`u2@jceN|cTWUe|%b)1wNttMTg#IU1;aK?XLnb>-U?{i*F8T+fI0U##uTDG-WAaTi8Ke zy<2qezRnK8K(HIw`gdC;bdyrJ1!-8IjZ=!jyy+<^lLY9i4Jju8ipYI@V?cH@6VZv} z?QR=Jxb!x7S)Y&(e27=Es7drm3%^DK(brJ@!CjIuN(=urnRzF>%n^X1%4B1^p`1^> zUpbLw8x&(Pf+3fi$uPd6&cEpsNf^SU>NvDEQ4@26y!F7u;vl)$s2ZP2<7^Zm>-iqrEw_`giRwYXv>e%1{xWR zn+y$1UpKLdM+w&R!@{R~1!PO&XPk(9bFqFSYAAs9n0FP3rFI)XR##1fUx%6=nFjd= z9j-k+e!@D|0si_ko^TlE;98nE@W4(nFGhINy=xEpL^WCs_&_5s1#_NRYkK!YW-7%{ zz$`_xwn3!(H-yz6oo{wq3BIiQTYJ(bS0+XPWf!8zCp$QaL1KwF^g55c9=Nug&?XZb zINCKFBVembB|R%bH&$jt_q0%yLokj~i4WKj?yitp3}t*B%9HsEy77g2#cI-&X`Pa} z41E^ik=o$#xZByVk<}m~a3{4ZnZB-Jxx|_A&6C)+0E#k=w9(bO#=AarG}R4mk#SiN zFOr2=@PwSCr*+AKOMzZCjHhs6TC%84akx~xMni$`K17Aufx`@C8>*Oq=bAEMwPXkt z+Nx3mL08(_VZ$2ptt^G3aWNTR-BQ$zi5VLWU1Scm;+{;S%e4j7tG(JvabZ8rV$AK~ z1W0bzXXVd>nHo)&Isk#s?Pf5W+WA+jpWcKdx~ee78R{{oDC*Z)=~^(OjkW_xgN&L>zR$w3f2eW*J%B7&a~1!a_bAuhUMX$edD0bvx<^j9yi2@F$tFx~ zAen1}_MWBSyy_oO{qGE-BH462L_zcwhk&tASBJ_^S_CPW@?C_M8M5G5^9`uK!%fDD zFm)ASXhXnpiM2GJ06rwfJi}t;Wy23uKdR6sNk}Xi15i7TD1d{+ZO-Pq|Fe+}gy5C!_s3SeIQSB1;d<7mKbF$I9`6m+} z!pQud65>@?Ysxrp!$*8yG>~}#2{|_sFJVM_HT#g<{k^UQ97e4|Ag2gpxO)z1U&t=2 z_sb73XW*euSQG7}z1c}gC}E3^yqwKD0^Td(1@z|a0B`r{g#j40)AWL9yzsUv|2}-X z=4tEML;G9`W}iosZ6o~X1^3c4{=KNLWUkJ;!$Ojs*P6m{dsF^4S%sJ;oGot<@MXrI zj`ddkWOMM;j}ytrRPrRN?ks(dABSoHG8afIRG*4zBOQGK55rqJ1N{ny*KfZ!oMqjW z{jexZThc}gRyE!o^0~m|tE6(FiZPba`H06k%=%?FZ0lHDDjcqz77^>!_sW_Qw+XNM z8FtArSh;5yP87{aa+=M4JPsq^lFs7%Q%yQ-m7E0cnNPSQ?4})o8jgoyW8L?9v%N!+ z&>vy-=&4`e50aqunTBu=Y}*UhgXv^mQP9A%H|v-NOJ2o;aQ&Mab;*^b3E^BTQ%$d| zAJ#YPt(^{?bV)Xs#J+y{yZB7MQAaMGRP0$!snv+-<1Q7`>f z);FYrtPh?T!Am9^C zh4!Gr{M9e`la`%XmkzRy}wgn~jB=aqTGkvF%k2mifLXf<0& zJf0Rkpe00`w-9<~FaKO8;ihNvX0zGi5p5Ly+>a}e4E-!2i=+o3l9kXb9 z8(`2L=P&)%*0{Do8K3yzvTB5R-%Szl??Als-c}dgHALprFy|EfaD0dneN#G7 zuV1P>L7O|yLT5o<&0tg!+=+sh2~c`N%sfZyvf&H72{Y#kBJ$NJLJWU$w~Fo!DT zap;j7at@#ozL`@X>^kN+sdt$!U@O&HxSCpi*1Z2FDL&3_r^y@&5#II|nI=R0tCt_; zKgnao1pf<{HV#qJZ_Ot;R{_FgB97lHz zer9`5VzSvg2Y@%`$QYWyJP~`nCG~RtIjOIk>1|WKr$ydU(?ov&X_C#u`;|1w(7dY4XSfIatFh*hENwtZ1 z&1)I`NNWlk{5)j1F%L401e`W3R)=ZC8)Avu%5_zOvc(?hXRoc{R+FRWC(J4Px z#S65#{Tz5a5W9YZUB#`Yf|I6FJXY_;Rq^N_;!Mz`WvRCmJr zj8#*U6M`7nuqNqLHZjRk4w;nT|&^IBB8rz`@+D~q9z3n{^XZ(Wp?5RrP zVQ%RBo6@@`OvLx<&5ljdCoHp#xHfKU%Q!vwFA{o&Nl_|u%sI@aWPzfLr39YX9+`pU zhlbOFpiFO-sr?v~B=eCt}St|FQzszZHbe3vKyd z?c9A3Bv&J1kR*!dhRxa^LWO|&UTqeFk_tkTE_=6pR7_mOS?)@2m# z?p5Dard^cp=CiIwXoOeFnndS_m3ajnkb~SNr}ZHU;pEbsGtuVdqjJ+Uf_g&h?RyeZ z9rKk0m&R2TA&$!wa>IGA0b!dSz6@2zxj2vB>(??<<7NhaauuvATLt8#im8YB2M1%`nv^2r?%(RM&7y4l&=HR% z4%a!{G=3XZy)$c^2O8w8K-QYtmAmsYeFy9MQnDOavy{;~;uO!m@Ipb7R>FO*0ntWR zhWbgJnf6zrhu8iHn+=~IKPozzYG6GHqnuvxoXhA_MN#g<(b%QNv6h(39!4myy%No? zR=gW7$PF}6DIL39!ot?7$sI?@T^4MQ#(1PM>lGsF+z*~X{>Tm@+mBK{)XOyW4-E1f z?dQHCM_DZ#_f8RJ+4cQJ@C83BLQVt}+hW*^I4eK58R9xbY?uke2nhi(DDeuzwKU3_ zevS8TOj3Egc@1KPq8nXdZMK-3u~xN>296X{&9iYCGe)^} zort!VKW;Q)2}~okWTvqn)o0+2m%spbDk24AneEuwVb9TnRA&yn-E2R9|YN5-HLIas6eY)q#B` zn{Q0@D2xd`H#c`(o%j(j9 z*>hlpd!sutSsX@#9tnHf!`U_+6SD?c9|?)_YiVzCKO&p^jAAJ^Yd!%3m zvkTteWFh~tmMaw)jeQPop-%X9pZ8DAEQ-`3J^eDcsi%i4t0Xb5X=^*0dzv(>JV~w% zXJ?TY07ZtSLpAy~$+Z)gR|$Tnj0;+XnIFfYVYaX^RZ=1vv3=bg;@vni1wGiwQdr4f z{n&dO?XM`_c=#c@#K2?q$Jt&(|2W19Y2^`R6j9jXA@9|>dXTizQU{|kOgD}Z%e-M~ ztTsQVn>dO=pN?+Xe<{G7tg-4LYq4lu39C|2QBAih;ZJT zFIvf{0}Y9SA89t<2CatPpiDrniM*!hC{DGeZ;9Kqr&`%sM>@N0?3HJ)&d)|Y!NDc1 zp)_LIA;l{0bGkz%baj50dEJj%t&bm0?0yln`35988uj$eBumDH(&NKmct95VU?iGt zfwqbb8CIOy1X(VKm$kSH{+mZbcH82-17ZwUxT)1%N_f&ip6piYdRu->-G6!fZ%L-t z;HYap4XMgt7Qi}71vt%wIhq<7NRofXztqlFko=hkNV9c{AmlE`Iwl^_4~9*}9I4lsQbb%Hvt_XkMtw zdnYI=)lrP=+0M4Y@;lGSmx}d~U0xlGH5cE#$shM2Giy2Wsr9{f|S{_CIuTx(z4`_7P?WE3+gDzl&+j7K@9sUxSk*${OdGg%sNc<*i9D5@8!{@p#6452HB+H(s| zU-{E5iG+DCY5T9v_^_g50yV)43&p4CRmuAAxb0^r@8Q_$>9q3e<`g(htgz29u4+mR z6tfwwx>Y_S2~+{{5*R`fT2HjRT2Uv~;Og1MR7WU}aJw4ATAB*oLq#p3$M{>(C4a8= zW5)mi5m36{B9o~Zr2({jDEb93aKS}xVs zlkas6B+d77h$;f&8r@mqc?!I^4aVf%%$f7)1W4C?C$6zx_-<_dQg**9<;RwNxMJlg zVD%mwK3<=j&k+j5Kn))bZV4_3vC~(d)Q7KveN+E=j}W1v+~GyHe)2r}Fp?{k75V?8WI) z*XgkZAY|MPOR5Z29q+!m&fYxhDgI&Np6!>>K3WcU8i)$X#j#bKwTTiz zuL8Uws6n~QB$cgtVr{V(PD*cWlGhI%L9sCSq7wL7##%CRQr9WfY7Fv4^puW<#+Fte zUUUWbK^8K}DgDraX5%&5@YP_>#@yKBid7!U&)HTdUYREoC4xfV?hd3BhW~+lP1VzP zw4f~>!!LhkDG|t2-gQSuiP|c~wHecni2rf{LV82mFcH29 z>^R$Jsu623JeY*lmY*yIhpxA4{R95=d35odnBe_@?w_dvG*VM1$3$8=8~P4}MzpAa{`x_O-F2Y& z>~^8y_{BdIo3k2;ax_8e%hzOcNx3iNab%v-+)pIj7KJV`?Av$^&6%2@&fkhby2)f8 zEVP!gGi&bxdR|neK~M@0`>N%;a_b-VM;R*}Dj?o7FA4T^Nx>*4I75PdlGP6;Vw)PG zatfvCx<*NUe;~HCGm@M}AA)VpXA6w3$w}I-T}Pujw)>XxUZ8??{|*(Vh|@~7tqjzK z6IH$^!yM{%pIxwuwf`y7**hN^T6c4?M$on|UvUl8h{mH-s};Pd1h8^9=xJ_!Y~G~W zFHcP|*zZm}>Tj;gbBd?$FZe@qAga(;{bzmw18j&YsEhlRjY+|<*|aM*=-@}J(X9l% zI!c=#V?5=karCbQ;@lXgKf2RvKO8}S+%hkulA?u0WRrhT?B&ZbYb6*wQw@nc%B2{( zMS0K2OZ|bV__M{9**)s}!y4}Wvfv=Yh;jHKN7$>Te%pyH@ot#QE_ofji{tL2TGaa# zLwl`_txG^@tO6=YghGCa~U zM?Y!wOa$J$hS;mEz=+blY|MCl2s)UX=kchhI#toqxb1p@HNmY=ESF;l5-nI(qmPl1esIWG69WKfJVHEm4~xvdbLsNF+ZZb zb><(()nOHm{89{?A5;$xikov%#j8Aqy}#hQ0GhP^ST}>{96sfmQ86vQL=G&(G^ds9 zvP9RwXCk~Yt}_W~-W!DDxb{Ep{x0Zp3DKe_D4*0BOCtMX=~R{zccP3=N>-dPfx^t+ z;*@yJ8WjC7q5bdPGRsJx>L%4e_h){sI;W?Fa&MFKzQOk9yQ|u$dx5%>uHT<;4t&aj^#VFzE~btl(>+m441U< zN}^5~*1UBdhxu_*cBbu|afr1$c+s$>pM#N1tW$F1%#+s-;p)@28gC9{h;tp<=B`2} zrX#)ZG*}&L><@FkR?|QFJ)~ft;dx0KoDlkov|lgjEHTquj!8T_2x?u?GPLDaSak@O zI}~!S^Gj5NGroYKfAZfJ)(lWkdd9akJ!dEBx10%eYvB#{hqeborV7n7}1vDPEw=&=+6oexu*0&GhyLXuWk;2Lzhl`cI~6 zzld$Te_=mxA>4l`r&Uo`L<;UvcmL6MTX>$Pt2)vZ@y<#Z)E5?OWz1^XvuQ5)5GnvsOKC}- z#%gY(x#w$J-!1OkaHb2I&D8iRk>>Qo=MGD}QwKDk!llFPa-x8W*uUPK&ryWdK$_j3 zsg5TPks3^o*87(D|MalYF_r?oHW3d$Ndugs5vj*X*cvYHko!>L*ZFrTK?v-ZTiS@V z4jZmY4XOI2@)I$Xtwjyh=}D?6VI@U*0lf;w@ck2>Pao+9n}B zWKJ%+fJQ9>Lzva znsi`wbJyeVA)b8=_$7J{bcx)fJ`1(t1C+f%l{ZMS+P{yIaY%}~erXLo$P9g`=;~6V z=SO)$<@95zupe(#`pMCDeynFq(zI-CuIcFcn>kNw!8RE)EcBnRIBoFbjzS=9p5K%o z>W(_lbbZJ&+J*2v${LUHdAS!)w7kL#W@_;455iJ@B-neVyr>~M%0oGS-F*`C@7M0# zGN-orxs2Tlf8r*YG?$Qy?%eL0MwtrZ6l;hS|COR;)sbL{)Lq1~pm$lmNScfOO4#CrZFunvf`|T@s|6c(e=RZ5At&bisS8N zCx~p=oPJ*J_I87N#4oLXlyo{dU1QBy4C~{lcFWSt4@|={zewgDmavz(0vLu^9iu0d z6mO`RunY-W%pAC}4{L2>@tf%{TCQUyUno>KUqXSX8TV-e4i;R-QQ(Z#;1y zrQxgUG&t>5Cr4I7um?szd`ll;IJF(CKMF^Z4bvB!*Bi*nk*jkO*uMBaau{kvqxrdz|^X z;u1#IAiFD`uv;*lsUFrBV>=xBoR$*-b~ptBCAbxtLOV6ZZ$aGL$RhGHWWZKW6b63U z>KVQ0$=HdNc(YQb#zR)!f75vj8LBEqEvVZF1$3DzBfJV}3L~8TxEN}WZbq= z?-$R=|K~C1e?%>aLpa-RgjKvWr3lD@u0n!8QY6MuN!*kyQD6BCqs@a zMYXC;b(N(%H}+}H?DXtLAF1t4((7<#d=G4HM%f0I2v4EfKEbx5$zkZ59$alZLYR18 z3aI5X>ALtI2Gw+ycj3(gA(y`j%W^vLzp1%@MDQ_O8!Xt1E>*Z7qu^xsuaV5THsU#A z_%=TvJ=mlcd}SI+==pR~UQyP*dyli2`GS1Krx_0?fbITOJ!0~xa&3GFSS)-SmwN)1c2Y1l$}Qf+Y=a2@ zywz?Xk)_w2@XCw5qvV7~?z%)DM`y7w(*6f+!RnZWAF|swaNlCqWGK3Y{%MCO>UL~m zGT!D1In}dHoBw^4(dwD9xq0RFBI9iVP~{up8}ks#w#Y7{d{P0^Y*630ygJorW;i{9 zcwwhalfGL5Au#&{mC5quZOqCOFcbx_N?t_?x>&Zm&}|9tcmzyskXm)km~u^+zd)Hx zd<#;1<*RbEG%w(Dlt*RxjCW?r>-p#MEJ0~hD{4Zh%o=zezvc}3DecuL+TG*BOV4QQ z3dL9v@)lGXq&w+lDh%)ccOFDj?U!vI!{w^d=E+g2(8-eP?Z&$IIDMzQpoV0PD@M@EFU?Kh-*H8u809g% z`YK!e9Wbd{4Cg1)dHu|axzUA0!RqonXztBQ9-jTdM8#%CZ1q{E-v9Bnts}Z?mZ>d0 z&NvWVSV%QSK&OS;R^lDYJiB5u9(=dqppd4&=h7ozUusgNe1XW!z^|zqJ3mX9tKMG6 ze70b=$)|O2V)WN%M_)YadUWFWwU`fTzs@aM+XK(9K zL@%8*UjNAyLi+IU{~sz~tkIwehNld&U*<2;6s-MY3JV+JdFtC}0tP3k5>`xNs+T2X zHr4IPEYL*ZdGiVCg-E_FW2fOGWU-UyuSN5CKVUMM@4ZJ)7Z;x-DY)IlQy|8=did1u zE$SkaTL|`USaV&lT9YE)L>z!)c!VZu0B@P@=fz?_kGOr^DT+LLL0TYpMIM5P76xr=HzYq6K`gV zRvb&``b_;*Hr;*UzPM059s~&6A2DhMxcPU6QIx;cM6+ZOuJqqx{jddLDKWiDZ^@th z^@lkQhP_fG3knCerFBMoNR{yRAFN$p40XBDbwuW~Z>P&fd3@mL_UwwSHB69w3k^}F zMr{h>sxq%*d0o}p$=5-8Sq8|RyCcr}%x?Ir%~%$-qbjDeg%U;lWPM=_*6HaeBR08^ zSn3ZhJ-RQu+*oaDM7x1=b29?^=q5OFf;ZqL{R56F%K>a1`?llv6|y(TS0*Bf9OaQ592zOou=z*A zX?11~q*nsF9V4!c`LVnA#A6*2Yac?D!YAL;ATgmWkgp6I7ebaN37d&NZuuYXUR!$J z5FzG+oX&boM6J%N%2svrXhx~VH}4qTJ5r@m7BJCg%|c#((r?9afnGU8Rj^|@g;VBd z*3R;9Gh=y@UkbvdMCruO>7OLO)()Lf9Rp4oHA0<+>rBgPbnE*B3+C^rEYUQq9#p2C z0LJ~^Dqs6p?%j{?xyl^<8j;8rS}3{Mu%T(o_R)tw=1v%M0Nw9HD(EL@d}(I=X;!sq*i8b)+4|jW6GCG z@pu+SR$d5ZZ%fmRaE9(d+c7+Ae?18$G0@C*hJdCDGgtK| zc|s9+OH@zEm?*0t9!`p6y2>&~=l5?9p7Q#McSWuwyov&w8flTtm)4#n{kd$qupq@P zlE=T_y&=)tPjMl<_hHi-eEcMCT+29CrN3AF5ORETmI0ikpwGejk)MQ@y zguPS2R5wcD?wxxWh4ZEaG{KDT#0#(J&#wMbHPTy`R_%vqUuxFo$L(TA8yOyMG&u#3 z6cziMjB^@Ey@BwJbvSd*p~DOYAF@zaqgiw8N)BOVf{suxYe8nOOQxeP=D` zekF_|rAK9r!-H!OTq(MVj^lT!JLMW&63vAK^B1HlTP!6U?K6Ao@Uf|9Q5_Xtpriyb zHK|Vi@jGsC-|e^q?7pMvp+}l=zG;kAiWNh{^3d_135NE9_HezU8h1WviQ=z!B`W6a za#YKTwIogVTFtqLyr+9|#FGo2dQjX}0|@=%!;Tywl1gV7PAeY=Peq%iCZ@X>UdE2q z=aI~i-^j;Pu9gn&YU<)pCMi1hc$97(V#X5N5P7v4u5^2>AYiAmdhV~eT~s}GU!O&# zvEJ8ZIbo-~OYS`O^+&{NpW1yzx^PokScv@+=RbyujMSO8Q@Mjjd4vtZe_Y@~o-*1F zvuhqI!q@8zwNZq|jt z8=h(fV6;t$Zt%w3=%gP8V)GGr<5K(Be0<_p^$2T^@uHK@ZC@)dHQY%BlbJjqCmrP5 ztJu$7mooIWOy^$q_w)rb5)l)q6j!T6R32-q{v$4>pP2SGVR!`Pa;7KhT5^5{oi?aP z6TNfjet6D^e+bq9cKStok|rNDF(4p&I~*Une+or;FFFp%_lH(lKh2+T&*(`Xhs$Jh zpVJ6MVEkaqA=W=9SMU#xc71AM4yBKsOp*}A82XtPV^*%|IWsk&V><4qXT`3M)KH_A zp}}I?q3Q~JGtXctmySDT#RCa(hgLSd<5K`sr1vd6fSuOCV;FlX0HqwydUgtv!bnqz zqsn@j-BzJQC@HnL(H;1=9dC}4gjDRAR8nx;>hk9X=w}H}={u4q){%nfdoGxP@*fDm z)&+I%n!G%mlKJQ^)H_20d`#}&{@ZJ20(=ESykfaTB+BY-0Zl|mInT_ZzboV-ZT^vZ zPDPGmoiP;E523iC2_UiHftmWnAQmBOMpA|9y`)$N{UkAo1gwA)KU7ltaKVM=ul)=k zeL77>Oos{zMcptQNAN4uqB@WM&*oD7x4keDMU*LC{ z+>xOAsz3e-8aU%6kq$;F$9K=J7+Qm|d$LS+0?6nK1;mt4DL8smTo*13Hq$pJBJuQw$( zC%;bXj&$FB^@LRjZTf-K0@f43>>-iffPdhGe7A~zgcpX*8>Irj2G>+%R5TChJ&Qlk zoN=WfIO2b|uwPNg3kge9ZDTzITK-OX>wZxcrF>a?Bw;m8t>g0Uh_b?R3~|$swKcY0 zf7!&^*CBcq3N98~r#kZ6Xtqhsbaytb{OhDJ8734_{N12m#r#ijD%-pc$2JWbcM6&} zvP2vW(CGj3`JbH~Ru(4Ag^L=FAz_XNB*?H=qOnbuwMBGwd8cXEgy%B1T6Dkplw;Ka z{1Zc~I+oaGSemlLEQ^u(Q!rtt3dc956=@K%1lS+q-y!gNmJal+cu? z>P?;*X2e}0wuL+2bym7bJ#wMnN(}^BS$WU7f%w=o5V8c2O<(x047NeBDe1UHuoMhq zODSkR08U@ave`lU`NrPm;8kpNKP47#hNC2yDU`-IJ480*Uk^Se#_9r6n(Pf)7rUfQ z1bEM}S^B};8|N@`z^MGr@nji>rwv*$-@MOfbtX1w-n&%;)0UOj55g?fs=<`5{>t$D z|0)jzBQ*g0?NitvV!K>t{E)+Sls^YSn;>=o6-@mReAg-lTp_-i5qxGtR=J+4#?om052xlw%()X$YTl<_v$L zP|EUyco(hg2H25@J zX+KOmsSn8%_JqTh0ClqMA)wjv0J3v~#7RlG6R{qqvt8-H5ht7U+N zA|%2;hBhr!w8nYPxAF_UdHR0RnFsiFYY5FR`ncAQ7AVNA>L*0A+(^`{ zJ97x^`H+Pu$lukp%H5!a!$52haww3rLu98z62Li1zyGEf+eFVhSYK@4k>pyGGRcQ+ zP1=ml>gQQ!g2qQhd^2j^bGW&oVJ}o|WmWMMz!?x*@`h(^GQqJ%wc0)DDiXaRb(tXY*=_lGY*iQ5=?GR<+ z@pYR_n5awiqE{J)%kpCjTH3Y=LszwKEwa&8i(TaS{kCp6SirU&8};UT!W93Mq99bA z@2ZO-D6VM?;}5Wj*mxMHSA|nxwoNyb;At>;(c2jJtytxt$62!&QV5>1vBw#vX~)-? z)gAo(C1PsVa>Dqt4NASx(GBo=vDNlVMg|CI<^PeB{VYH%LLU=t#PoP&Z`gKJ(MfJf zl#73h=?QPzMf3G*_luwK+t<`d#-;j&m_={X?-RYDY0#^D(XDgZduq@IR-@Y4f=Q5* zGs}=urCoW&Pr?FuvE5?7it|d-ohii~5S_%{=4J96({t-`p)6<&}b<~&Cj(k3Ig|ACe92rh3I*d4U-NjI}T0oS2s$gV9PS} z8c*dx?3(|KDLlwc^0$I8{A|6omE4Y2?~8}JBXmCVUV4B3bKgf_HM#bvS{MujBkQZ$ z)yFPRb8{N%nQ^6nTw1cU!aE#C(?->j;MDYb7VcYdG)etzy;Y`_e|zX*e1e7iv)+eJ z!_NW$r&^9^9(pOXxei>ohJ@d$j+$%@(M1iSh(zr#@8{WFioZp1zKKlb1au>biNhoZ z!xg~ui++Pk76rlD9AaJSO3<3;mv@dYsqWNoq=XH= with Serializable< ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png'); static const exolix = ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png'); - static const thorChain = - ExchangeProviderDescription(title: 'ThorChain' , raw: 8, image: 'assets/images/thorchain.png'); - static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); + static const thorChain = + ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); + static const quantex = + ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -43,10 +44,12 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< return trocador; case 6: return exolix; - case 8: - return thorChain; case 7: return all; + case 8: + return thorChain; + case 9: + return quantex; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/quantex_exchange_provider.dart b/lib/exchange/provider/quantex_exchange_provider.dart new file mode 100644 index 000000000..9ab7fbb55 --- /dev/null +++ b/lib/exchange/provider/quantex_exchange_provider.dart @@ -0,0 +1,252 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; +import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:http/http.dart'; + +class QuantexExchangeProvider extends ExchangeProvider { + QuantexExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static final List _notSupported = [ + ...(CryptoCurrency.all + .where((element) => ![ + CryptoCurrency.btc, + CryptoCurrency.sol, + CryptoCurrency.eth, + CryptoCurrency.ltc, + CryptoCurrency.ada, + CryptoCurrency.bch, + CryptoCurrency.usdt, + CryptoCurrency.bnb, + CryptoCurrency.xmr, + ].contains(element)) + .toList()) + ]; + + static final markup = secrets.quantexExchangeMarkup; + + static const apiAuthority = 'api.myquantex.com'; + static const getRate = '/api/swap/get-rate'; + static const getCoins = '/api/swap/get-coins'; + static const createOrder = '/api/swap/create-order'; + + @override + String get title => 'Quantex'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => false; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.quantex; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits({ + required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode, + }) async { + try { + final uri = Uri.https(apiAuthority, getCoins); + final response = await get(uri); + + final responseJSON = json.decode(response.body) as Map; + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final coinsInfo = responseJSON['data'] as List; + + for (var coin in coinsInfo) { + if (coin['id'].toString().toUpperCase() == _normalizeCurrency(from)) { + return Limits( + min: double.parse(coin['min'].toString()), + max: double.parse(coin['max'].toString()), + ); + } + } + + // coin not found: + return Limits(min: 0, max: 0); + } catch (e) { + print(e.toString()); + return Limits(min: 0, max: 0); + } + } + + @override + Future fetchRate({ + required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount, + }) async { + try { + if (amount == 0) return 0.0; + + final headers = {}; + final params = {}; + final body = { + 'coin_send': _normalizeCurrency(from), + 'coin_receive': _normalizeCurrency(to), + 'ref': 'cake', + }; + + final uri = Uri.https(apiAuthority, getRate, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map; + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final data = responseBody['data'] as Map; + double rate = double.parse(data['price'].toString()); + return rate; + } catch (e) { + print("error fetching rate: ${e.toString()}"); + return 0.0; + } + } + + @override + Future createTrade({ + required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll, + }) async { + try { + final headers = {}; + final params = {}; + var body = { + 'coin_send': _normalizeCurrency(request.fromCurrency), + 'coin_receive': _normalizeCurrency(request.toCurrency), + 'amount_send': request.fromAmount, + 'recipient': request.toAddress, + 'ref': 'cake', + 'markup': markup, + }; + + String? fromNetwork = _networkFor(request.fromCurrency); + String? toNetwork = _networkFor(request.toCurrency); + if (fromNetwork != null) body['coin_send_network'] = fromNetwork; + if (toNetwork != null) body['coin_receive_network'] = toNetwork; + + final uri = Uri.https(apiAuthority, createOrder, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map; + + if (response.statusCode == 400 || responseBody["success"] == false) { + final error = responseBody['errors'][0]['msg'] as String; + throw TradeNotCreatedException(description, description: error); + } + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseData = responseBody['data'] as Map; + + return Trade( + id: responseData["order_id"] as String, + inputAddress: responseData["server_address"] as String, + amount: request.fromAmount, + from: request.fromCurrency, + to: request.toCurrency, + provider: description, + createdAt: DateTime.now(), + state: TradeState.created, + payoutAddress: request.toAddress, + isSendAll: isSendAll, + ); + } catch (e) { + print("error creating trade: ${e.toString()}"); + throw TradeNotCreatedException(description, description: e.toString()); + } + } + + @override + Future findTradeById({required String id}) async { + try { + final headers = {}; + final params = {}; + var body = { + 'order_id': id, + }; + + final uri = Uri.https(apiAuthority, createOrder, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map; + + if (response.statusCode == 400 || responseBody["success"] == false) { + final error = responseBody['errors'][0]['msg'] as String; + throw TradeNotCreatedException(description, description: error); + } + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseData = responseBody['data'] as Map; + final fromCurrency = responseData['coin_send'] as String; + final from = CryptoCurrency.fromString(fromCurrency); + final toCurrency = responseData['coin_receive'] as String; + final to = CryptoCurrency.fromString(toCurrency); + final inputAddress = responseData['server_address'] as String; + final status = responseData['status'] as String; + final state = TradeState.deserialize(raw: status); + final response_id = responseData['order_id'] as String; + final expectedSendAmount = responseData['amount_send'] as String; + + return Trade( + id: response_id, + from: from, + to: to, + provider: description, + inputAddress: inputAddress, + amount: expectedSendAmount, + state: state, + ); + } catch (e) { + print("error getting trade: ${e.toString()}"); + throw TradeNotFoundException( + id, + provider: description, + description: e.toString(), + ); + } + } + + String _normalizeCurrency(CryptoCurrency currency) { + switch (currency) { + default: + return currency.title.toUpperCase(); + } + } + + String? _networkFor(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.usdt: + return "USDT_ERC20"; + case CryptoCurrency.bnb: + return "BNB_BSC"; + default: + return null; + } + } +} diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 2c58a96f4..0a196835e 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -28,6 +28,7 @@ class TradeState extends EnumerableItem with Serializable { TradeState(raw: 'waitingAuthorization', title: 'Waiting authorization'); static const failed = TradeState(raw: 'failed', title: 'Failed'); static const completed = TradeState(raw: 'completed', title: 'Completed'); + static const expired = TradeState(raw: 'expired', title: 'Expired'); static const settling = TradeState(raw: 'settling', title: 'Settlement in progress'); static const settled = TradeState(raw: 'settled', title: 'Settlement completed'); static const wait = TradeState(raw: 'wait', title: 'Waiting'); @@ -39,7 +40,33 @@ class TradeState extends EnumerableItem with Serializable { static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging'); static const sending = TradeState(raw: 'sending', title: 'Sending'); static const success = TradeState(raw: 'success', title: 'Success'); + static TradeState deserialize({required String raw}) { + + switch (raw) { + case '1': + return unpaid; + case '2': + return paidUnconfirmed; + case '3': + return sending; + case '4': + return confirmed; + case '5': + case '6': + return exchanging; + case '7': + return sending; + case '8': + return complete; + case '9': + return expired; + case '10': + return underpaid; + case '11': + return failed; + } + switch (raw) { case 'NOT_FOUND': return notFound; diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 94fec2fa2..c5ce7a591 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; @@ -48,6 +49,9 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.exolix: _provider = ExolixExchangeProvider(); break; + case ExchangeProviderDescription.quantex: + _provider = QuantexExchangeProvider(); + break; case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index e5533f48a..1560a4be0 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -30,6 +30,7 @@ import 'package:cake_wallet/exchange/limits_state.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; @@ -157,6 +158,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ThorChainExchangeProvider(tradesStore: trades), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), + QuantexExchangeProvider(), ]; @observable diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 1da322778..c88008982 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; @@ -56,6 +57,9 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; + case ExchangeProviderDescription.quantex: + _provider = QuantexExchangeProvider(); + break; } _updateItems(); @@ -80,6 +84,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://exolix.com/transaction/${trade.id}'; case ExchangeProviderDescription.thorChain: return 'https://track.ninerealms.com/${trade.id}'; + case ExchangeProviderDescription.quantex: + return 'https://myquantex.com/send/${trade.id}'; } return null; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 89e4de12d..9559e83b3 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -38,6 +38,7 @@ class SecretKey { SecretKey('walletConnectProjectId', () => ''), SecretKey('moralisApiKey', () => ''), SecretKey('ankrApiKey', () => ''), + SecretKey('quantexExchangeMarkup', () => ''), ]; static final evmChainsSecrets = [ From fbecc5c994710f5429f443e5dc16a882b4dd4fbd Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Wed, 15 May 2024 00:54:39 +0100 Subject: [PATCH 09/25] fix: Tron Timeout Issue (#1448) * fix: Tron timeout issue * fix: Revert * fix: Tron transaction expiry error --- cw_tron/lib/tron_client.dart | 4 ++-- scripts/android/pubspec_gen.sh | 2 +- scripts/ios/app_config.sh | 2 +- scripts/macos/app_config.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index 92684cebc..8eca02af6 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -208,7 +208,7 @@ class TronClient { TransactionContract(type: contract.contractType, parameter: parameter); // Set the transaction expiration time (maximum 24 hours) - final expireTime = DateTime.now().add(const Duration(hours: 24)); + final expireTime = DateTime.now().add(const Duration(minutes: 30)); // Create a raw transaction TransactionRaw rawTransaction = TransactionRaw( @@ -369,7 +369,7 @@ class TronClient { TransactionContract(type: contract.contractType, parameter: parameter); // Set the transaction expiration time (maximum 24 hours) - final expireTime = DateTime.now().add(const Duration(hours: 24)); + final expireTime = DateTime.now().add(const Duration(minutes: 30)); // Create a raw transaction TransactionRaw rawTransaction = TransactionRaw( diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index d238052fe..bc7985506 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 9f59d6632..ab7fbd422 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -28,7 +28,7 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" ;; $HAVEN) diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index bd1417c4b..a1143bb12 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -31,7 +31,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml From 82391d4a5b1b96961d43ed17a3b4853e8062f219 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 17 May 2024 08:15:19 -0500 Subject: [PATCH 10/25] Generic fixes (#1454) * Handle Bluetooth is disabled * Allow signMessage using ledger_bitcoin * Fix desktop wallet selection dropdown --- cw_bitcoin/lib/bitcoin_wallet.dart | 42 ++++++++++++++----- cw_bitcoin/pubspec.lock | 6 +-- cw_bitcoin/pubspec.yaml | 2 +- .../connect_device/connect_device_page.dart | 38 ++++++++++------- .../desktop_wallet_selection_dropdown.dart | 33 ++++++++------- 5 files changed, 77 insertions(+), 44 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index f96b0e4da..b02116541 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,23 +1,24 @@ -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:convert/convert.dart'; +import 'dart:convert'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:convert/convert.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_bitcoin/psbt_transaction_builder.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_flutter/ledger_flutter.dart'; import 'package:mobx/mobx.dart'; -import 'package:flutter/foundation.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; -import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; -import 'package:bip39/bip39.dart' as bip39; part 'bitcoin_wallet.g.dart'; @@ -215,4 +216,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); return BtcTransaction.fromRaw(hex.encode(rawHex)); } + + @override + Future signMessage(String message, {String? address = null}) async { + if (walletInfo.isHardwareWallet) { + final addressEntry = address != null + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) + : null; + final index = addressEntry?.index ?? 0; + final isChange = addressEntry?.isHidden == true ? 1 : 0; + final accountPath = walletInfo.derivationInfo?.derivationPath; + final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; + + final signature = await _bitcoinLedgerApp! + .signMessage(_ledgerDevice!, message: ascii.encode(message), signDerivationPath: derivationPath); + return base64Encode(signature); + } + + return super.signMessage(message, address: address); + } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index e6f0b34dd..7690c9c85 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -475,10 +475,10 @@ packages: description: path: "." ref: HEAD - resolved-ref: b6ed573cbeb57d5f0d39dfe4254bf9d15b620ab6 - url: "https://github.com/cake-tech/ledger-bitcoin.git" + resolved-ref: f819d37e235e239c315e93856abbf5e5d3b71dab + url: "https://github.com/cake-tech/ledger-bitcoin" source: git - version: "0.0.1" + version: "0.0.2" ledger_flutter: dependency: "direct main" description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 84254b5b5..265d2f9a2 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: ledger_flutter: ^1.0.1 ledger_bitcoin: git: - url: https://github.com/cake-tech/ledger-bitcoin.git + url: https://github.com/cake-tech/ledger-bitcoin dev_dependencies: flutter_test: diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index dfb32beba..80b396a34 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +// import 'package:cake_wallet/src/screens/connect_device/debug_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -78,15 +79,13 @@ class ConnectDevicePageBodyState extends State { @override void initState() { super.initState(); - Future.delayed( - Duration(seconds: 1), - () => _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))), - ); - // _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); + WidgetsBinding.instance.addPostFrameCallback((_) { + _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); - if (Platform.isAndroid) { - _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); - } + if (Platform.isAndroid) { + _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); + } + }); } @override @@ -103,14 +102,16 @@ class ConnectDevicePageBodyState extends State { } Future _refreshBleDevices() async { - final isBleEnabled = await Permission.bluetooth.serviceStatus.isEnabled; - - setState(() => bleIsEnabled = isBleEnabled); - - if (isBleEnabled) { - _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))); + try { + _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))) + ..onError((e) { + throw e as Exception; + }); + setState(() => bleIsEnabled = true); _bleRefreshTimer?.cancel(); _bleRefreshTimer = null; + } catch (e) { + setState(() => bleIsEnabled = false); } } @@ -142,6 +143,15 @@ class ConnectDevicePageBodyState extends State { textAlign: TextAlign.center, ), ), + // DeviceTile( + // onPressed: () => Navigator.of(context).push( + // MaterialPageRoute( + // builder: (BuildContext context) => DebugDevicePage(), + // ), + // ), + // title: "Debug Ledger", + // leading: imageLedger, + // ), if (!bleIsEnabled) Padding( padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 18ab9d030..adf0840c9 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -117,22 +117,25 @@ class _DesktopWalletSelectionDropDownState extends State( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).change_wallet_alert_title, - alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name), - leftButtonText: S.of(context).cancel, - rightButtonText: S.of(context).change, - actionLeftButton: () => Navigator.of(dialogContext).pop(false), - actionRightButton: () => Navigator.of(dialogContext).pop(true)); - }) ?? - false; - if (confirmed) { - await _loadWallet(selectedWallet); - } + WidgetsBinding.instance.addPostFrameCallback((_) async { + final confirmed = await showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).change_wallet_alert_title, + alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name), + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).change, + actionLeftButton: () => Navigator.of(dialogContext).pop(false), + actionRightButton: () => Navigator.of(dialogContext).pop(true)); + }) ?? + false; + + if (confirmed) { + await _loadWallet(selectedWallet); + } + }); } Image _imageFor({required WalletType type}) { From aeff8972eac5128b41aeda99e0fdc3a1aef563fd Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 21 May 2024 05:12:12 +0300 Subject: [PATCH 11/25] Update electrum_wallet.dart (#1443) --- cw_bitcoin/lib/electrum_wallet.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 783eb10d7..b899744a6 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -877,7 +877,6 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; - coin.bitcoinAddressRecord.balance += coinInfo.value; } else { _addCoinInfo(coin); } From f846f91e5f78726515a2eb66580ec5c880f93cb7 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 21 May 2024 05:26:49 +0300 Subject: [PATCH 12/25] Maybe Fix Bluetooth Popup (#1458) Co-authored-by: Konstantin Ullrich --- lib/di.dart | 3 ++- lib/src/screens/connect_device/connect_device_page.dart | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 92bf25c9a..69168451f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -311,7 +311,6 @@ Future setup({ getIt.registerFactory>(() => _powNodeSource, instanceName: Node.boxName + "pow"); getIt.registerSingleton(AuthenticationStore()); - getIt.registerSingleton(LedgerViewModel()); getIt.registerSingleton(WalletListStore()); getIt.registerSingleton(NodeListStoreBase.instance); getIt.registerSingleton(settingsStore); @@ -336,6 +335,8 @@ Future setup({ getIt.registerSingleton( AnonpayTransactionsStore(anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource)); + getIt.registerLazySingleton(() => LedgerViewModel()); + final secretStore = await SecretStoreBase.load(getIt.get()); getIt.registerSingleton(secretStore); diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 80b396a34..a482b1c41 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -// import 'package:cake_wallet/src/screens/connect_device/debug_device_page.dart'; +import 'package:cake_wallet/src/screens/connect_device/debug_device_page.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -105,7 +105,7 @@ class ConnectDevicePageBodyState extends State { try { _bleRefresh = ledger.scan().listen((device) => setState(() => bleDevices.add(device))) ..onError((e) { - throw e as Exception; + throw e.toString(); }); setState(() => bleIsEnabled = true); _bleRefreshTimer?.cancel(); From 7b5204fdaabd3b1f56646ba9d5fe72f22a67759c Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 22 May 2024 04:18:04 +0300 Subject: [PATCH 13/25] Generic enhancements (#1456) * minor enhancement * show camera consent at least once, even if camera permission is granted already * minor enhancement [skip ci] * Add taproot derivation path to electrum_derivations.dart * remove unused import [skip ci] * Initialize Ledger only when necessary * Update app versions --- assets/text/Release_Notes.txt | 4 +- cw_bitcoin/lib/electrum_derivations.dart | 6 +++ cw_bitcoin/lib/litecoin_wallet.dart | 1 - lib/bitcoin/cw_bitcoin.dart | 3 ++ lib/buy/moonpay/moonpay_provider.dart | 24 +++++---- lib/di.dart | 25 +++++---- lib/entities/preferences_key.dart | 1 + lib/src/screens/buy/webview_page.dart | 14 +++-- lib/src/screens/send/send_page.dart | 7 ++- .../dashboard/dashboard_view_model.dart | 52 +++++++++++-------- lib/view_model/send/send_view_model.dart | 4 +- scripts/android/app_env.sh | 8 +-- scripts/ios/app_env.sh | 8 +-- scripts/macos/app_env.sh | 8 +-- 14 files changed, 98 insertions(+), 67 deletions(-) diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index faad67777..483f249cf 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1 +1,3 @@ -Bug fixes and generic enhancements \ No newline at end of file +Add Tron wallet +Hardware wallets enhancements +Bug fixes \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_derivations.dart b/cw_bitcoin/lib/electrum_derivations.dart index 19d444a41..7e19f1cb4 100644 --- a/cw_bitcoin/lib/electrum_derivations.dart +++ b/cw_bitcoin/lib/electrum_derivations.dart @@ -28,6 +28,12 @@ Map> electrum_derivations = { description: "Standard BIP84 native segwit", scriptType: "p2wpkh", ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/86'/0'/0'", + description: "Standard BIP86 Taproot", + scriptType: "p2tr", + ), DerivationInfo( derivationType: DerivationType.bip39, derivationPath: "m/0'", diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 2ffb99405..9cc2072ca 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -14,7 +14,6 @@ import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/litecoin_network.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bip39/bip39.dart' as bip39; part 'litecoin_wallet.g.dart'; diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 56c9c7dff..082e9b22f 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -371,6 +371,9 @@ class CWBitcoin extends Bitcoin { case "p2wpkh-p2sh": address = generateP2SHAddress(hd: hd, network: network); break; + case "p2tr": + address = generateP2TRAddress(hd: hd, network: network); + break; default: continue; } diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 53f018d57..59251e064 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -281,17 +281,19 @@ class MoonPayProvider extends BuyProvider { throw Exception('Could not launch URL'); } } catch (e) { - await showDialog( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: 'MoonPay', - alertContent: 'The MoonPay service is currently unavailable: $e', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: 'MoonPay', + alertContent: 'The MoonPay service is currently unavailable: $e', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } } } diff --git a/lib/di.dart b/lib/di.dart index 69168451f..c039b68f4 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -330,8 +330,7 @@ Future setup({ getIt.registerSingleton( ExchangeTemplateStore(templateSource: _exchangeTemplates)); getIt.registerSingleton( - YatStore(appStore: getIt.get(), secureStorage: getIt.get()) - ..init()); + YatStore(appStore: getIt.get(), secureStorage: getIt.get())..init()); getIt.registerSingleton( AnonpayTransactionsStore(anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource)); @@ -626,7 +625,7 @@ Future setup({ getIt.get(), getIt.get(), _transactionDescriptionBox, - getIt.get(), + getIt.get().wallet!.isHardwareWallet ? getIt.get() : null, ), ); @@ -833,10 +832,14 @@ Future setup({ isSelected: isSelected)); getIt.registerFactory(() => RobinhoodBuyProvider( - wallet: getIt.get().wallet!, ledgerVM: getIt.get())); + wallet: getIt.get().wallet!, + ledgerVM: + getIt.get().wallet!.isHardwareWallet ? getIt.get() : null)); getIt.registerFactory(() => DFXBuyProvider( - wallet: getIt.get().wallet!, ledgerVM: getIt.get())); + wallet: getIt.get().wallet!, + ledgerVM: + getIt.get().wallet!.isHardwareWallet ? getIt.get() : null)); getIt.registerFactory(() => MoonPayProvider( settingsStore: getIt.get().settingsStore, @@ -937,9 +940,9 @@ Future setup({ (derivations, _) => WalletRestoreChooseDerivationViewModel(derivationInfos: derivations)); getIt.registerFactoryParam, void>( - (credentials, _) => + (derivations, _) => WalletRestoreChooseDerivationPage(getIt.get( - param1: credentials, + param1: derivations, ))); getIt.registerFactoryParam( @@ -987,8 +990,8 @@ Future setup({ getIt.registerFactory(() => BackupPage(getIt.get())); - getIt.registerFactory(() => - EditBackupPasswordViewModel(getIt.get(), getIt.get())); + getIt.registerFactory( + () => EditBackupPasswordViewModel(getIt.get(), getIt.get())); getIt.registerFactory(() => EditBackupPasswordPage(getIt.get())); @@ -1036,8 +1039,8 @@ Future setup({ getIt.registerFactory(() => SupportPage(getIt.get())); - getIt.registerFactory(() => SupportChatPage(getIt.get(), - secureStorage: getIt.get())); + getIt.registerFactory(() => + SupportChatPage(getIt.get(), secureStorage: getIt.get())); getIt.registerFactory(() => SupportOtherLinksPage(getIt.get())); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 79177178c..d184c74b1 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -65,6 +65,7 @@ class PreferencesKey { static const lookupsUnstoppableDomains = 'looks_up_unstoppable_domain'; static const lookupsOpenAlias = 'looks_up_open_alias'; static const lookupsENS = 'looks_up_ens'; + static const showCameraConsent = 'show_camera_consent'; static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; diff --git a/lib/src/screens/buy/webview_page.dart b/lib/src/screens/buy/webview_page.dart index 0af5952c4..d6f171f36 100644 --- a/lib/src/screens/buy/webview_page.dart +++ b/lib/src/screens/buy/webview_page.dart @@ -1,10 +1,14 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/permission_handler.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class WebViewPage extends BasePage { WebViewPage(this._title, this._url); @@ -42,8 +46,9 @@ class WebViewPageBodyState extends State { ), initialUrlRequest: URLRequest(url: WebUri.uri(widget.uri)), onPermissionRequest: (controller, request) async { - bool permissionGranted = await Permission.camera.status == PermissionStatus.granted; - if (!permissionGranted) { + final sharedPrefs = getIt.get(); + + if (sharedPrefs.getBool(PreferencesKey.showCameraConsent) ?? true) { final bool userConsent = await showPopUp( context: context, builder: (BuildContext context) { @@ -65,9 +70,12 @@ class WebViewPageBodyState extends State { ); } - permissionGranted = await Permission.camera.request().isGranted; + sharedPrefs.setBool(PreferencesKey.showCameraConsent, false); } + bool permissionGranted = + await PermissionHandler.checkPermission(Permission.camera, context); + return PermissionResponse( resources: request.resources, action: permissionGranted diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index d9b74869f..65c5a07f6 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -35,7 +35,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; -import 'package:cw_core/crypto_currency.dart'; import 'package:url_launcher/url_launcher.dart'; class SendPage extends BasePage { @@ -373,17 +372,17 @@ class SendPage extends BasePage { } if (sendViewModel.wallet.isHardwareWallet) { - if (!sendViewModel.ledgerViewModel.isConnected) { + if (!sendViewModel.ledgerViewModel!.isConnected) { await Navigator.of(context).pushNamed(Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: sendViewModel.walletType, onConnectDevice: (BuildContext context, _) { - sendViewModel.ledgerViewModel.setLedger(sendViewModel.wallet); + sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); Navigator.of(context).pop(); }, )); } else { - sendViewModel.ledgerViewModel.setLedger(sendViewModel.wallet); + sendViewModel.ledgerViewModel!.setLedger(sendViewModel.wallet); } } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 24d02d68d..bd8ae6dda 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -329,6 +329,8 @@ abstract class DashboardViewModelBase with Store { .toList(); } + bool get hasBuyProviders => ProvidersHelper.getAvailableBuyProviderTypes(wallet.type).isNotEmpty; + List get availableSellProviders { final providerTypes = ProvidersHelper.getAvailableSellProviderTypes(wallet.type); return providerTypes @@ -338,6 +340,8 @@ abstract class DashboardViewModelBase with Store { .toList(); } + bool get hasSellProviders => ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty; + bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup; @action @@ -350,13 +354,13 @@ abstract class DashboardViewModelBase with Store { bool hasExchangeAction; @computed - bool get isEnabledBuyAction => !settingsStore.disableBuy && availableBuyProviders.isNotEmpty; + bool get isEnabledBuyAction => !settingsStore.disableBuy && hasBuyProviders; @observable bool hasBuyAction; @computed - bool get isEnabledSellAction => !settingsStore.disableSell && availableSellProviders.isNotEmpty; + bool get isEnabledSellAction => !settingsStore.disableSell && hasSellProviders; @observable bool hasSellAction; @@ -495,34 +499,38 @@ abstract class DashboardViewModelBase with Store { void setSyncAll(bool value) => settingsStore.currentSyncAll = value; Future> checkAffectedWallets() async { - // await load file - final vulnerableSeedsString = await rootBundle - .loadString('assets/text/cakewallet_weak_bitcoin_seeds_hashed_sorted_version1.txt'); - final vulnerableSeeds = vulnerableSeedsString.split("\n"); + try { + // await load file + final vulnerableSeedsString = await rootBundle + .loadString('assets/text/cakewallet_weak_bitcoin_seeds_hashed_sorted_version1.txt'); + final vulnerableSeeds = vulnerableSeedsString.split("\n"); - final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); + final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); - List affectedWallets = []; - for (var walletInfo in walletInfoSource.values) { - if (walletInfo.type == WalletType.bitcoin) { - final password = await keyService.getWalletPassword(walletName: walletInfo.name); - final path = await pathForWallet(name: walletInfo.name, type: walletInfo.type); - final jsonSource = await read(path: path, password: password); - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String?; + List affectedWallets = []; + for (var walletInfo in walletInfoSource.values) { + if (walletInfo.type == WalletType.bitcoin) { + final password = await keyService.getWalletPassword(walletName: walletInfo.name); + final path = await pathForWallet(name: walletInfo.name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String?; - if (mnemonic == null) continue; + if (mnemonic == null) continue; - final hash = await Cryptography.instance.sha256().hash(utf8.encode(mnemonic)); - final seedSha = bytesToHex(hash.bytes); + final hash = await Cryptography.instance.sha256().hash(utf8.encode(mnemonic)); + final seedSha = bytesToHex(hash.bytes); - if (vulnerableSeeds.contains(seedSha)) { - affectedWallets.add(walletInfo.name); + if (vulnerableSeeds.contains(seedSha)) { + affectedWallets.add(walletInfo.name); + } } } - } - return affectedWallets; + return affectedWallets; + } catch (_) { + return []; + } } Future getServicesStatus() async { diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index a633d3982..4b30ecef3 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -269,7 +269,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final SendTemplateViewModel sendTemplateViewModel; final BalanceViewModel balanceViewModel; final ContactListViewModel contactListViewModel; - final LedgerViewModel ledgerViewModel; + final LedgerViewModel? ledgerViewModel; final FiatConversionStore _fiatConversationStore; final Box transactionDescriptionBox; @@ -365,7 +365,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final errorCode = e.errorCode.toRadixString(16); final fallbackMsg = e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode"; - final errorMsg = ledgerViewModel.interpretErrorCode(errorCode) ?? fallbackMsg; + final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg; state = FailureState(errorMsg); } else { diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index b520a3179..cc4219930 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.13.2" -MONERO_COM_BUILD_NUMBER=88 +MONERO_COM_VERSION="1.14.0" +MONERO_COM_BUILD_NUMBER=89 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.16.2" -CAKEWALLET_BUILD_NUMBER=212 +CAKEWALLET_VERSION="4.17.0" +CAKEWALLET_BUILD_NUMBER=213 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index f70963745..82da5fa41 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.13.2" -MONERO_COM_BUILD_NUMBER=86 +MONERO_COM_VERSION="1.14.0" +MONERO_COM_BUILD_NUMBER=87 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.16.2" -CAKEWALLET_BUILD_NUMBER=240 +CAKEWALLET_VERSION="4.17.0" +CAKEWALLET_BUILD_NUMBER=244 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index afdac3e6c..196d49eaf 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.3.2" -MONERO_COM_BUILD_NUMBER=19 +MONERO_COM_VERSION="1.4.0" +MONERO_COM_BUILD_NUMBER=20 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.9.2" -CAKEWALLET_BUILD_NUMBER=73 +CAKEWALLET_VERSION="1.10.0" +CAKEWALLET_BUILD_NUMBER=76 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From dfba5be2cdf9ba291d8b8a294cfe10311d966028 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 22 May 2024 04:19:33 +0300 Subject: [PATCH 14/25] bump android build number [skip ci] --- scripts/android/app_env.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index cc4219930..fe0d1a524 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -23,7 +23,7 @@ MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.17.0" -CAKEWALLET_BUILD_NUMBER=213 +CAKEWALLET_BUILD_NUMBER=215 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" @@ -72,4 +72,4 @@ export APP_ANDROID_VERSION export APP_ANDROID_BUILD_NUMBER export APP_ANDROID_BUNDLE_ID export APP_ANDROID_PACKAGE -export APP_ANDROID_SCHEME \ No newline at end of file +export APP_ANDROID_SCHEME From 461fe1c85990e94d4e18329c2ea9813ce4dbb197 Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 23 May 2024 16:21:51 +0100 Subject: [PATCH 15/25] fix: Desktop resize bug (#1461) * fix: Desktop resize bug * Change default spl tokens * Minor enhancements [skip ci] * minor enhancements [skip ci] --------- Co-authored-by: OmarHatem --- cw_solana/lib/default_spl_tokens.dart | 87 ++++++++-------- lib/src/screens/dashboard/dashboard_page.dart | 98 ++++++++++++------- lib/utils/responsive_layout_util.dart | 6 +- 3 files changed, 114 insertions(+), 77 deletions(-) diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart index 7acad78e0..21b5ef79d 100644 --- a/cw_solana/lib/default_spl_tokens.dart +++ b/cw_solana/lib/default_spl_tokens.dart @@ -19,30 +19,6 @@ class DefaultSPLTokens { mint: 'usdcsol', enabled: true, ), - SPLToken( - name: 'Wrapped Ethereum (Sollet)', - symbol: 'soETH', - mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk', - decimal: 6, - mint: 'soEth', - iconPath: 'assets/images/eth_icon.png', - ), - SPLToken( - name: 'Wrapped SOL', - symbol: 'WSOL', - mintAddress: 'So11111111111111111111111111111111111111112', - decimal: 9, - mint: 'WSOL', - iconPath: 'assets/images/sol_icon.png', - ), - SPLToken( - name: 'Wrapped Bitcoin (Sollet)', - symbol: 'BTC', - mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E', - decimal: 6, - mint: 'btcsol', - iconPath: 'assets/images/btc.png', - ), SPLToken( name: 'Bonk', symbol: 'Bonk', @@ -50,21 +26,7 @@ class DefaultSPLTokens { decimal: 5, mint: 'Bonk', iconPath: 'assets/images/bonk_icon.png', - ), - SPLToken( - name: 'Helium Network Token', - symbol: 'HNT', - mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux', - decimal: 8, - mint: 'hnt', - iconPath: 'assets/images/hnt_icon.png', - ), - SPLToken( - name: 'Pyth Network', - symbol: 'PYTH', - mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', - decimal: 6, - mint: 'pyth', + enabled: true, ), SPLToken( name: 'Raydium', @@ -73,6 +35,51 @@ class DefaultSPLTokens { decimal: 6, mint: 'ray', iconPath: 'assets/images/ray_icon.png', + enabled: true, + ), + SPLToken( + name: 'Wrapped Ethereum (Sollet)', + symbol: 'soETH', + mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk', + decimal: 6, + mint: 'soEth', + iconPath: 'assets/images/eth_icon.png', + enabled: false, + ), + SPLToken( + name: 'Wrapped SOL', + symbol: 'WSOL', + mintAddress: 'So11111111111111111111111111111111111111112', + decimal: 9, + mint: 'WSOL', + iconPath: 'assets/images/sol_icon.png', + enabled: false, + ), + SPLToken( + name: 'Wrapped Bitcoin (Sollet)', + symbol: 'BTC', + mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E', + decimal: 6, + mint: 'btcsol', + iconPath: 'assets/images/btc.png', + enabled: false, + ), + SPLToken( + name: 'Helium Network Token', + symbol: 'HNT', + mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux', + decimal: 8, + mint: 'hnt', + iconPath: 'assets/images/hnt_icon.png', + enabled: false, + ), + SPLToken( + name: 'Pyth Network', + symbol: 'PYTH', + mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', + decimal: 6, + mint: 'pyth', + enabled: false, ), SPLToken( name: 'GMT', @@ -81,6 +88,7 @@ class DefaultSPLTokens { decimal: 6, mint: 'ray', iconPath: 'assets/images/gmt_icon.png', + enabled: false, ), SPLToken( name: 'AvocadoCoin', @@ -89,6 +97,7 @@ class DefaultSPLTokens { decimal: 8, mint: 'avdo', iconPath: 'assets/images/avdo_icon.png', + enabled: false, ), ]; diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 8d9dc9829..ec97d191f 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -36,7 +36,7 @@ import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart' import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; -class DashboardPage extends StatelessWidget { +class DashboardPage extends StatefulWidget { DashboardPage({ required this.bottomSheetService, required this.balancePage, @@ -50,43 +50,71 @@ class DashboardPage extends StatelessWidget { final WalletAddressListViewModel addressListViewModel; @override - Widget build(BuildContext context) { - final screenHeight = MediaQuery.of(context).size.height; - return Scaffold( - body: Builder( - builder: (_) { - final dashboardPageView = RefreshIndicator( - displacement: screenHeight * 0.1, - onRefresh: () async => await dashboardViewModel.refreshDashboard(), - child: SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: Container( - height: screenHeight, - child: _DashboardPageView( - balancePage: balancePage, - bottomSheetService: bottomSheetService, - dashboardViewModel: dashboardViewModel, - addressListViewModel: addressListViewModel, - ), - ), - ), - ); + State createState() => _DashboardPageState(); +} - if (DeviceInfo.instance.isDesktop) { - if (responsiveLayoutUtil.screenWidth > - ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) { - return getIt.get(); - } else { - return dashboardPageView; - } - } else if (responsiveLayoutUtil.shouldRenderMobileUI) { - return dashboardPageView; - } else { - return getIt.get(); - } - }, +class _DashboardPageState extends State { + @override + void initState() { + super.initState(); + + bool isMobileLayout = + responsiveLayoutUtil.screenWidth < ResponsiveLayoutUtilBase.kMobileThreshold; + + reaction((_) => responsiveLayoutUtil.screenWidth, (screenWidth) { + // Check if it was previously in mobile layout, and now changing to desktop + if (isMobileLayout && + screenWidth > ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) { + setState(() { + isMobileLayout = false; + }); + } + + // Check if it was previously in desktop layout, and now changing to mobile + if (!isMobileLayout && + screenWidth <= ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) { + setState(() { + isMobileLayout = true; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + Widget dashboardChild; + + final dashboardPageView = RefreshIndicator( + displacement: responsiveLayoutUtil.screenHeight * 0.1, + onRefresh: () async => await widget.dashboardViewModel.refreshDashboard(), + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Container( + height: responsiveLayoutUtil.screenHeight, + child: _DashboardPageView( + balancePage: widget.balancePage, + bottomSheetService: widget.bottomSheetService, + dashboardViewModel: widget.dashboardViewModel, + addressListViewModel: widget.addressListViewModel, + ), + ), ), ); + + if (DeviceInfo.instance.isDesktop) { + if (responsiveLayoutUtil.screenWidth > + ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) { + dashboardChild = getIt.get(); + } else { + dashboardChild = dashboardPageView; + } + } else if (responsiveLayoutUtil.shouldRenderMobileUI) { + dashboardChild = dashboardPageView; + } else { + dashboardChild = getIt.get(); + } + + return Scaffold(body: dashboardChild); } } diff --git a/lib/utils/responsive_layout_util.dart b/lib/utils/responsive_layout_util.dart index 86a4a3776..aa30b3f07 100644 --- a/lib/utils/responsive_layout_util.dart +++ b/lib/utils/responsive_layout_util.dart @@ -6,7 +6,7 @@ part 'responsive_layout_util.g.dart'; class _ResponsiveLayoutUtil = ResponsiveLayoutUtilBase with _$_ResponsiveLayoutUtil; abstract class ResponsiveLayoutUtilBase with Store, WidgetsBindingObserver { - static const double _kMobileThreshold = 550; + static const double kMobileThreshold = 550; static const double kDesktopMaxWidthConstraint = 400; static const double kDesktopMaxDashBoardWidthConstraint = 900; static const double kPopupWidth = 400; @@ -42,13 +42,13 @@ abstract class ResponsiveLayoutUtilBase with Store, WidgetsBindingObserver { @computed bool get shouldRenderMobileUI { - return (screenWidth <= _kMobileThreshold) || + return (screenWidth <= kMobileThreshold) || (orientation == Orientation.portrait && screenWidth < screenHeight) || (orientation == Orientation.landscape && screenWidth < screenHeight); } bool get shouldRenderTabletUI { - return screenWidth > _kMobileThreshold && screenWidth < kDesktopMaxDashBoardWidthConstraint; + return screenWidth > kMobileThreshold && screenWidth < kDesktopMaxDashBoardWidthConstraint; } } From 7d0720b21d0492951e508614384bef77808d591c Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 23 May 2024 19:43:05 +0300 Subject: [PATCH 16/25] Generic enhancements (#1464) * Fix wallet not considering seed length for Tron, Polygon and Solana * Change Trocador to be prioritized if same rate [skip ci] --- lib/core/wallet_creation_service.dart | 21 ++++++++++++++++++- .../exchange/exchange_view_model.dart | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index a55e9ee3f..1fa50a6be 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -58,7 +58,7 @@ class WalletCreationService { checkIfExists(credentials.name); final password = generateWalletPassword(); credentials.password = password; - if (type == WalletType.bitcoinCash || type == WalletType.ethereum) { + if (_hasSeedPhraseLengthOption) { credentials.seedPhraseLength = settingsStore.seedPhraseLength.value; } await keyService.saveWalletPassword(password: password, walletName: credentials.name); @@ -72,6 +72,25 @@ class WalletCreationService { return wallet; } + bool get _hasSeedPhraseLengthOption { + switch (type) { + case WalletType.ethereum: + case WalletType.bitcoinCash: + case WalletType.polygon: + case WalletType.solana: + case WalletType.tron: + return true; + case WalletType.monero: + case WalletType.none: + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.haven: + case WalletType.nano: + case WalletType.banano: + return false; + } + } + Future restoreFromKeys(WalletCredentials credentials, {bool? isTestnet}) async { checkIfExists(credentials.name); final password = generateWalletPassword(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 1560a4be0..5e0443bf8 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -154,11 +154,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ChangeNowExchangeProvider(settingsStore: _settingsStore), SideShiftExchangeProvider(), SimpleSwapExchangeProvider(), - TrocadorExchangeProvider( - useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ThorChainExchangeProvider(tradesStore: trades), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), QuantexExchangeProvider(), + TrocadorExchangeProvider( + useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ]; @observable From 0dc53895c60f8e8317fc7dd4b29d1c93a49258ba Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Sat, 25 May 2024 10:57:30 -0400 Subject: [PATCH 17/25] Fix connection leak when service bulletin is disabled (#1465) * Fix connection leak when service bulletin disabled * Update dashboard_view_model.dart --------- Co-authored-by: Omar Hatem --- .../dashboard/dashboard_view_model.dart | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index bd8ae6dda..f438c5724 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -535,24 +535,29 @@ abstract class DashboardViewModelBase with Store { Future getServicesStatus() async { try { - final res = await http.get(Uri.parse("https://service-api.cakewallet.com/v1/active-notices")); + if (isEnabledBulletinAction) { + final res = await http.get(Uri.parse("https://service-api.cakewallet.com/v1/active-notices")); - if (res.statusCode < 200 || res.statusCode >= 300) { - throw res.body; + if (res.statusCode < 200 || res.statusCode >= 300) { + throw res.body; + } + + final oldSha = sharedPreferences.getString(PreferencesKey.serviceStatusShaKey); + + final hash = await Cryptography.instance.sha256().hash(utf8.encode(res.body)); + final currentSha = bytesToHex(hash.bytes); + + final hasUpdates = oldSha != currentSha; + + return ServicesResponse.fromJson( + json.decode(res.body) as Map, + hasUpdates, + currentSha, + ); } - - final oldSha = sharedPreferences.getString(PreferencesKey.serviceStatusShaKey); - - final hash = await Cryptography.instance.sha256().hash(utf8.encode(res.body)); - final currentSha = bytesToHex(hash.bytes); - - final hasUpdates = oldSha != currentSha; - - return ServicesResponse.fromJson( - json.decode(res.body) as Map, - hasUpdates, - currentSha, - ); + else { + return ServicesResponse([], false, ''); + } } catch (_) { return ServicesResponse([], false, ''); } From 8c1206ea04ac64ff43a35ffac7d3a73ec564d7fb Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Sat, 25 May 2024 16:59:29 +0200 Subject: [PATCH 18/25] Hide "Show Seed" Option on Hardware Wallets (#1463) --- lib/di.dart | 14 ++++++------ .../settings/security_backup_page.dart | 22 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index c039b68f4..6a97cf62c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -14,6 +14,7 @@ import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/key_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; @@ -26,10 +27,6 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; -import 'package:cake_wallet/view_model/link_view_model.dart'; -import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; -import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -124,6 +121,7 @@ import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; +import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; @@ -147,6 +145,7 @@ import 'package:cake_wallet/store/templates/send_template_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -179,6 +178,7 @@ import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model. import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; @@ -222,6 +222,7 @@ import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; @@ -233,7 +234,6 @@ import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/core/secure_storage.dart'; import 'buy/dfx/dfx_buy_provider.dart'; import 'core/totp_request_details.dart'; @@ -795,8 +795,8 @@ Future setup({ getIt.registerFactory(() => ConnectionSyncPage(getIt.get())); - getIt.registerFactory( - () => SecurityBackupPage(getIt.get(), getIt.get())); + getIt.registerFactory(() => SecurityBackupPage(getIt.get(), + getIt.get(), getIt.get().wallet!.isHardwareWallet)); getIt.registerFactory(() => PrivacyPage(getIt.get())); diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index 1f0f58ad4..470f49190 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -9,14 +9,13 @@ import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; -import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class SecurityBackupPage extends BasePage { - SecurityBackupPage(this._securitySettingsViewModel, this._authService); + SecurityBackupPage(this._securitySettingsViewModel, this._authService, [this._isHardwareWallet = false]); final AuthService _authService; @@ -25,20 +24,23 @@ class SecurityBackupPage extends BasePage { final SecuritySettingsViewModel _securitySettingsViewModel; + final bool _isHardwareWallet; + @override Widget body(BuildContext context) { return Container( padding: EdgeInsets.only(top: 10), child: Column(mainAxisSize: MainAxisSize.min, children: [ - SettingsCellWithArrow( - title: S.current.show_keys, - handler: (_) => _authService.authenticateAction( - context, - route: Routes.showKeys, - conditionToDetermineIfToUse2FA: _securitySettingsViewModel - .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + if (!_isHardwareWallet) + SettingsCellWithArrow( + title: S.current.show_keys, + handler: (_) => _authService.authenticateAction( + context, + route: Routes.showKeys, + conditionToDetermineIfToUse2FA: + _securitySettingsViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), ), - ), SettingsCellWithArrow( title: S.current.create_backup, handler: (_) => _authService.authenticateAction( From 058522caf1f1250c61ff9ad0408cae0e8e1e2150 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sun, 26 May 2024 18:09:39 +0300 Subject: [PATCH 19/25] Fix Contact page reaction excuted more than once (#1467) --- lib/src/screens/contact/contact_page.dart | 29 ++++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/lib/src/screens/contact/contact_page.dart b/lib/src/screens/contact/contact_page.dart index 099be41d5..7dea970ec 100644 --- a/lib/src/screens/contact/contact_page.dart +++ b/lib/src/screens/contact/contact_page.dart @@ -46,6 +46,7 @@ class ContactPage extends BasePage { final TextEditingController _nameController; final TextEditingController _currencyTypeController; final TextEditingController _addressController; + bool _isEffectsApplied = false; @override Widget body(BuildContext context) { @@ -53,15 +54,7 @@ class ContactPage extends BasePage { color: Theme.of(context).extension()!.detailsTitlesColor, height: 8); - reaction((_) => contactViewModel.state, (ExecutionState state) { - if (state is FailureState) { - _onContactSavingFailure(context, state.error); - } - - if (state is ExecutedSuccessfullyState) { - _onContactSavedSuccessfully(context); - } - }); + _setEffects(context); return Observer( builder: (_) => ScrollableWithBottomSection( @@ -177,4 +170,22 @@ class ContactPage extends BasePage { void _onContactSavedSuccessfully(BuildContext context) => Navigator.of(context).pop(); + + void _setEffects(BuildContext context) { + if (_isEffectsApplied) { + return; + } + + _isEffectsApplied = true; + + reaction((_) => contactViewModel.state, (ExecutionState state) { + if (state is FailureState) { + _onContactSavingFailure(context, state.error); + } + + if (state is ExecutedSuccessfullyState) { + _onContactSavedSuccessfully(context); + } + }); + } } From 24f6541fa696c756c12aa2d9afa94d13aff6a0a8 Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 28 May 2024 14:32:36 +0100 Subject: [PATCH 20/25] CW-647-Birdpay-Trc20 (#1469) * fix: Desktop resize bug * fix: Birdpay working for trc20 and adjust transaction fee currency * fix: Filter logic * fix: Solana token balance not fully displaying * fix: Minor enhancements --- cw_core/lib/crypto_currency.dart | 2 +- cw_solana/lib/solana_balance.dart | 4 ++-- lib/view_model/send/send_view_model.dart | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index fabc84051..fb702eca1 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -253,7 +253,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static CryptoCurrency fromString(String name, {CryptoCurrency? walletCurrency}) { try { return CryptoCurrency.all.firstWhere((element) => - element.title.toLowerCase() == name && + element.title.toLowerCase() == name.toLowerCase() && (element.tag == null || element.tag == walletCurrency?.title || element.tag == walletCurrency?.tag)); diff --git a/cw_solana/lib/solana_balance.dart b/cw_solana/lib/solana_balance.dart index b1f0ef153..e88a510dd 100644 --- a/cw_solana/lib/solana_balance.dart +++ b/cw_solana/lib/solana_balance.dart @@ -15,8 +15,8 @@ class SolanaBalance extends Balance { String _balanceFormatted() { String stringBalance = balance.toString(); - if (stringBalance.toString().length >= 6) { - stringBalance = stringBalance.substring(0, 6); + if (stringBalance.toString().length >= 12) { + stringBalance = stringBalance.substring(0, 12); } return stringBalance; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 4b30ecef3..534e501dd 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -139,8 +139,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor String get pendingTransactionFeeFiatAmount { try { if (pendingTransaction != null) { - final currency = - isEVMCompatibleChain(walletType) ? wallet.currency : selectedCryptoCurrency; + final currency = pendingTransactionFeeCurrency(walletType); final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[currency]!, cryptoAmount: pendingTransaction!.feeFormatted); @@ -153,6 +152,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } + CryptoCurrency pendingTransactionFeeCurrency(WalletType type) { + switch (type) { + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.tron: + case WalletType.solana: + return wallet.currency; + default: + return selectedCryptoCurrency; + } + } + FiatCurrency get fiat => _settingsStore.fiatCurrency; TransactionPriority get transactionPriority { From faa49d21e8ab99303d756e98447c908669141515 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 28 May 2024 15:32:48 +0200 Subject: [PATCH 21/25] Potential fix for missing polyseeds in the future (#1468) --- cw_monero/ios/Classes/monero_api.cpp | 11 ++ cw_monero/lib/api/signatures.dart | 4 + cw_monero/lib/api/types.dart | 4 + cw_monero/lib/api/wallet.dart | 145 ++++++++++++++------------- 4 files changed, 94 insertions(+), 70 deletions(-) diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 87be785ac..01a8d9a51 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -399,6 +399,7 @@ extern "C" return false; } + wallet->store(std::string(path)); change_current_wallet(wallet); return true; } @@ -464,6 +465,16 @@ extern "C" return strdup(get_current_wallet()->address(account_index, address_index).c_str()); } + char *get_cache_attribute(char *name) + { + return strdup(get_current_wallet()->getCacheAttribute(std::string(name)).c_str()); + } + + bool set_cache_attribute(char *name, char *value) + { + get_current_wallet()->setCacheAttribute(std::string(name), std::string(value)); + return true; + } const char *seed() { diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index bba7b5528..40f338c8c 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -154,3 +154,7 @@ typedef freeze_coin = Void Function(Int32 index); typedef thaw_coin = Void Function(Int32 index); typedef sign_message = Pointer Function(Pointer message, Pointer address); + +typedef get_cache_attribute = Pointer Function(Pointer name); + +typedef set_cache_attribute = Int8 Function(Pointer name, Pointer value); diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index 87d8a153e..6b36ab5e3 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -154,3 +154,7 @@ typedef FreezeCoin = void Function(int); typedef ThawCoin = void Function(int); typedef SignMessage = Pointer Function(Pointer, Pointer); + +typedef GetCacheAttribute = Pointer Function(Pointer); + +typedef SetCacheAttribute = int Function(Pointer, Pointer); diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index ffa5fe13b..448c661e6 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -1,26 +1,24 @@ import 'dart:async'; import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:cw_monero/api/structs/ut8_box.dart'; + import 'package:cw_monero/api/convert_utf8_to_string.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; +import 'package:cw_monero/api/monero_api.dart'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/structs/ut8_box.dart'; +import 'package:cw_monero/api/types.dart'; +import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; int _boolToInt(bool value) => value ? 1 : 0; -final getFileNameNative = moneroApi - .lookup>('get_filename') - .asFunction(); +final getFileNameNative = + moneroApi.lookup>('get_filename').asFunction(); -final getSeedNative = - moneroApi.lookup>('seed').asFunction(); +final getSeedNative = moneroApi.lookup>('seed').asFunction(); -final getAddressNative = moneroApi - .lookup>('get_address') - .asFunction(); +final getAddressNative = + moneroApi.lookup>('get_address').asFunction(); final getFullBalanceNative = moneroApi .lookup>('get_full_balance') @@ -38,41 +36,34 @@ final getNodeHeightNative = moneroApi .lookup>('get_node_height') .asFunction(); -final isConnectedNative = moneroApi - .lookup>('is_connected') - .asFunction(); +final isConnectedNative = + moneroApi.lookup>('is_connected').asFunction(); -final setupNodeNative = moneroApi - .lookup>('setup_node') - .asFunction(); +final setupNodeNative = + moneroApi.lookup>('setup_node').asFunction(); -final startRefreshNative = moneroApi - .lookup>('start_refresh') - .asFunction(); +final startRefreshNative = + moneroApi.lookup>('start_refresh').asFunction(); final connecToNodeNative = moneroApi .lookup>('connect_to_node') .asFunction(); final setRefreshFromBlockHeightNative = moneroApi - .lookup>( - 'set_refresh_from_block_height') + .lookup>('set_refresh_from_block_height') .asFunction(); final setRecoveringFromSeedNative = moneroApi - .lookup>( - 'set_recovering_from_seed') + .lookup>('set_recovering_from_seed') .asFunction(); -final storeNative = - moneroApi.lookup>('store').asFunction(); +final storeNative = moneroApi.lookup>('store').asFunction(); final setPasswordNative = moneroApi.lookup>('set_password').asFunction(); -final setListenerNative = moneroApi - .lookup>('set_listener') - .asFunction(); +final setListenerNative = + moneroApi.lookup>('set_listener').asFunction(); final getSyncingHeightNative = moneroApi .lookup>('get_syncing_height') @@ -83,8 +74,7 @@ final isNeededToRefreshNative = moneroApi .asFunction(); final isNewTransactionExistNative = moneroApi - .lookup>( - 'is_new_transaction_exist') + .lookup>('is_new_transaction_exist') .asFunction(); final getSecretViewKeyNative = moneroApi @@ -107,9 +97,8 @@ final closeCurrentWalletNative = moneroApi .lookup>('close_current_wallet') .asFunction(); -final onStartupNative = moneroApi - .lookup>('on_startup') - .asFunction(); +final onStartupNative = + moneroApi.lookup>('on_startup').asFunction(); final rescanBlockchainAsyncNative = moneroApi .lookup>('rescan_blockchain') @@ -123,13 +112,19 @@ final setTrustedDaemonNative = moneroApi .lookup>('set_trusted_daemon') .asFunction(); -final trustedDaemonNative = moneroApi - .lookup>('trusted_daemon') - .asFunction(); +final trustedDaemonNative = + moneroApi.lookup>('trusted_daemon').asFunction(); -final signMessageNative = moneroApi - .lookup>('sign_message') - .asFunction(); +final signMessageNative = + moneroApi.lookup>('sign_message').asFunction(); + +final getCacheAttributeNative = moneroApi + .lookup>('get_cache_attribute') + .asFunction(); + +final setCacheAttributeNative = moneroApi + .lookup>('set_cache_attribute') + .asFunction(); int getSyncingHeight() => getSyncingHeightNative(); @@ -144,11 +139,9 @@ String getSeed() => convertUTF8ToString(pointer: getSeedNative()); String getAddress({int accountIndex = 0, int addressIndex = 0}) => convertUTF8ToString(pointer: getAddressNative(accountIndex, addressIndex)); -int getFullBalance({int accountIndex = 0}) => - getFullBalanceNative(accountIndex); +int getFullBalance({int accountIndex = 0}) => getFullBalanceNative(accountIndex); -int getUnlockedBalance({int accountIndex = 0}) => - getUnlockedBalanceNative(accountIndex); +int getUnlockedBalance({int accountIndex = 0}) => getUnlockedBalanceNative(accountIndex); int getCurrentHeight() => getCurrentHeightNative(); @@ -187,7 +180,7 @@ bool setupNodeSync( passwordPointer, _boolToInt(useSSL), _boolToInt(isLightWallet), - socksProxyAddressPointer, + socksProxyAddressPointer, errorMessagePointer) != 0; @@ -202,8 +195,7 @@ bool setupNodeSync( } if (!isSetupNode) { - throw SetupWalletException( - message: convertUTF8ToString(pointer: errorMessagePointer)); + throw SetupWalletException(message: convertUTF8ToString(pointer: errorMessagePointer)); } return isSetupNode; @@ -213,8 +205,7 @@ void startRefreshSync() => startRefreshNative(); Future connectToNode() async => connecToNodeNative() != 0; -void setRefreshFromBlockHeight({required int height}) => - setRefreshFromBlockHeightNative(height); +void setRefreshFromBlockHeight({required int height}) => setRefreshFromBlockHeightNative(height); void setRecoveringFromSeed({required bool isRecovery}) => setRecoveringFromSeedNative(_boolToInt(isRecovery)); @@ -230,7 +221,7 @@ void setPasswordSync(String password) { final errorMessagePointer = calloc(); final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0; calloc.free(passwordPointer); - + if (!changed) { final message = errorMessagePointer.ref.getValue(); calloc.free(errorMessagePointer); @@ -242,24 +233,19 @@ void setPasswordSync(String password) { void closeCurrentWallet() => closeCurrentWalletNative(); -String getSecretViewKey() => - convertUTF8ToString(pointer: getSecretViewKeyNative()); +String getSecretViewKey() => convertUTF8ToString(pointer: getSecretViewKeyNative()); -String getPublicViewKey() => - convertUTF8ToString(pointer: getPublicViewKeyNative()); +String getPublicViewKey() => convertUTF8ToString(pointer: getPublicViewKeyNative()); -String getSecretSpendKey() => - convertUTF8ToString(pointer: getSecretSpendKeyNative()); +String getSecretSpendKey() => convertUTF8ToString(pointer: getSecretSpendKeyNative()); -String getPublicSpendKey() => - convertUTF8ToString(pointer: getPublicSpendKeyNative()); +String getPublicSpendKey() => convertUTF8ToString(pointer: getPublicSpendKeyNative()); class SyncListener { - SyncListener(this.onNewBlock, this.onNewTransaction) - : _cachedBlockchainHeight = 0, - _lastKnownBlockHeight = 0, - _initialSyncHeight = 0; - + SyncListener(this.onNewBlock, this.onNewTransaction) + : _cachedBlockchainHeight = 0, + _lastKnownBlockHeight = 0, + _initialSyncHeight = 0; void Function(int, int, double) onNewBlock; void Function() onNewTransaction; @@ -281,8 +267,7 @@ class SyncListener { _cachedBlockchainHeight = 0; _lastKnownBlockHeight = 0; _initialSyncHeight = 0; - _updateSyncInfoTimer ??= - Timer.periodic(Duration(milliseconds: 1200), (_) async { + _updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async { if (isNewTransactionExist()) { onNewTransaction(); } @@ -321,8 +306,8 @@ class SyncListener { void stop() => _updateSyncInfoTimer?.cancel(); } -SyncListener setListeners(void Function(int, int, double) onNewBlock, - void Function() onNewTransaction) { +SyncListener setListeners( + void Function(int, int, double) onNewBlock, void Function() onNewTransaction) { final listener = SyncListener(onNewBlock, onNewTransaction); setListenerNative(); return listener; @@ -364,7 +349,7 @@ Future setupNode( bool isLightWallet = false}) => compute, void>(_setupNodeSync, { 'address': address, - 'login': login , + 'login': login, 'password': password, 'useSSL': useSSL, 'isLightWallet': isLightWallet, @@ -397,3 +382,23 @@ String signMessage(String message, {String address = ""}) { return signature; } + +bool setCacheAttribute(String name, String value) { + final namePointer = name.toNativeUtf8(); + final valuePointer = value.toNativeUtf8(); + + final isSet = setCacheAttributeNative(namePointer, valuePointer); + calloc.free(namePointer); + calloc.free(valuePointer); + + return isSet == 1; +} + +String getCacheAttribute(String name) { + final namePointer = name.toNativeUtf8(); + + final value = convertUTF8ToString(pointer: getCacheAttributeNative(namePointer)); + calloc.free(namePointer); + + return value; +} From 96b9b60f50bf115cc7ec1e88472ae7f007d80df8 Mon Sep 17 00:00:00 2001 From: Rafael Date: Wed, 29 May 2024 11:43:48 -0300 Subject: [PATCH 22/25] Cw 453 (#1306) * feat: rebase btc-addr-types, migrate to bitcoin_base * feat: allow scanning elect-rs using get_tweaks * feat: scanning and adding addresses working with getTweaks, add btc SP address type * chore: pubspec.lock * chore: pubspec.lock * fix: scan when switching, fix multiple unspents in same tx * fix: initial scan * fix: initial scan * fix: scanning issues * fix: sync, storing silent unspents * chore: deps * fix: label issues, clear spent utxo * chore: deps * fix: build * fix: missing types * feat: new electrs API & changes, fixes for last block scanning * feat: Scan Silent Payments homepage toggle * chore: build configure * feat: generic fixes, testnet UI improvements, useSSL on bitcoin nodes * fix: invalid Object in sendData * feat: improve addresses page & address book displays * feat: silent payments labeled addresses disclaimer * fix: missing i18n * chore: print * feat: single block scan, rescan by date working for btc mainnet * feat: new cake features page replace market page, move sp scan toggle, auto switch node pop up alert * feat: delete silent addresses * fix: red dot in non ssl nodes * fix: inconsistent connection states, fix tx history * fix: tx & balance displays, cpfp sending * feat: new rust lib * chore: node path * fix: check node based on network * fix: missing txcount from addresses * style: padding in feature page cards * fix: restore not getting all wallet addresses by type * fix: auto switch node broken * fix: silent payment txs not being restored * feat: change scanning to subscription model, sync improvements * fix: scan re-subscription * fix: default nodes * fix: improve scanning by date, fix single block scan * refactor: common function for input tx selection * fix: nodes & build * fix: send all with multiple outs * refactor: unchanged file * Update pr_test_build.yml * chore: upgrade * chore: merge changes * refactor: unchanged files [skip ci] * fix: scan fixes, add date, allow sending while scanning * feat: sync fixes, sp settings * feat: fix resyncing * fix: date from height logic, status disconnected & chain tip get * fix: params * feat: electrum migration if using cake electrum * fix nodes update versions * re-enable tron * update sp_scanner to work on iOS [skip ci] * fix: wrong socket for old electrum nodes * Fix unchecked wallet type call * fix: double balance * feat: node domain * fix: menu name * fix: update tip on set scanning * fix: connection switching back and forth * feat: check if node is electrs, and supports sp * chore: fix build * minor enhancements * fixes and enhancements * solve conflicts with main * fix: status toggle * minor enhancement * Monero.com fixes * update sp_scanner to include windows and linux --------- Co-authored-by: Omar Hatem --- .github/workflows/pr_test_build.yml | 2 +- assets/images/cards.svg | 65 + assets/images/tbtc.png | Bin 0 -> 3983 bytes assets/text/Release_Notes.txt | 5 +- cw_bitcoin/lib/bitcoin_address_record.dart | 148 ++- .../lib/bitcoin_receive_page_option.dart | 23 + cw_bitcoin/lib/bitcoin_unspent.dart | 61 +- cw_bitcoin/lib/bitcoin_wallet.dart | 66 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 4 +- cw_bitcoin/lib/bitcoin_wallet_service.dart | 42 +- cw_bitcoin/lib/electrum.dart | 68 +- cw_bitcoin/lib/electrum_balance.dart | 11 +- .../lib/electrum_transaction_history.dart | 26 +- cw_bitcoin/lib/electrum_transaction_info.dart | 85 +- cw_bitcoin/lib/electrum_wallet.dart | 1184 +++++++++++++---- cw_bitcoin/lib/electrum_wallet_addresses.dart | 239 +++- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 23 +- cw_bitcoin/lib/exceptions.dart | 6 +- cw_bitcoin/lib/litecoin_wallet.dart | 1 - cw_bitcoin/lib/litecoin_wallet_addresses.dart | 1 - .../lib/pending_bitcoin_transaction.dart | 5 + cw_bitcoin/pubspec.lock | 128 +- cw_bitcoin/pubspec.yaml | 14 +- .../lib/src/bitcoin_cash_wallet.dart | 1 - .../src/bitcoin_cash_wallet_addresses.dart | 1 - cw_bitcoin_cash/pubspec.yaml | 10 +- cw_core/lib/crypto_currency.dart | 4 +- cw_core/lib/currency_for_wallet_type.dart | 5 +- cw_core/lib/exceptions.dart | 7 +- cw_core/lib/get_height_by_date.dart | 54 + cw_core/lib/node.dart | 8 +- cw_core/lib/sync_status.dart | 39 +- cw_core/lib/unspent_coins_info.dart | 8 +- cw_core/lib/unspent_transaction_output.dart | 3 +- cw_core/lib/wallet_base.dart | 2 +- cw_core/lib/wallet_info.dart | 35 +- cw_core/lib/wallet_type.dart | 5 +- cw_shared_external/pubspec.lock | 147 -- cw_tron/pubspec.yaml | 10 +- howto-build-android.md | 20 +- ios/Podfile.lock | 6 + lib/bitcoin/cw_bitcoin.dart | 155 ++- lib/core/address_validator.dart | 6 +- lib/core/sync_status_title.dart | 22 +- lib/di.dart | 25 +- lib/entities/default_settings_migration.dart | 49 +- lib/entities/preferences_key.dart | 2 + lib/main.dart | 2 +- lib/reactions/check_connection.dart | 21 +- .../on_wallet_sync_status_change.dart | 8 +- lib/router.dart | 5 + lib/routes.dart | 1 + lib/src/screens/dashboard/dashboard_page.dart | 10 +- .../desktop_dashboard_actions.dart | 8 +- .../desktop_wallet_selection_dropdown.dart | 77 +- .../screens/dashboard/pages/address_page.dart | 5 + .../screens/dashboard/pages/balance_page.dart | 246 +++- ...lace_page.dart => cake_features_page.dart} | 62 +- .../dashboard/widgets/menu_widget.dart | 6 + .../present_receive_option_picker.dart | 141 +- lib/src/screens/receive/receive_page.dart | 250 ++-- .../screens/receive/widgets/address_cell.dart | 35 +- lib/src/screens/rescan/rescan_page.dart | 63 +- lib/src/screens/send/send_page.dart | 5 + .../settings/connection_sync_page.dart | 4 +- .../settings/silent_payments_settings.dart | 50 + .../unspent_coins_list_page.dart | 1 + .../widgets/unspent_coins_list_item.dart | 28 +- .../screens/wallet_list/wallet_list_page.dart | 11 +- lib/src/widgets/blockchain_height_widget.dart | 66 +- lib/src/widgets/dashboard_card_widget.dart | 67 +- lib/src/widgets/setting_actions.dart | 10 + lib/store/settings_store.dart | 29 +- .../contact_list/contact_list_view_model.dart | 16 +- .../dashboard/balance_view_model.dart | 3 + .../dashboard/cake_features_view_model.dart | 16 + .../dashboard/dashboard_view_model.dart | 36 +- .../dashboard/market_place_view_model.dart | 17 - lib/view_model/rescan_view_model.dart | 27 +- lib/view_model/send/send_view_model.dart | 15 +- .../silent_payments_settings_view_model.dart | 33 + .../unspent_coins/unspent_coins_item.dart | 6 +- .../unspent_coins_list_view_model.dart | 45 +- .../wallet_address_list_header.dart | 5 +- .../wallet_address_list_item.dart | 9 +- .../wallet_address_list_view_model.dart | 71 +- .../wallet_list/wallet_list_item.dart | 2 + .../wallet_list/wallet_list_view_model.dart | 1 + macos/Podfile.lock | 8 +- model_generator.sh | 22 +- pubspec_base.yaml | 6 +- res/values/strings_ar.arb | 24 + res/values/strings_bg.arb | 24 + res/values/strings_cs.arb | 24 + res/values/strings_de.arb | 24 + res/values/strings_en.arb | 24 + res/values/strings_es.arb | 24 + res/values/strings_fr.arb | 24 + res/values/strings_ha.arb | 24 + res/values/strings_hi.arb | 24 + res/values/strings_hr.arb | 24 + res/values/strings_id.arb | 24 + res/values/strings_it.arb | 24 + res/values/strings_ja.arb | 24 + res/values/strings_ko.arb | 24 + res/values/strings_my.arb | 24 + res/values/strings_nl.arb | 24 + res/values/strings_pl.arb | 24 + res/values/strings_pt.arb | 28 +- res/values/strings_ru.arb | 24 + res/values/strings_th.arb | 24 + res/values/strings_tl.arb | 24 + res/values/strings_tr.arb | 24 + res/values/strings_uk.arb | 24 + res/values/strings_ur.arb | 24 + res/values/strings_yo.arb | 24 + res/values/strings_zh.arb | 24 + scripts/android/app_env.fish | 75 ++ scripts/android/app_env.sh | 8 +- scripts/ios/app_env.sh | 8 +- scripts/macos/app_env.sh | 8 +- tool/configure.dart | 34 +- 122 files changed, 3889 insertions(+), 1252 deletions(-) create mode 100644 assets/images/cards.svg create mode 100644 assets/images/tbtc.png delete mode 100644 cw_shared_external/pubspec.lock rename lib/src/screens/dashboard/pages/{market_place_page.dart => cake_features_page.dart} (60%) create mode 100644 lib/src/screens/settings/silent_payments_settings.dart create mode 100644 lib/view_model/dashboard/cake_features_view_model.dart delete mode 100644 lib/view_model/dashboard/market_place_view_model.dart create mode 100644 lib/view_model/settings/silent_payments_settings_view_model.dart create mode 100644 scripts/android/app_env.fish diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index acaa12fe0..b4ddcfefe 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -42,7 +42,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.19.5" + flutter-version: "3.19.6" channel: stable - name: Install package dependencies diff --git a/assets/images/cards.svg b/assets/images/cards.svg new file mode 100644 index 000000000..699f9d311 --- /dev/null +++ b/assets/images/cards.svg @@ -0,0 +1,65 @@ + + + + diff --git a/assets/images/tbtc.png b/assets/images/tbtc.png new file mode 100644 index 0000000000000000000000000000000000000000..bd4323edfce97bfadd2a2cf764af04af74b368b8 GIT binary patch literal 3983 zcmV;A4{-2_P)EX>4Tx04R}tkv&MmKpe$i)0V1K9Lyl%kfAzR1Ql_tRVYG*P%E_RU_SZkM+H^bh|{W*Vj)BONgw~P>leu-ldA%S z91EyHgXH?b{@{1FR&i?5ONu6e@QdSoi~^Bepw)1k?_a(JTfSK%#82Lj)CMz?or~d0+n^2Z33Ec@HGHf5Ck3eeb^C-2LA72Ka?6N@OB1 zk(jv6g$U^amawx45J~|*Lf}Cu+yK~sKm-ag06BmzfXV@;iGtP=f?C2d#oAs$QC{9& z`Ogy2?c2B8+)=7yLCNC)T>_9$3-H1M^cn$kl7Ky-kat|l&(GIRWnv_0cfwApmjrLx@8OjuRnLbJEhRN zNa*(M+imXL?RyUdFW=V@Ii*sukiI09O8dqQ&l0>IKYfYX#df-4O z?|?p^v}LA%p{D`(PqOT>QVYTIqlfl2@CN9*q|KoMh7?d>+9b`$>xe)blP#4tat>%> zN{TPlG8X~(O!7KMgFuUztgN*9k(?wLvCRZ7@D*PYLa^rPr(kVu4Y^#7K8=P?K&Ma! zkIq>Xm3O7EP-SrmO-V_yQfdX~0J@m3JDTR{fxo`}Mt|jlWl`VI2x;aKwElR5S4Y+4 z?Y`37%*yOIRH-5V;1f&$2oAcJ`ZElJ8PomH)pf5z7coB!YvWfVZ0;P0Y;3qv-?A?VIlM*>9c+gi8 zk~gom2%w3Hsa}-QJ>%gOf&&AF{9jR7HB5rmW++KdNKKtOW4HxCN%N9-ffhb;@-~jDm$`~JgM$ng?u9}j0{v&g$J=WtqYQ)Cm?iL<<_SUw zf`;4Isv7V_!QRd78%7j|rlh3UDz!{K05?l>V{L7Pv^~2Z6beVZM|XD*?CtG9$UXL@ znMZK(>u-6=Owpq15(nnz|4nIDrW86q>LmcHx!^^sWpU>Gm#D3)M^8@=oSYnCD;AGv z4;vfnAz%VPLt``Q8=IivC!2D#veDF*$_i!X07}9lB69#pxZ#1wyqwOQjWBqc+87jSWs5{eUnkAL>YDy-vPWvSMtPTyXr4o zVD(C}P%4e5n|{7NShqHw zE7o0n8ry$mP~{2es`1F#LxK$hT7CVxLC5yLf^+!O+wY*W^N!K|mn?n=uC6W|={LV?_!)t zK&-^!x++gFYg_;|BB8Z)4Q6LPQ=FWPt}7Kv?qX@eMCkxZ2txS=)7g0n+^4!3v7gt? z)49+PczPHg@N2(yi_7d}z*GtceK=!0q;q&qKA0X_v=DZ7wzyVNiJICvbaZs;6j~x9 z!jbs=T9esW`43mQES7lb0t!r>Na_v%t}YV9#w%#waP*>e{iot^0J>4BZCEv9*T zm<;X#;QJyO+HZ0wp&;J+0CnXX(BPnv4K&-^+ri#`WO+wNM<;TQ9^>>r$e;*H;3-B81gWm(8d%OEQ)!-9zU z*p#^5sFPYAw-k4}y71Yl(_G8wZQZ~gxWkup1_t;WwTqR5`tOFQsJI00yuZt&!+Gk7 z$02cc=2+!vn+~9qpmieH&wfD7YY8{Ev?A}H##P$d+6s#vh~!x1=?z^8Woa#6fEu%Z zw{Cafc1MTVdleR!m~=h?GdWHI33EfohEj%HhlTrD)BSw$n{BD6tf@hD^>s8fHX$T9 z2>wQH&>VFiOd5>_YIPsPV#Bk;cD9^1A>n7e9o^i20)`n0R`)!%8SbcTW%0YW>e)357zeTDbzN zRxUT1aRI<5pPfWo`%SJ~(zU}G(!c9ZHz`|SsY&HWq8}dBc|P7=@bUJ-@?S3BvT}D3@bne1eP^;D4;i+XlGYF_P?LFJaSSx>ezsHzJ-rbGM zg~hmBSPY=QIv*MmjM=k7FgtV>oToT(9-gUsdr@%iJeQY{JJ}1r8vMbKSVJb0sl&n} z=Kz>v@taer(A3<5B3UU;ojwB(4|n)@d2tG8MplklUDGiXMRE?N?LR*FGe%F;e}s?V ze!8#kkpHjza1|#$KZUybhA|I#QA+WzDCLp`H;zeICX>k5N&C^3?eP8+^h(tE<^l*o>^AtGRJ3wqJhEkz~s<#*Q4UMR( zu0cgrHD@D?!w~%d(9JFNR_Sd8^q^8p{s_Q@4?sIR@8IU3Ht+CWQ+Ia{E*BQ@1e96S zCSZH>^eyPEp-H7u1tIu=yK2)=z$%7iM+ao9_0L8M>0dV72KZ#WcT&;IF%<2SvptL+se9fZM@>!b zgaDHOT||RaGmbM&2^&*4fZ#CSH{t3cfk-4WIs3_zsy~~Xnf8&Xbmus**KYdU{+R^N+Vz& z1!&dJ2+bNHD_m{NtB=Zt4yNZfYz`DK8SHl*}{|J-`0YSihB9!i%D3T3?VrO<{+Hp&7EZ~CMGFjo(;D8zXgd)3U08s$!CkT+YD9Bqv ztt#Qj;f!*QiYVMt{MN1ZA~m}alx_n^&^Wp83d&#)>Ajnslas^gN2oc z2jEF?{L>W#=wgBYFf7TUubgC{{i9m`@q==T66#a002ovPDHLkV1f^$cqsq? literal 0 HcmV?d00001 diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 483f249cf..557dd8b26 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,3 +1,2 @@ -Add Tron wallet -Hardware wallets enhancements -Bug fixes \ No newline at end of file +Bitcoin Silent Payments +Bug fixes and generic enhancements diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index d1c3b6a61..bf36e6fb9 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -3,8 +3,8 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/script_hash.dart' as sh; -class BitcoinAddressRecord { - BitcoinAddressRecord( +abstract class BaseBitcoinAddressRecord { + BaseBitcoinAddressRecord( this.address, { required this.index, this.isHidden = false, @@ -13,15 +13,62 @@ class BitcoinAddressRecord { String name = '', bool isUsed = false, required this.type, - String? scriptHash, required this.network, }) : _txCount = txCount, _balance = balance, _name = name, - _isUsed = isUsed, - scriptHash = scriptHash ?? sh.scriptHash(address, network: network); + _isUsed = isUsed; - factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork network) { + @override + bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; + + final String address; + bool isHidden; + final int index; + int _txCount; + int _balance; + String _name; + bool _isUsed; + BasedUtxoNetwork? network; + + int get txCount => _txCount; + + String get name => _name; + + int get balance => _balance; + + set txCount(int value) => _txCount = value; + + set balance(int value) => _balance = value; + + bool get isUsed => _isUsed; + + void setAsUsed() => _isUsed = true; + void setNewName(String label) => _name = label; + + int get hashCode => address.hashCode; + + BitcoinAddressType type; + + String toJSON(); +} + +class BitcoinAddressRecord extends BaseBitcoinAddressRecord { + BitcoinAddressRecord( + super.address, { + required super.index, + super.isHidden = false, + super.txCount = 0, + super.balance = 0, + super.name = '', + super.isUsed = false, + required super.type, + String? scriptHash, + required super.network, + }) : scriptHash = + scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null); + + factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord( @@ -41,44 +88,15 @@ class BitcoinAddressRecord { ); } - @override - bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address; - - final String address; - bool isHidden; - final int index; - int _txCount; - int _balance; - String _name; - bool _isUsed; String? scriptHash; - BasedUtxoNetwork network; - int get txCount => _txCount; - - String get name => _name; - - int get balance => _balance; - - set txCount(int value) => _txCount = value; - - set balance(int value) => _balance = value; - - bool get isUsed => _isUsed; - - void setAsUsed() => _isUsed = true; - void setNewName(String label) => _name = label; - - @override - int get hashCode => address.hashCode; - - BitcoinAddressType type; - - String updateScriptHash(BasedUtxoNetwork network) { + String getScriptHash(BasedUtxoNetwork network) { + if (scriptHash != null) return scriptHash!; scriptHash = sh.scriptHash(address, network: network); return scriptHash!; } + @override String toJSON() => json.encode({ 'address': address, 'index': index, @@ -91,3 +109,57 @@ class BitcoinAddressRecord { 'scriptHash': scriptHash, }); } + +class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { + BitcoinSilentPaymentAddressRecord( + super.address, { + required super.index, + super.isHidden = false, + super.txCount = 0, + super.balance = 0, + super.name = '', + super.isUsed = false, + required this.silentPaymentTweak, + required super.network, + required super.type, + }) : super(); + + factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource, + {BasedUtxoNetwork? network}) { + final decoded = json.decode(jsonSource) as Map; + + return BitcoinSilentPaymentAddressRecord( + decoded['address'] as String, + index: decoded['index'] as int, + isHidden: decoded['isHidden'] as bool? ?? false, + isUsed: decoded['isUsed'] as bool? ?? false, + txCount: decoded['txCount'] as int? ?? 0, + name: decoded['name'] as String? ?? '', + balance: decoded['balance'] as int? ?? 0, + network: (decoded['network'] as String?) == null + ? network + : BasedUtxoNetwork.fromName(decoded['network'] as String), + silentPaymentTweak: decoded['silent_payment_tweak'] as String?, + type: decoded['type'] != null && decoded['type'] != '' + ? BitcoinAddressType.values + .firstWhere((type) => type.toString() == decoded['type'] as String) + : SilentPaymentsAddresType.p2sp, + ); + } + + final String? silentPaymentTweak; + + @override + String toJSON() => json.encode({ + 'address': address, + 'index': index, + 'isHidden': isHidden, + 'isUsed': isUsed, + 'txCount': txCount, + 'name': name, + 'balance': balance, + 'type': type.toString(), + 'network': network?.value, + 'silent_payment_tweak': silentPaymentTweak, + }); +} diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 2d2339a41..aa3d4a4cd 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -8,6 +8,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); + static const silent_payments = BitcoinReceivePageOption._('Silent Payments'); + const BitcoinReceivePageOption._(this.value); final String value; @@ -17,6 +19,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { } static const all = [ + BitcoinReceivePageOption.silent_payments, BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.p2tr, BitcoinReceivePageOption.p2wsh, @@ -24,6 +27,24 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinReceivePageOption.p2pkh ]; + BitcoinAddressType toType() { + switch (this) { + case BitcoinReceivePageOption.p2tr: + return SegwitAddresType.p2tr; + case BitcoinReceivePageOption.p2wsh: + return SegwitAddresType.p2wsh; + case BitcoinReceivePageOption.p2pkh: + return P2pkhAddressType.p2pkh; + case BitcoinReceivePageOption.p2sh: + return P2shAddressType.p2wpkhInP2sh; + case BitcoinReceivePageOption.silent_payments: + return SilentPaymentsAddresType.p2sp; + case BitcoinReceivePageOption.p2wpkh: + default: + return SegwitAddresType.p2wpkh; + } + } + factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) { switch (type) { case SegwitAddresType.p2tr: @@ -34,6 +55,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2pkh; case P2shAddressType.p2wpkhInP2sh: return BitcoinReceivePageOption.p2sh; + case SilentPaymentsAddresType.p2sp: + return BitcoinReceivePageOption.silent_payments; case SegwitAddresType.p2wpkh: default: return BitcoinReceivePageOption.p2wpkh; diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 52edea091..3691a7a22 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -2,13 +2,66 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/unspent_transaction_output.dart'; class BitcoinUnspent extends Unspent { - BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout) + BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout) : bitcoinAddressRecord = addressRecord, super(addressRecord.address, hash, value, vout, null); - factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map json) => + factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map json) => BitcoinUnspent( - address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int); + address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()), + json['tx_hash'] as String, + json['value'] as int, + json['tx_pos'] as int, + ); - final BitcoinAddressRecord bitcoinAddressRecord; + Map toJson() { + final json = { + 'address_record': bitcoinAddressRecord.toJSON(), + 'tx_hash': hash, + 'value': value, + 'tx_pos': vout, + }; + return json; + } + + final BaseBitcoinAddressRecord bitcoinAddressRecord; +} + +class BitcoinSilentPaymentsUnspent extends BitcoinUnspent { + BitcoinSilentPaymentsUnspent( + BitcoinSilentPaymentAddressRecord addressRecord, + String hash, + int value, + int vout, { + required this.silentPaymentTweak, + required this.silentPaymentLabel, + }) : super(addressRecord, hash, value, vout); + + @override + factory BitcoinSilentPaymentsUnspent.fromJSON( + BitcoinSilentPaymentAddressRecord? address, Map json) => + BitcoinSilentPaymentsUnspent( + address ?? BitcoinSilentPaymentAddressRecord.fromJSON(json['address_record'].toString()), + json['tx_hash'] as String, + json['value'] as int, + json['tx_pos'] as int, + silentPaymentTweak: json['silent_payment_tweak'] as String?, + silentPaymentLabel: json['silent_payment_label'] as String?, + ); + + @override + Map toJson() { + final json = { + 'address_record': bitcoinAddressRecord.toJSON(), + 'tx_hash': hash, + 'value': value, + 'tx_pos': vout, + 'silent_payment_tweak': silentPaymentTweak, + 'silent_payment_label': silentPaymentLabel, + }; + return json; + } + + String? silentPaymentTweak; + String? silentPaymentLabel; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index b02116541..3954631e8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -39,22 +39,28 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, String? passphrase, + List? initialSilentAddresses, + int initialSilentAddressIndex = 0, + bool? alwaysScan, }) : super( - mnemonic: mnemonic, - passphrase: passphrase, - xpub: xpub, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - networkType: networkParam == null - ? bitcoin.bitcoin - : networkParam == BitcoinNetwork.mainnet + mnemonic: mnemonic, + passphrase: passphrase, + xpub: xpub, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + networkType: networkParam == null ? bitcoin.bitcoin - : bitcoin.testnet, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - currency: CryptoCurrency.btc) { + : networkParam == BitcoinNetwork.mainnet + ? bitcoin.bitcoin + : bitcoin.testnet, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: + networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, + alwaysScan: alwaysScan, + ) { // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here) // String derivationPath = walletInfo.derivationInfo!.derivationPath!; @@ -62,14 +68,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); walletAddresses = BitcoinWalletAddresses( walletInfo, - electrumClient: electrumClient, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, mainHd: hd, sideHd: accountHD.derive(1), network: networkParam ?? network, + masterHd: + seedBytes != null ? bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) : null, ); + autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); @@ -84,9 +94,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { String? addressPageType, BasedUtxoNetwork? network, List? initialAddresses, + List? initialSilentAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + int initialSilentAddressIndex = 0, }) async { late Uint8List seedBytes; @@ -109,6 +121,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, seedBytes: seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, @@ -123,6 +137,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required String password, + required bool alwaysScan, }) async { final network = walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) @@ -163,12 +178,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, + initialSilentAddresses: snp.silentAddresses, + initialSilentAddressIndex: snp.silentAddressIndex, initialBalance: snp.balance, seedBytes: seedBytes, initialRegularAddressIndex: snp.regularAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex, addressPageType: snp.addressPageType, networkParam: network, + alwaysScan: alwaysScan, ); } @@ -179,7 +197,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) { _ledger = setLedger; _ledgerDevice = setLedgerDevice; - _bitcoinLedgerApp = BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + _bitcoinLedgerApp = + BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!); } @override @@ -202,16 +221,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( - utxo: utxo.utxo, - rawTx: rawTx, - ownerDetails: utxo.ownerDetails, - ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, - ownerMasterFingerprint: masterFingerprint, - ownerPublicKey: publicKeyAndDerivationPath.publicKey, + utxo: utxo.utxo, + rawTx: rawTx, + ownerDetails: utxo.ownerDetails, + ownerDerivationPath: publicKeyAndDerivationPath.derivationPath, + ownerMasterFingerprint: masterFingerprint, + ownerPublicKey: publicKeyAndDerivationPath.publicKey, )); } - final psbt = PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); + final psbt = + PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt); return BtcTransaction.fromRaw(hex.encode(rawHex)); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index f12577492..486e69b11 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -15,10 +15,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required super.mainHd, required super.sideHd, required super.network, - required super.electrumClient, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, + super.initialSilentAddresses, + super.initialSilentAddressIndex = 0, + super.masterHd, }) : super(walletInfo); @override diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index cf99324da..a9a6d96db 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -19,10 +19,11 @@ class BitcoinWalletService extends WalletService< BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { - BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan); final Box walletInfoSource; final Box unspentCoinsInfoSource; + final bool alwaysScan; @override WalletType getType() => WalletType.bitcoin; @@ -55,20 +56,24 @@ class BitcoinWalletService extends WalletService< .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; try { final wallet = await BitcoinWalletBase.open( - password: password, - name: name, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, + ); await wallet.init(); saveBackup(name); return wallet; } catch (_) { await restoreWalletFilesFromBackup(name); final wallet = await BitcoinWalletBase.open( - password: password, - name: name, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: name, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, + ); await wallet.init(); return wallet; } @@ -87,10 +92,12 @@ class BitcoinWalletService extends WalletService< final currentWalletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; final currentWallet = await BitcoinWalletBase.open( - password: password, - name: currentName, - walletInfo: currentWalletInfo, - unspentCoinsInfo: unspentCoinsInfoSource); + password: password, + name: currentName, + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + alwaysScan: alwaysScan, + ); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); @@ -105,12 +112,13 @@ class BitcoinWalletService extends WalletService< @override Future restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials, {bool? isTestnet}) async { - final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; credentials.walletInfo?.network = network.value; - credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath; + credentials.walletInfo?.derivationInfo?.derivationPath = + credentials.hwAccountData.derivationPath; - final wallet = await BitcoinWallet(password: credentials.password!, + final wallet = await BitcoinWallet( + password: credentials.password!, xpub: credentials.hwAccountData.xpub, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, @@ -123,7 +131,7 @@ class BitcoinWalletService extends WalletService< @override Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, - {bool? isTestnet}) async => + {bool? isTestnet}) async => throw UnimplementedError(); @override diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 0553170cc..afd5e2440 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -41,23 +41,35 @@ class ElectrumClient { bool get isConnected => _isConnected; Socket? socket; - void Function(bool)? onConnectionStatusChange; + void Function(bool?)? onConnectionStatusChange; int _id; final Map _tasks; + Map get tasks => _tasks; final Map _errors; bool _isConnected; Timer? _aliveTimer; String unterminatedString; - Future connectToUri(Uri uri) async => await connect(host: uri.host, port: uri.port); + Uri? uri; + bool? useSSL; - Future connect({required String host, required int port}) async { + Future connectToUri(Uri uri, {bool? useSSL}) async { + this.uri = uri; + this.useSSL = useSSL; + await connect(host: uri.host, port: uri.port, useSSL: useSSL); + } + + Future connect({required String host, required int port, bool? useSSL}) async { try { await socket?.close(); } catch (_) {} - socket = await SecureSocket.connect(host, port, - timeout: connectionTimeout, onBadCertificate: (_) => true); + if (useSSL == false) { + socket = await Socket.connect(host, port, timeout: connectionTimeout); + } else { + socket = await SecureSocket.connect(host, port, + timeout: connectionTimeout, onBadCertificate: (_) => true); + } _setIsConnected(true); socket!.listen((Uint8List event) { @@ -79,7 +91,7 @@ class ElectrumClient { _setIsConnected(false); }, onDone: () { unterminatedString = ''; - _setIsConnected(false); + _setIsConnected(null); }); keepAlive(); } @@ -134,11 +146,12 @@ class ElectrumClient { await callWithTimeout(method: 'server.ping'); _setIsConnected(true); } on RequestFailedTimeoutException catch (_) { - _setIsConnected(false); + _setIsConnected(null); } } - Future> version() => call(method: 'server.version').then((dynamic result) { + Future> version() => + call(method: 'server.version', params: ["", "1.4"]).then((dynamic result) { if (result is List) { return result.map((dynamic val) => val.toString()).toList(); } @@ -266,6 +279,18 @@ class ElectrumClient { Future> getHeader({required int height}) async => await call(method: 'blockchain.block.get_header', params: [height]) as Map; + BehaviorSubject? tweaksSubscribe({required int height, required int count}) { + _id += 1; + return subscribe( + id: 'blockchain.tweaks.subscribe:${height + count}', + method: 'blockchain.tweaks.subscribe', + params: [height, count, false], + ); + } + + Future getTweaks({required int height}) async => + await callWithTimeout(method: 'blockchain.tweaks.subscribe', params: [height, 1, false]); + Future estimatefee({required int p}) => call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { if (result is double) { @@ -308,9 +333,6 @@ class ElectrumClient { }); Future> feeRates({BasedUtxoNetwork? network}) async { - if (network == BitcoinNetwork.testnet) { - return [1, 1, 1]; - } try { final topDoubleString = await estimatefee(p: 1); final middleDoubleString = await estimatefee(p: 5); @@ -332,7 +354,7 @@ class ElectrumClient { // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" // } Future getCurrentBlockChainTip() => - call(method: 'blockchain.headers.subscribe').then((result) { + callWithTimeout(method: 'blockchain.headers.subscribe').then((result) { if (result is Map) { return result["height"] as int; } @@ -340,6 +362,12 @@ class ElectrumClient { return null; }); + BehaviorSubject? chainTipSubscribe() { + _id += 1; + return subscribe( + id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe'); + } + BehaviorSubject? scripthashUpdate(String scripthash) { _id += 1; return subscribe( @@ -396,7 +424,9 @@ class ElectrumClient { Future close() async { _aliveTimer?.cancel(); - await socket?.close(); + try { + await socket?.close(); + } catch (_) {} onConnectionStatusChange = null; } @@ -431,17 +461,25 @@ class ElectrumClient { _tasks[id]?.subject?.add(params.last); break; + case 'blockchain.headers.subscribe': + final params = request['params'] as List; + _tasks[method]?.subject?.add(params.last); + break; + case 'blockchain.tweaks.subscribe': + final params = request['params'] as List; + _tasks[_tasks.keys.first]?.subject?.add(params.last); + break; default: break; } } - void _setIsConnected(bool isConnected) { + void _setIsConnected(bool? isConnected) { if (_isConnected != isConnected) { onConnectionStatusChange?.call(isConnected); } - _isConnected = isConnected; + _isConnected = isConnected ?? false; } void _handleResponse(Map response) { diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 165ea447e..15d6843d8 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -3,8 +3,11 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/balance.dart'; class ElectrumBalance extends Balance { - const ElectrumBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) - : super(confirmed, unconfirmed); + ElectrumBalance({ + required this.confirmed, + required this.unconfirmed, + required this.frozen, + }) : super(confirmed, unconfirmed); static ElectrumBalance? fromJSON(String? jsonSource) { if (jsonSource == null) { @@ -19,8 +22,8 @@ class ElectrumBalance extends Balance { frozen: decoded['frozen'] as int? ?? 0); } - final int confirmed; - final int unconfirmed; + int confirmed; + int unconfirmed; final int frozen; @override diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index d478c3b12..a7de414e4 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -11,13 +11,11 @@ part 'electrum_transaction_history.g.dart'; const transactionsHistoryFileName = 'transactions.json'; -class ElectrumTransactionHistory = ElectrumTransactionHistoryBase - with _$ElectrumTransactionHistory; +class ElectrumTransactionHistory = ElectrumTransactionHistoryBase with _$ElectrumTransactionHistory; abstract class ElectrumTransactionHistoryBase extends TransactionHistoryBase with Store { - ElectrumTransactionHistoryBase( - {required this.walletInfo, required String password}) + ElectrumTransactionHistoryBase({required this.walletInfo, required String password}) : _password = password, _height = 0 { transactions = ObservableMap(); @@ -30,8 +28,7 @@ abstract class ElectrumTransactionHistoryBase Future init() async => await _load(); @override - void addOne(ElectrumTransactionInfo transaction) => - transactions[transaction.id] = transaction; + void addOne(ElectrumTransactionInfo transaction) => transactions[transaction.id] = transaction; @override void addMany(Map transactions) => @@ -40,11 +37,13 @@ abstract class ElectrumTransactionHistoryBase @override Future save() async { try { - final dirPath = - await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final path = '$dirPath/$transactionsHistoryFileName'; - final data = - json.encode({'height': _height, 'transactions': transactions}); + final txjson = {}; + for (final tx in transactions.entries) { + txjson[tx.key] = tx.value.toJson(); + } + final data = json.encode({'height': _height, 'transactions': txjson}); await writeData(path: path, password: _password, data: data); } catch (e) { print('Error while save bitcoin transaction history: ${e.toString()}'); @@ -57,8 +56,7 @@ abstract class ElectrumTransactionHistoryBase } Future> _read() async { - final dirPath = - await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); final path = '$dirPath/$transactionsHistoryFileName'; final content = await read(path: path, password: _password); return json.decode(content) as Map; @@ -84,7 +82,5 @@ abstract class ElectrumTransactionHistoryBase } } - void _update(ElectrumTransactionInfo transaction) => - transactions[transaction.id] = transaction; - + void _update(ElectrumTransactionInfo transaction) => transactions[transaction.id] = transaction; } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index f980bd884..d06cfe9de 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,9 +1,8 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/format_amount.dart'; @@ -19,6 +18,8 @@ class ElectrumTransactionBundle { } class ElectrumTransactionInfo extends TransactionInfo { + List? unspents; + ElectrumTransactionInfo(this.type, {required String id, required int height, @@ -29,7 +30,9 @@ class ElectrumTransactionInfo extends TransactionInfo { required TransactionDirection direction, required bool isPending, required DateTime date, - required int confirmations}) { + required int confirmations, + String? to, + this.unspents}) { this.id = id; this.height = height; this.amount = amount; @@ -40,6 +43,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.date = date; this.isPending = isPending; this.confirmations = confirmations; + this.to = to; } factory ElectrumTransactionInfo.fromElectrumVerbose(Map obj, WalletType type, @@ -153,52 +157,31 @@ class ElectrumTransactionInfo extends TransactionInfo { confirmations: bundle.confirmations); } - factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex, - {List? addresses, required int height, int? timestamp, required int confirmations}) { - final tx = bitcoin.Transaction.fromHex(hex); - var exist = false; - var amount = 0; - - if (addresses != null) { - tx.outs.forEach((out) { - try { - final p2pkh = - bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin); - exist = addresses.contains(p2pkh.data.address); - - if (exist) { - amount += out.value!; - } - } catch (_) {} - }); - } - - final date = - timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now(); - - return ElectrumTransactionInfo(type, - id: tx.getId(), - height: height, - isPending: false, - fee: null, - direction: TransactionDirection.incoming, - amount: amount, - date: date, - confirmations: confirmations); - } - factory ElectrumTransactionInfo.fromJson(Map data, WalletType type) { - return ElectrumTransactionInfo(type, - id: data['id'] as String, - height: data['height'] as int, - amount: data['amount'] as int, - fee: data['fee'] as int, - direction: parseTransactionDirectionFromInt(data['direction'] as int), - date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), - isPending: data['isPending'] as bool, - inputAddresses: data['inputAddresses'] as List, - outputAddresses: data['outputAddresses'] as List, - confirmations: data['confirmations'] as int); + final inputAddresses = data['inputAddresses'] as List? ?? []; + final outputAddresses = data['outputAddresses'] as List? ?? []; + final unspents = data['unspents'] as List? ?? []; + + return ElectrumTransactionInfo( + type, + id: data['id'] as String, + height: data['height'] as int, + amount: data['amount'] as int, + fee: data['fee'] as int, + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + inputAddresses: + inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(), + outputAddresses: + outputAddresses.isEmpty ? [] : outputAddresses.map((e) => e.toString()).toList(), + to: data['to'] as String?, + unspents: unspents + .map((unspent) => + BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) + .toList(), + ); } final WalletType type; @@ -244,8 +227,14 @@ class ElectrumTransactionInfo extends TransactionInfo { m['isPending'] = isPending; m['confirmations'] = confirmations; m['fee'] = fee; + m['to'] = to; + m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? []; m['inputAddresses'] = inputAddresses; m['outputAddresses'] = outputAddresses; return m; } + + String toString() { + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)'; + } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index b899744a6..96f871a4b 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -35,33 +36,39 @@ import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; +import 'package:sp_scanner/sp_scanner.dart'; part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; +const int TWEAKS_COUNT = 25; + abstract class ElectrumWalletBase extends WalletBase with Store { - ElectrumWalletBase( - {required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required this.networkType, - String? xpub, - String? mnemonic, - Uint8List? seedBytes, - this.passphrase, - List? initialAddresses, - ElectrumClient? electrumClient, - ElectrumBalance? initialBalance, - CryptoCurrency? currency}) - : accountHD = + ElectrumWalletBase({ + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required this.networkType, + String? xpub, + String? mnemonic, + Uint8List? seedBytes, + this.passphrase, + List? initialAddresses, + ElectrumClient? electrumClient, + ElectrumBalance? initialBalance, + CryptoCurrency? currency, + this.alwaysScan, + }) : accountHD = getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, @@ -72,8 +79,12 @@ abstract class ElectrumWalletBase _scripthashesUpdateSubject = {}, balance = ObservableMap.of(currency != null ? { - currency: - initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0) + currency: initialBalance ?? + ElectrumBalance( + confirmed: 0, + unconfirmed: 0, + frozen: 0, + ) } : {}), this.unspentCoinsInfo = unspentCoinsInfo, @@ -84,6 +95,23 @@ abstract class ElectrumWalletBase this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); + + reaction((_) => syncStatus, (SyncStatus syncStatus) { + if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) + silentPaymentsScanningActive = syncStatus is SyncingSyncStatus; + + if (syncStatus is NotConnectedSyncStatus) { + // Needs to re-subscribe to all scripthashes when reconnected + _scripthashesUpdateSubject = {}; + } + + // Message is shown on the UI for 3 seconds, revert to synced + if (syncStatus is SyncedTipSyncStatus) { + Timer(Duration(seconds: 3), () { + if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus(); + }); + } + }); } static bitcoin.HDWallet getAccountHDWallet( @@ -113,6 +141,8 @@ abstract class ElectrumWalletBase static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; + bool? alwaysScan; + final bitcoin.HDWallet accountHD; final String? _mnemonic; @@ -137,6 +167,8 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; + Set get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + List get scriptHashes => walletAddresses.addressesByReceiveType .map((addr) => scriptHash(addr.address, network: network)) .toList(); @@ -157,6 +189,64 @@ abstract class ElectrumWalletBase @override bool? isTestnet; + bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; + + @observable + bool nodeSupportsSilentPayments = true; + @observable + bool silentPaymentsScanningActive = false; + + @action + Future setSilentPaymentsScanning(bool active) async { + silentPaymentsScanningActive = active; + + if (active) { + syncStatus = AttemptingSyncStatus(); + + final tip = await getUpdatedChainTip(); + + if (tip == walletInfo.restoreHeight) { + syncStatus = SyncedTipSyncStatus(tip); + } + + if (tip > walletInfo.restoreHeight) { + _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); + } + } else { + alwaysScan = false; + + (await _isolate)?.kill(priority: Isolate.immediate); + + if (electrumClient.isConnected) { + syncStatus = SyncedSyncStatus(); + } else { + if (electrumClient.uri != null) { + await electrumClient.connectToUri(electrumClient.uri!); + startSync(); + } + } + } + } + + int? _currentChainTip; + + Future getCurrentChainTip() async { + if (_currentChainTip != null) { + return _currentChainTip!; + } + _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; + + return _currentChainTip!; + } + + Future getUpdatedChainTip() async { + final newTip = await electrumClient.getCurrentBlockChainTip(); + if (newTip != null && newTip > (_currentChainTip ?? 0)) { + _currentChainTip = newTip; + } + return _currentChainTip ?? 0; + } + @override BitcoinWalletKeys get keys => BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); @@ -164,32 +254,179 @@ abstract class ElectrumWalletBase String _password; List unspentCoins; List _feeRates; + + // ignore: prefer_final_fields Map?> _scripthashesUpdateSubject; + + // ignore: prefer_final_fields + BehaviorSubject? _chainTipUpdateSubject; bool _isTransactionUpdating; + Future? _isolate; void Function(FlutterErrorDetails)? _onError; + Timer? _autoSaveTimer; + static const int _autoSaveInterval = 30; Future init() async { await walletAddresses.init(); await transactionHistory.init(); await save(); + + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); + } + + @action + Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { + final chainTip = chainTipParam ?? await getUpdatedChainTip(); + + if (chainTip == height) { + syncStatus = SyncedSyncStatus(); + return; + } + + syncStatus = AttemptingSyncStatus(); + + if (_isolate != null) { + final runningIsolate = await _isolate!; + runningIsolate.kill(priority: Isolate.immediate); + } + + final receivePort = ReceivePort(); + _isolate = Isolate.spawn( + startRefresh, + ScanData( + sendPort: receivePort.sendPort, + silentAddress: walletAddresses.silentAddress!, + network: network, + height: height, + chainTip: chainTip, + electrumClient: ElectrumClient(), + transactionHistoryIds: transactionHistory.transactions.keys.toList(), + node: ScanNode(node!.uri, node!.useSSL), + labels: walletAddresses.labels, + labelIndexes: walletAddresses.silentAddresses + .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1) + .map((addr) => addr.index) + .toList(), + isSingleScan: doSingleScan ?? false, + )); + + await for (var message in receivePort) { + if (message is Map) { + for (final map in message.entries) { + final txid = map.key; + final tx = map.value; + + if (tx.unspents != null) { + final existingTxInfo = transactionHistory.transactions[txid]; + final txAlreadyExisted = existingTxInfo != null; + + // Updating tx after re-scanned + if (txAlreadyExisted) { + existingTxInfo.amount = tx.amount; + existingTxInfo.confirmations = tx.confirmations; + existingTxInfo.height = tx.height; + + final newUnspents = tx.unspents! + .where((unspent) => !(existingTxInfo.unspents?.any((element) => + element.hash.contains(unspent.hash) && + element.vout == unspent.vout && + element.value == unspent.value) ?? + false)) + .toList(); + + if (newUnspents.isNotEmpty) { + newUnspents.forEach(_updateSilentAddressRecord); + + existingTxInfo.unspents ??= []; + existingTxInfo.unspents!.addAll(newUnspents); + + final newAmount = newUnspents.length > 1 + ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) + : newUnspents[0].value; + + if (existingTxInfo.direction == TransactionDirection.incoming) { + existingTxInfo.amount += newAmount; + } + + // Updates existing TX + transactionHistory.addOne(existingTxInfo); + // Update balance record + balance[currency]!.confirmed += newAmount; + } + } else { + // else: First time seeing this TX after scanning + tx.unspents!.forEach(_updateSilentAddressRecord); + + // Add new TX record + transactionHistory.addMany(message); + // Update balance record + balance[currency]!.confirmed += tx.amount; + } + + await updateAllUnspents(); + } + } + } + + if (message is SyncResponse) { + if (message.syncStatus is UnsupportedSyncStatus) { + nodeSupportsSilentPayments = false; + } + + syncStatus = message.syncStatus; + await walletInfo.updateRestoreHeight(message.height); + } + } + } + + void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) { + final silentAddress = walletAddresses.silentAddress!; + final silentPaymentAddress = SilentPaymentAddress( + version: silentAddress.version, + B_scan: silentAddress.B_scan, + B_spend: unspent.silentPaymentLabel != null + ? silentAddress.B_spend.tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)), + ) + : silentAddress.B_spend, + hrp: silentAddress.hrp, + ); + + final addressRecord = walletAddresses.silentAddresses + .firstWhereOrNull((address) => address.address == silentPaymentAddress.toString()); + addressRecord?.txCount += 1; + addressRecord?.balance += unspent.value; + + walletAddresses.addSilentAddresses( + [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord], + ); } @action @override Future startSync() async { try { - syncStatus = AttemptingSyncStatus(); + syncStatus = SyncronizingSyncStatus(); + + if (hasSilentPaymentsScanning) { + await _setInitialHeight(); + } + + await _subscribeForUpdates(); + await updateTransactions(); - _subscribeForUpdates(); - await updateUnspent(); + await updateAllUnspents(); await updateBalance(); - _feeRates = await electrumClient.feeRates(network: network); - Timer.periodic( - const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates()); + Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); - syncStatus = SyncedSyncStatus(); + if (alwaysScan == true) { + _setListeners(walletInfo.restoreHeight); + } else { + syncStatus = SyncedSyncStatus(); + } } catch (e, stacktrace) { print(stacktrace); print(e.toString()); @@ -197,18 +434,39 @@ abstract class ElectrumWalletBase } } + @action + Future updateFeeRates() async { + final feeRates = await electrumClient.feeRates(network: network); + if (feeRates != [0, 0, 0]) { + _feeRates = feeRates; + } + } + + Node? node; + @action @override Future connectToNode({required Node node}) async { + this.node = node; + try { syncStatus = ConnectingSyncStatus(); - await electrumClient.connectToUri(node.uri); - electrumClient.onConnectionStatusChange = (bool isConnected) { - if (!isConnected) { + + await electrumClient.close(); + + electrumClient.onConnectionStatusChange = (bool? isConnected) async { + if (syncStatus is SyncingSyncStatus) return; + + if (isConnected == true && syncStatus is! SyncedSyncStatus) { + syncStatus = ConnectedSyncStatus(); + } else if (isConnected == false) { syncStatus = LostConnectionSyncStatus(); + } else if (!(isConnected ?? false) && syncStatus is! ConnectingSyncStatus) { + syncStatus = NotConnectedSyncStatus(); } }; - syncStatus = ConnectedSyncStatus(); + + await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); } catch (e) { print(e.toString()); syncStatus = FailedSyncStatus(); @@ -219,60 +477,95 @@ abstract class ElectrumWalletBase bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; - Future estimateSendAllTx( - List outputs, - int feeRate, { - String? memo, - int credentialsAmount = 0, - }) async { - final utxos = []; - final privateKeys = []; + UtxoDetails _createUTXOS({ + required bool sendAll, + required int credentialsAmount, + required bool paysToSilentPayment, + int? inputsCount, + }) { + List utxos = []; + List vinOutpoints = []; + List inputPrivKeyInfos = []; final publicKeys = {}; - int allInputsAmount = 0; - + bool spendsSilentPayment = false; bool spendsUnconfirmedTX = false; - for (int i = 0; i < unspentCoins.length; i++) { - final utx = unspentCoins[i]; + int leftAmount = credentialsAmount; + final availableInputs = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList(); + final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); - if (utx.isSending && !utx.isFrozen) { - if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; + for (int i = 0; i < availableInputs.length; i++) { + final utx = availableInputs[i]; + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; - allInputsAmount += utx.value; - - final address = addressTypeFromStr(utx.address, network); - final hd = - utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; - final derivationPath = - "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" - "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" - "/${utx.bitcoinAddressRecord.index}"; - final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!; - - publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); - - if (!walletInfo.isHardwareWallet) { - final privkey = - generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); - - privateKeys.add(privkey); + if (paysToSilentPayment) { + // Check inputs for shared secret derivation + if (utx.bitcoinAddressRecord.type == SegwitAddresType.p2wsh) { + throw BitcoinTransactionSilentPaymentsNotSupported(); } + } - utxos.add( - UtxoWithAddress( - utxo: BitcoinUtxo( - txHash: utx.hash, - value: BigInt.from(utx.value), - vout: utx.vout, - scriptType: _getScriptType(address), - ), - ownerDetails: UtxoAddressDetails( - publicKey: pubKeyHex, - address: address, - ), + allInputsAmount += utx.value; + leftAmount = leftAmount - utx.value; + + final address = addressTypeFromStr(utx.address, network); + ECPrivate? privkey; + bool? isSilentPayment = false; + + final hd = + utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; + final derivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" + "/${utx.bitcoinAddressRecord.index}"; + final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!; + + publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + + if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; + privkey = walletAddresses.silentAddress!.b_spend.tweakAdd( + BigintUtils.fromBytes( + BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!), ), ); + spendsSilentPayment = true; + isSilentPayment = true; + } else { + privkey = + generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); + } + + vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); + inputPrivKeyInfos.add(ECPrivateInfo( + privkey, + address.type == SegwitAddresType.p2tr, + tweak: !isSilentPayment, + )); + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utx.hash, + value: BigInt.from(utx.value), + vout: utx.vout, + scriptType: _getScriptType(address), + isSilentPayment: isSilentPayment, + ), + ownerDetails: UtxoAddressDetails( + publicKey: privkey.getPublic().toHex(), + address: address, + ), + ), + ); + + // sendAll continues for all inputs + if (!sendAll) { + bool amountIsAcquired = leftAmount <= 0; + if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) { + break; + } } } @@ -280,20 +573,48 @@ abstract class ElectrumWalletBase throw BitcoinTransactionNoInputsException(); } + return UtxoDetails( + availableInputs: availableInputs, + unconfirmedCoins: unconfirmedCoins, + utxos: utxos, + vinOutpoints: vinOutpoints, + inputPrivKeyInfos: inputPrivKeyInfos, + publicKeys: publicKeys, + allInputsAmount: allInputsAmount, + spendsSilentPayment: spendsSilentPayment, + spendsUnconfirmedTX: spendsUnconfirmedTX, + ); + } + + Future estimateSendAllTx( + List outputs, + int feeRate, { + String? memo, + int credentialsAmount = 0, + bool hasSilentPayment = false, + }) async { + final utxoDetails = _createUTXOS( + sendAll: true, + credentialsAmount: credentialsAmount, + paysToSilentPayment: hasSilentPayment, + ); + int estimatedSize; if (network is BitcoinCashNetwork) { estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxos, + utxos: utxoDetails.utxos, outputs: outputs, network: network as BitcoinCashNetwork, memo: memo, ); } else { estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxos, + utxos: utxoDetails.utxos, outputs: outputs, network: network, memo: memo, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, ); } @@ -304,7 +625,11 @@ abstract class ElectrumWalletBase } // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change - int amount = allInputsAmount - fee; + int amount = utxoDetails.allInputsAmount - fee; + + if (amount <= 0) { + throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); + } if (amount <= 0) { throw BitcoinTransactionWrongBalanceException(); @@ -323,19 +648,21 @@ abstract class ElectrumWalletBase } } - outputs[outputs.length - 1] = - BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); + if (outputs.length == 1) { + outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); + } return EstimatedTxResult( - utxos: utxos, - privateKeys: privateKeys, - publicKeys: publicKeys, + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, fee: fee, amount: amount, isSendAll: true, hasChange: false, memo: memo, - spendsUnconfirmedTX: spendsUnconfirmedTX, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, ); } @@ -346,79 +673,22 @@ abstract class ElectrumWalletBase int? inputsCount, String? memo, bool? useUnconfirmed, + bool hasSilentPayment = false, }) async { - final utxos = []; - final privateKeys = []; - final publicKeys = {}; + final utxoDetails = _createUTXOS( + sendAll: false, + credentialsAmount: credentialsAmount, + inputsCount: inputsCount, + paysToSilentPayment: hasSilentPayment, + ); - int allInputsAmount = 0; - bool spendsUnconfirmedTX = false; - - int leftAmount = credentialsAmount; - final sendingCoins = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList(); - final unconfirmedCoins = sendingCoins.where((utx) => utx.confirmations == 0).toList(); - - for (int i = 0; i < sendingCoins.length; i++) { - final utx = sendingCoins[i]; - - final isUncormirmed = utx.confirmations == 0; - if (useUnconfirmed != true && isUncormirmed) continue; - - if (!spendsUnconfirmedTX) spendsUnconfirmedTX = isUncormirmed; - - allInputsAmount += utx.value; - leftAmount = leftAmount - utx.value; - - final address = addressTypeFromStr(utx.address, network); - - final hd = - utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; - final derivationPath = - "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" - "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" - "/${utx.bitcoinAddressRecord.index}"; - final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!; - - publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); - - if (!walletInfo.isHardwareWallet) { - final privkey = - generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); - - privateKeys.add(privkey); - } - - utxos.add( - UtxoWithAddress( - utxo: BitcoinUtxo( - txHash: utx.hash, - value: BigInt.from(utx.value), - vout: utx.vout, - scriptType: _getScriptType(address), - ), - ownerDetails: UtxoAddressDetails( - publicKey: pubKeyHex, - address: address, - ), - ), - ); - - bool amountIsAcquired = leftAmount <= 0; - if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) { - break; - } - } - - if (utxos.isEmpty) { - throw BitcoinTransactionNoInputsException(); - } - - final spendingAllCoins = sendingCoins.length == utxos.length; - final spendingAllConfirmedCoins = - !spendsUnconfirmedTX && utxos.length == sendingCoins.length - unconfirmedCoins.length; + final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; + final spendingAllConfirmedCoins = !utxoDetails.spendsUnconfirmedTX && + utxoDetails.utxos.length == + utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length; // How much is being spent - how much is being sent - int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount; + int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount; if (amountLeftForChangeAndFee <= 0) { if (!spendingAllCoins) { @@ -426,11 +696,12 @@ abstract class ElectrumWalletBase credentialsAmount, outputs, feeRate, - inputsCount: utxos.length + 1, + inputsCount: utxoDetails.utxos.length + 1, memo: memo, - useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + hasSilentPayment: hasSilentPayment, ); } + throw BitcoinTransactionWrongBalanceException(); } @@ -444,17 +715,19 @@ abstract class ElectrumWalletBase int estimatedSize; if (network is BitcoinCashNetwork) { estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( - utxos: utxos, + utxos: utxoDetails.utxos, outputs: outputs, network: network as BitcoinCashNetwork, memo: memo, ); } else { estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxos, + utxos: utxoDetails.utxos, outputs: outputs, network: network, memo: memo, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, ); } @@ -482,7 +755,7 @@ abstract class ElectrumWalletBase credentialsAmount, outputs, feeRate, - inputsCount: utxos.length + 1, + inputsCount: utxoDetails.utxos.length + 1, memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, ); @@ -499,7 +772,7 @@ abstract class ElectrumWalletBase } // Estimate to user how much is needed to send to cover the fee - final maxAmountWithReturningChange = allInputsAmount - _dustAmount - fee - 1; + final maxAmountWithReturningChange = utxoDetails.allInputsAmount - _dustAmount - fee - 1; throw BitcoinTransactionNoDustOnChangeException( bitcoinAmountToString(amount: maxAmountWithReturningChange), bitcoinAmountToString(amount: estimatedSendAll.amount), @@ -517,35 +790,34 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(); } - if (totalAmount > allInputsAmount) { + if (totalAmount > utxoDetails.allInputsAmount) { if (spendingAllCoins) { throw BitcoinTransactionWrongBalanceException(); } else { - if (amountLeftForChangeAndFee > fee) { - outputs.removeLast(); - } - + outputs.removeLast(); return estimateTxForAmount( credentialsAmount, outputs, feeRate, - inputsCount: utxos.length + 1, + inputsCount: utxoDetails.utxos.length + 1, memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + hasSilentPayment: hasSilentPayment, ); } } return EstimatedTxResult( - utxos: utxos, - privateKeys: privateKeys, - publicKeys: publicKeys, + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, fee: fee, amount: amount, hasChange: true, isSendAll: false, memo: memo, - spendsUnconfirmedTX: spendsUnconfirmedTX, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, ); } @@ -559,6 +831,7 @@ abstract class ElectrumWalletBase final memo = transactionCredentials.outputs.first.memo; int credentialsAmount = 0; + bool hasSilentPayment = false; for (final out in transactionCredentials.outputs) { final outputAmount = out.formattedCryptoAmount!; @@ -578,6 +851,10 @@ abstract class ElectrumWalletBase final address = addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network); + if (address is SilentPaymentAddress) { + hasSilentPayment = true; + } + if (sendAll) { // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent outputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); @@ -597,6 +874,7 @@ abstract class ElectrumWalletBase feeRateInt, memo: memo, credentialsAmount: credentialsAmount, + hasSilentPayment: hasSilentPayment, ); } else { estimatedTx = await estimateTxForAmount( @@ -604,6 +882,7 @@ abstract class ElectrumWalletBase outputs, feeRateInt, memo: memo, + hasSilentPayment: hasSilentPayment, ); } @@ -662,8 +941,8 @@ abstract class ElectrumWalletBase bool hasTaprootInputs = false; final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { - final key = estimatedTx.privateKeys - .firstWhereOrNull((element) => element.getPublic().toHex() == publicKey); + final key = estimatedTx.inputPrivKeyInfos + .firstWhereOrNull((element) => element.privkey.getPublic().toHex() == publicKey); if (key == null) { throw Exception("Cannot find private key"); @@ -671,9 +950,13 @@ abstract class ElectrumWalletBase if (utxo.utxo.isP2tr()) { hasTaprootInputs = true; - return key.signTapRoot(txDigest, sighash: sighash); + return key.privkey.signTapRoot( + txDigest, + sighash: sighash, + tweak: utxo.utxo.isSilentPayment != true, + ); } else { - return key.signInput(txDigest, sigHash: sighash); + return key.privkey.signInput(txDigest, sigHash: sighash); } }); @@ -690,6 +973,14 @@ abstract class ElectrumWalletBase hasTaprootInputs: hasTaprootInputs, )..addListener((transaction) async { transactionHistory.addOne(transaction); + if (estimatedTx.spendsSilentPayment) { + transactionHistory.transactions.values.forEach((tx) { + tx.unspents?.removeWhere( + (unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash)); + transactionHistory.addOne(tx); + }); + } + await updateBalance(); }); } catch (e) { @@ -723,6 +1014,8 @@ abstract class ElectrumWalletBase 'balance': balance[currency]?.toJSON(), 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, 'derivationPath': walletInfo.derivationInfo?.derivationPath, + 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), + 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), }); int feeRate(TransactionPriority priority) { @@ -827,35 +1120,40 @@ abstract class ElectrumWalletBase await transactionHistory.changePassword(password); } + @action @override - Future rescan({required int height}) async => throw UnimplementedError(); + Future rescan( + {required int height, int? chainTip, ScanData? scanData, bool? doSingleScan}) async { + silentPaymentsScanningActive = true; + _setListeners(height, doSingleScan: doSingleScan); + } @override Future close() async { try { await electrumClient.close(); } catch (_) {} + _autoSaveTimer?.cancel(); } Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - Future updateUnspent() async { + @action + Future updateAllUnspents() async { List updatedUnspentCoins = []; - final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); + if (hasSilentPaymentsScanning) { + // Update unspents stored from scanned silent payment transactions + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null) { + updatedUnspentCoins.addAll(tx.unspents!); + } + }); + } - await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient - .getListUnspentWithAddress(address.address, network) - .then((unspent) => Future.forEach>(unspent, (unspent) async { - try { - final coin = BitcoinUnspent.fromJSON(address, unspent); - final tx = await fetchTransactionInfo( - hash: coin.hash, height: 0, myAddresses: addressesSet); - coin.isChange = tx?.direction == TransactionDirection.outgoing; - coin.confirmations = tx?.confirmations; - updatedUnspentCoins.add(coin); - } catch (_) {} - })))); + await Future.wait(walletAddresses.allAddresses.map((address) async { + updatedUnspentCoins.addAll(await fetchUnspent(address)); + })); unspentCoins = updatedUnspentCoins; @@ -877,6 +1175,8 @@ abstract class ElectrumWalletBase coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; } else { _addCoinInfo(coin); } @@ -886,6 +1186,57 @@ abstract class ElectrumWalletBase await _refreshUnspentCoinsInfo(); } + @action + Future updateUnspents(BitcoinAddressRecord address) async { + final newUnspentCoins = await fetchUnspent(address); + + if (newUnspentCoins.isNotEmpty) { + unspentCoins.addAll(newUnspentCoins); + + newUnspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + _addCoinInfo(coin); + } + }); + } + } + + @action + Future> fetchUnspent(BitcoinAddressRecord address) async { + final unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + + List updatedUnspentCoins = []; + + await Future.wait(unspents.map((unspent) async { + try { + final coin = BitcoinUnspent.fromJSON(address, unspent); + final tx = await fetchTransactionInfo(hash: coin.hash, height: 0); + coin.isChange = address.isHidden; + coin.confirmations = tx?.confirmations; + + updatedUnspentCoins.add(coin); + } catch (_) {} + })); + + return updatedUnspentCoins; + } + + @action Future _addCoinInfo(BitcoinUnspent coin) async { final newInfo = UnspentCoinsInfo( walletId: id, @@ -897,6 +1248,7 @@ abstract class ElectrumWalletBase value: coin.value, vout: coin.vout, isChange: coin.isChange, + isSilentPayment: coin is BitcoinSilentPaymentsUnspent, ); await unspentCoinsInfo.add(newInfo); @@ -1112,8 +1464,9 @@ abstract class ElectrumWalletBase (await http.get(Uri.parse("https://blockstream.info/testnet/api/tx/$hash/status"))).body); time = status["block_time"] as int?; - final tip = await electrumClient.getCurrentBlockChainTip() ?? 0; - confirmations = tip - (status["block_height"] as int? ?? 0); + final height = status["block_height"] as int? ?? 0; + final tip = await getCurrentChainTip(); + if (tip > 0) confirmations = height > 0 ? tip - height + 1 : 0; } else { final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); @@ -1122,13 +1475,11 @@ abstract class ElectrumWalletBase confirmations = verboseTransaction['confirmations'] as int? ?? 0; } - final original = bitcoin_base.BtcTransaction.fromRaw(transactionHex); - final ins = []; + final original = BtcTransaction.fromRaw(transactionHex); + final ins = []; for (final vin in original.inputs) { - final txHex = await electrumClient.getTransactionHex(hash: vin.txId); - final tx = bitcoin_base.BtcTransaction.fromRaw(txHex); - ins.add(tx); + ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId))); } return ElectrumTransactionBundle( @@ -1140,18 +1491,15 @@ abstract class ElectrumWalletBase } Future fetchTransactionInfo( - {required String hash, - required int height, - required Set myAddresses, - bool? retryOnFailure}) async { + {required String hash, required int height, bool? retryOnFailure}) async { try { return ElectrumTransactionInfo.fromElectrumBundle( await getTransactionExpanded(hash: hash), walletInfo.type, network, - addresses: myAddresses, height: height); + addresses: addressesSet, height: height); } catch (e) { if (e is FormatException && retryOnFailure == true) { await Future.delayed(const Duration(seconds: 2)); - return fetchTransactionInfo(hash: hash, height: height, myAddresses: myAddresses); + return fetchTransactionInfo(hash: hash, height: height); } return null; } @@ -1161,39 +1509,15 @@ abstract class ElectrumWalletBase Future> fetchTransactions() async { try { final Map historiesWithDetails = {}; - final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); - final currentHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; - await Future.wait(ADDRESS_TYPES.map((type) { - final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); - - return Future.wait(addressesByType.map((addressRecord) async { - final history = await _fetchAddressHistory(addressRecord, addressesSet, currentHeight); - final balance = await electrumClient.getBalance(addressRecord.scriptHash!); - - if (history.isNotEmpty) { - addressRecord.txCount = history.length; - addressRecord.balance = balance['confirmed'] as int? ?? 0; - historiesWithDetails.addAll(history); - - final matchedAddresses = - addressesByType.where((addr) => addr.isHidden == addressRecord.isHidden); - - final isLastUsedAddress = - history.isNotEmpty && addressRecord.address == matchedAddresses.last.address; - - if (isLastUsedAddress) { - await walletAddresses.discoverAddresses( - matchedAddresses.toList(), - addressRecord.isHidden, - (address, addressesSet) => - _fetchAddressHistory(address, addressesSet, currentHeight) - .then((history) => history.isNotEmpty ? address.address : null), - type: type); - } - } - })); - })); + if (type == WalletType.bitcoin) { + await Future.wait(ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + } else if (type == WalletType.bitcoinCash) { + await fetchTransactionsForAddressType(historiesWithDetails, P2pkhAddressType.p2pkh); + } else if (type == WalletType.litecoin) { + await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh); + } return historiesWithDetails; } catch (e) { @@ -1202,13 +1526,59 @@ abstract class ElectrumWalletBase } } + Future fetchTransactionsForAddressType( + Map historiesWithDetails, + BitcoinAddressType type, + ) async { + final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); + final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true); + final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); + + await Future.wait(addressesByType.map((addressRecord) async { + final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); + + if (history.isNotEmpty) { + addressRecord.txCount = history.length; + historiesWithDetails.addAll(history); + + final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; + final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >= + matchedAddresses.length - + (addressRecord.isHidden + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + if (isUsedAddressUnderGap) { + final prevLength = walletAddresses.allAddresses.length; + + // Discover new addresses for the same address type until the gap limit is respected + await walletAddresses.discoverAddresses( + matchedAddresses.toList(), + addressRecord.isHidden, + (address) async { + await _subscribeForUpdates(); + return _fetchAddressHistory(address, await getCurrentChainTip()) + .then((history) => history.isNotEmpty ? address.address : null); + }, + type: type, + ); + + final newLength = walletAddresses.allAddresses.length; + + if (newLength > prevLength) { + await fetchTransactionsForAddressType(historiesWithDetails, type); + } + } + } + })); + } + Future> _fetchAddressHistory( - BitcoinAddressRecord addressRecord, Set addressesSet, int currentHeight) async { + BitcoinAddressRecord addressRecord, int? currentHeight) async { try { final Map historiesWithDetails = {}; - final history = await electrumClient - .getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network)); + final history = await electrumClient.getHistory(addressRecord.getScriptHash(network)); if (history.isNotEmpty) { addressRecord.setAsUsed(); @@ -1222,14 +1592,13 @@ abstract class ElectrumWalletBase if (height > 0) { storedTx.height = height; // the tx's block itself is the first confirmation so add 1 - storedTx.confirmations = currentHeight - height + 1; + if (currentHeight != null) storedTx.confirmations = currentHeight - height + 1; storedTx.isPending = storedTx.confirmations == 0; } historiesWithDetails[txid] = storedTx; } else { - final tx = await fetchTransactionInfo( - hash: txid, height: height, myAddresses: addressesSet, retryOnFailure: true); + final tx = await fetchTransactionInfo(hash: txid, height: height, retryOnFailure: true); if (tx != null) { historiesWithDetails[txid] = tx; @@ -1258,6 +1627,12 @@ abstract class ElectrumWalletBase return; } + transactionHistory.transactions.values.forEach((tx) async { + if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height > 0) { + tx.confirmations = await getCurrentChainTip() - tx.height + 1; + } + }); + _isTransactionUpdating = true; await fetchTransactions(); walletAddresses.updateReceiveAddresses(); @@ -1269,15 +1644,22 @@ abstract class ElectrumWalletBase } } - void _subscribeForUpdates() { - scriptHashes.forEach((sh) async { + Future _subscribeForUpdates() async { + final unsubscribedScriptHashes = walletAddresses.allAddresses.where( + (address) => !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)), + ); + + await Future.wait(unsubscribedScriptHashes.map((address) async { + final sh = address.getScriptHash(network); await _scripthashesUpdateSubject[sh]?.close(); - _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh); + _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh]?.listen((event) async { try { - await updateUnspent(); + await updateUnspents(address); + await updateBalance(); - await updateTransactions(); + + await _fetchAddressHistory(address, await getCurrentChainTip()); } catch (e, s) { print(e.toString()); _onError?.call(FlutterErrorDetails( @@ -1287,7 +1669,7 @@ abstract class ElectrumWalletBase )); } }); - }); + })); } Future _fetchBalances() async { @@ -1301,22 +1683,26 @@ abstract class ElectrumWalletBase } var totalFrozen = 0; - unspentCoinsInfo.values.forEach((info) { - unspentCoins.forEach((element) { - if (element.hash == info.hash && - element.vout == info.vout && - info.isFrozen && - element.bitcoinAddressRecord.address == info.address && - element.value == info.value) { - totalFrozen += element.value; - } - }); - }); - - final balances = await Future.wait(balanceFutures); var totalConfirmed = 0; var totalUnconfirmed = 0; + if (hasSilentPaymentsScanning) { + // Add values from unspent coins that are not fetched by the address list + // i.e. scanned silent payments + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null) { + tx.unspents!.forEach((unspent) { + if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + if (unspent.isFrozen) totalFrozen += unspent.value; + totalConfirmed += unspent.value; + } + }); + } + }); + } + + final balances = await Future.wait(balanceFutures); + for (var i = 0; i < balances.length; i++) { final addressRecord = addresses[i]; final balance = balances[i]; @@ -1363,6 +1749,29 @@ abstract class ElectrumWalletBase return base64Encode(HD.signMessage(message)); } + Future _setInitialHeight() async { + if (_chainTipUpdateSubject != null) return; + + if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) { + await getUpdatedChainTip(); + await walletInfo.updateRestoreHeight(_currentChainTip!); + } + + _chainTipUpdateSubject = electrumClient.chainTipSubscribe(); + _chainTipUpdateSubject?.listen((e) async { + final event = e as Map; + final height = int.tryParse(event['height'].toString()); + + if (height != null) { + _currentChainTip = height; + + if (alwaysScan == true && syncStatus is SyncedSyncStatus) { + _setListeners(walletInfo.restoreHeight); + } + } + }); + } + static BasedUtxoNetwork _getNetwork(bitcoin.NetworkType networkType, CryptoCurrency? currency) { if (networkType == bitcoin.bitcoin && currency == CryptoCurrency.bch) { return BitcoinCashNetwork.mainnet; @@ -1383,39 +1792,248 @@ abstract class ElectrumWalletBase derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); } -class EstimateTxParams { - EstimateTxParams( - {required this.amount, - required this.feeRate, - required this.priority, - required this.outputsCount, - required this.size}); +class ScanNode { + final Uri uri; + final bool? useSSL; - final int amount; - final int feeRate; - final TransactionPriority priority; - final int outputsCount; - final int size; + ScanNode(this.uri, this.useSSL); +} + +class ScanData { + final SendPort sendPort; + final SilentPaymentOwner silentAddress; + final int height; + final ScanNode node; + final BasedUtxoNetwork network; + final int chainTip; + final ElectrumClient electrumClient; + final List transactionHistoryIds; + final Map labels; + final List labelIndexes; + final bool isSingleScan; + + ScanData({ + required this.sendPort, + required this.silentAddress, + required this.height, + required this.node, + required this.network, + required this.chainTip, + required this.electrumClient, + required this.transactionHistoryIds, + required this.labels, + required this.labelIndexes, + required this.isSingleScan, + }); + + factory ScanData.fromHeight(ScanData scanData, int newHeight) { + return ScanData( + sendPort: scanData.sendPort, + silentAddress: scanData.silentAddress, + height: newHeight, + node: scanData.node, + network: scanData.network, + chainTip: scanData.chainTip, + transactionHistoryIds: scanData.transactionHistoryIds, + electrumClient: scanData.electrumClient, + labels: scanData.labels, + labelIndexes: scanData.labelIndexes, + isSingleScan: scanData.isSingleScan, + ); + } +} + +class SyncResponse { + final int height; + final SyncStatus syncStatus; + + SyncResponse(this.height, this.syncStatus); +} + +Future startRefresh(ScanData scanData) async { + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; + + BehaviorSubject? tweaksSubscription = null; + + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + + // Initial status UI update, send how many blocks left to scan + scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + + final electrumClient = scanData.electrumClient; + await electrumClient.connectToUri(scanData.node.uri, useSSL: scanData.node.useSSL); + + if (tweaksSubscription == null) { + final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT; + final receiver = Receiver( + scanData.silentAddress.b_scan.toHex(), + scanData.silentAddress.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ); + + tweaksSubscription = await electrumClient.tweaksSubscribe(height: syncHeight, count: count); + tweaksSubscription?.listen((t) async { + final tweaks = t as Map; + + if (tweaks["message"] != null) { + // re-subscribe to continue receiving messages + electrumClient.tweaksSubscribe(height: syncHeight, count: count); + return; + } + + final blockHeight = tweaks.keys.first; + final tweakHeight = int.parse(blockHeight); + + try { + final blockTweaks = tweaks[blockHeight] as Map; + + for (var j = 0; j < blockTweaks.keys.length; j++) { + final txid = blockTweaks.keys.elementAt(j); + final details = blockTweaks[txid] as Map; + final outputPubkeys = (details["output_pubkeys"] as Map); + final tweak = details["tweak"].toString(); + + try { + // scanOutputs called from rust here + final addToWallet = scanOutputs( + outputPubkeys.values.toList(), + tweak, + receiver, + ); + + if (addToWallet.isEmpty) { + // no results tx, continue to next tx + continue; + } + + // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) + final txInfo = ElectrumTransactionInfo( + WalletType.bitcoin, + id: txid, + height: tweakHeight, + amount: 0, + fee: 0, + direction: TransactionDirection.incoming, + isPending: false, + date: scanData.network == BitcoinNetwork.mainnet + ? getDateByBitcoinHeight(tweakHeight) + : DateTime.now(), + confirmations: scanData.chainTip - tweakHeight + 1, + unspents: [], + ); + + addToWallet.forEach((label, value) { + (value as Map).forEach((output, tweak) { + final t_k = tweak.toString(); + + final receivingOutputAddress = ECPublic.fromHex(output) + .toTaprootAddress(tweak: false) + .toAddress(scanData.network); + + int? amount; + int? pos; + outputPubkeys.entries.firstWhere((k) { + final isMatchingOutput = k.value[0] == output; + if (isMatchingOutput) { + amount = int.parse(k.value[1].toString()); + pos = int.parse(k.key.toString()); + return true; + } + return false; + }); + + final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( + receivingOutputAddress, + index: 0, + isHidden: false, + isUsed: true, + network: scanData.network, + silentPaymentTweak: t_k, + type: SegwitAddresType.p2tr, + txCount: 1, + balance: amount!, + ); + + final unspent = BitcoinSilentPaymentsUnspent( + receivedAddressRecord, + txid, + amount!, + pos!, + silentPaymentTweak: t_k, + silentPaymentLabel: label == "None" ? null : label, + ); + + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); + }); + + scanData.sendPort.send({txInfo.id: txInfo}); + } catch (_) {} + } + } catch (_) {} + + syncHeight = tweakHeight; + scanData.sendPort.send( + SyncResponse( + syncHeight, + SyncingSyncStatus.fromHeightValues( + scanData.chainTip, + initialSyncHeight, + syncHeight, + ), + ), + ); + + if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + if (tweakHeight >= scanData.chainTip) + scanData.sendPort.send(SyncResponse( + syncHeight, + SyncedTipSyncStatus(scanData.chainTip), + )); + + if (scanData.isSingleScan) { + scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); + } + + await tweaksSubscription!.close(); + await electrumClient.close(); + } + }); + } + + if (tweaksSubscription == null) { + return scanData.sendPort.send( + SyncResponse(syncHeight, UnsupportedSyncStatus()), + ); + } } class EstimatedTxResult { EstimatedTxResult({ required this.utxos, - required this.privateKeys, + required this.inputPrivKeyInfos, required this.publicKeys, required this.fee, required this.amount, required this.hasChange, required this.isSendAll, this.memo, + required this.spendsSilentPayment, required this.spendsUnconfirmedTX, }); final List utxos; - final List privateKeys; + final List inputPrivKeyInfos; final Map publicKeys; // PubKey to derivationPath final int fee; final int amount; + final bool spendsSilentPayment; final bool hasChange; final bool isSendAll; final String? memo; @@ -1447,6 +2065,8 @@ BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) return P2wshAddress.fromAddress(address: address, network: network); } else if (P2trAddress.regex.hasMatch(address)) { return P2trAddress.fromAddress(address: address, network: network); + } else if (SilentPaymentAddress.regex.hasMatch(address)) { + return SilentPaymentAddress.fromAddress(address); } else { return P2wpkhAddress.fromAddress(address: address, network: network); } @@ -1461,7 +2081,33 @@ BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { return SegwitAddresType.p2wsh; } else if (type is P2trAddress) { return SegwitAddresType.p2tr; + } else if (type is SilentPaymentsAddresType) { + return SilentPaymentsAddresType.p2sp; } else { return SegwitAddresType.p2wpkh; } } + +class UtxoDetails { + final List availableInputs; + final List unconfirmedCoins; + final List utxos; + final List vinOutpoints; + final List inputPrivKeyInfos; + final Map publicKeys; // PubKey to derivationPath + final int allInputsAmount; + final bool spendsSilentPayment; + final bool spendsUnconfirmedTX; + + UtxoDetails({ + required this.availableInputs, + required this.unconfirmedCoins, + required this.utxos, + required this.vinOutpoints, + required this.inputPrivKeyInfos, + required this.publicKeys, + required this.allInputsAmount, + required this.spendsSilentPayment, + required this.spendsUnconfirmedTX, + }); +} diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index c43d4988a..e0857a6d0 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,7 +1,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -24,15 +24,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { WalletInfo walletInfo, { required this.mainHd, required this.sideHd, - required this.electrumClient, required this.network, List? initialAddresses, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + List? initialSilentAddresses, + int initialSilentAddressIndex = 0, + bitcoin.HDWallet? masterHd, BitcoinAddressType? initialAddressPageType, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = - ObservableList.of(([]).toSet()), + ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) .toSet()), @@ -45,7 +47,38 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { (walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddresType.p2wpkh), + silentAddresses = ObservableList.of( + (initialSilentAddresses ?? []).toSet()), + currentSilentAddressIndex = initialSilentAddressIndex, super(walletInfo) { + if (masterHd != null) { + silentAddress = SilentPaymentOwner.fromPrivateKeys( + b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privKey!), + b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privKey!), + hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'); + + if (silentAddresses.length == 0) { + silentAddresses.add(BitcoinSilentPaymentAddressRecord( + silentAddress.toString(), + index: 0, + isHidden: false, + name: "", + silentPaymentTweak: null, + network: network, + type: SilentPaymentsAddresType.p2sp, + )); + silentAddresses.add(BitcoinSilentPaymentAddressRecord( + silentAddress!.toLabeledSilentPaymentAddress(0).toString(), + index: 0, + isHidden: true, + name: "", + silentPaymentTweak: BytesUtils.toHexString(silentAddress!.generateLabel(0)), + network: network, + type: SilentPaymentsAddresType.p2sp, + )); + } + } + updateAddressesByMatch(); } @@ -54,27 +87,40 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const gap = 20; final ObservableList _addresses; - // Matched by addressPageType - late ObservableList addressesByReceiveType; + late ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; - final ElectrumClient electrumClient; + final ObservableList silentAddresses; final BasedUtxoNetwork network; final bitcoin.HDWallet mainHd; final bitcoin.HDWallet sideHd; + @observable + SilentPaymentOwner? silentAddress; + @observable late BitcoinAddressType _addressPageType; @computed BitcoinAddressType get addressPageType => _addressPageType; + @observable + String? activeSilentAddress; + @computed List get allAddresses => _addresses; @override @computed String get address { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + if (activeSilentAddress != null) { + return activeSilentAddress!; + } + + return silentAddress.toString(); + } + String receiveAddress; final typeMatchingReceiveAddresses = @@ -103,6 +149,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); + + if (selected.silentPaymentTweak != null && silentAddress != null) { + activeSilentAddress = + silentAddress!.toLabeledSilentPaymentAddress(selected.index).toString(); + } else { + activeSilentAddress = silentAddress!.toString(); + } + return; + } + final addressRecord = _addresses.firstWhere((addressRecord) => addressRecord.address == addr); previousAddressRecord = addressRecord; @@ -129,6 +187,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { void set currentChangeAddressIndex(int index) => currentChangeAddressIndexByType[_addressPageType.toString()] = index; + int currentSilentAddressIndex; + @observable BitcoinAddressRecord? previousAddressRecord; @@ -196,7 +256,50 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - BitcoinAddressRecord generateNewAddress({String label = ''}) { + Map get labels { + final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32)); + final labels = {}; + for (int i = 0; i < silentAddresses.length; i++) { + final silentAddressRecord = silentAddresses[i]; + final silentPaymentTweak = silentAddressRecord.silentPaymentTweak; + + if (silentPaymentTweak != null && + SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) { + labels[G + .tweakMul(BigintUtils.fromBytes(BytesUtils.fromHexString(silentPaymentTweak))) + .toHex()] = silentPaymentTweak; + } + } + return labels; + } + + @action + BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { + if (addressPageType == SilentPaymentsAddresType.p2sp && silentAddress != null) { + final currentSilentAddressIndex = silentAddresses + .where((addressRecord) => addressRecord.type != SegwitAddresType.p2tr) + .length - + 1; + + this.currentSilentAddressIndex = currentSilentAddressIndex; + + final address = BitcoinSilentPaymentAddressRecord( + silentAddress!.toLabeledSilentPaymentAddress(currentSilentAddressIndex).toString(), + index: currentSilentAddressIndex, + isHidden: false, + name: label, + silentPaymentTweak: + BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)), + network: network, + type: SilentPaymentsAddresType.p2sp, + ); + + silentAddresses.add(address); + updateAddressesByMatch(); + + return address; + } + final newAddressIndex = addressesByReceiveType.fold( 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc); @@ -221,12 +324,70 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { Future updateAddressesInBox() async { try { addressesMap.clear(); - addressesMap[address] = ''; + addressesMap[address] = 'Active'; allAddressesMap.clear(); _addresses.forEach((addressRecord) { allAddressesMap[addressRecord.address] = addressRecord.name; }); + + final lastP2wpkh = _addresses + .where((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastP2pkh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); + if (lastP2pkh.address != address) { + addressesMap[lastP2pkh.address] = 'P2PKH'; + } else { + addressesMap[address] = 'Active - P2PKH'; + } + + final lastP2sh = _addresses.firstWhere((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); + if (lastP2sh.address != address) { + addressesMap[lastP2sh.address] = 'P2SH'; + } else { + addressesMap[address] = 'Active - P2SH'; + } + + final lastP2tr = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); + if (lastP2tr.address != address) { + addressesMap[lastP2tr.address] = 'P2TR'; + } else { + addressesMap[address] = 'Active - P2TR'; + } + + final lastP2wsh = _addresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); + if (lastP2wsh.address != address) { + addressesMap[lastP2wsh.address] = 'P2WSH'; + } else { + addressesMap[address] = 'Active - P2WSH'; + } + + silentAddresses.forEach((addressRecord) { + if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { + return; + } + + if (addressRecord.address != address) { + addressesMap[addressRecord.address] = addressRecord.name.isEmpty + ? "Silent Payments" + : "Silent Payments - " + addressRecord.name; + } else { + addressesMap[address] = 'Active - Silent Payments'; + } + }); + await saveAddressesInBox(); } catch (e) { print(e.toString()); @@ -235,18 +396,41 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void updateAddress(String address, String label) { - final addressRecord = - _addresses.firstWhere((addressRecord) => addressRecord.address == address); - addressRecord.setNewName(label); - final index = _addresses.indexOf(addressRecord); - _addresses.remove(addressRecord); - _addresses.insert(index, addressRecord); + BaseBitcoinAddressRecord? foundAddress; + _addresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); + silentAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); - updateAddressesByMatch(); + if (foundAddress != null) { + foundAddress!.setNewName(label); + + if (foundAddress is BitcoinAddressRecord) { + final index = _addresses.indexOf(foundAddress); + _addresses.remove(foundAddress); + _addresses.insert(index, foundAddress as BitcoinAddressRecord); + } else { + final index = silentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord); + silentAddresses.remove(foundAddress); + silentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord); + } + } } @action void updateAddressesByMatch() { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + addressesByReceiveType.clear(); + addressesByReceiveType.addAll(silentAddresses); + return; + } + addressesByReceiveType.clear(); addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList()); } @@ -272,7 +456,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future discoverAddresses(List addressList, bool isHidden, - Future Function(BitcoinAddressRecord, Set) getAddressHistory, + Future Function(BitcoinAddressRecord) getAddressHistory, {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { if (!isHidden) { _validateSideHdAddresses(addressList.toList()); @@ -282,8 +466,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { startIndex: addressList.length, isHidden: isHidden, type: type); addAddresses(newAddresses); - final addressesWithHistory = await Future.wait(newAddresses - .map((addr) => getAddressHistory(addr, _addresses.map((e) => e.address).toSet()))); + final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory)); final isLastAddressUsed = addressesWithHistory.last == addressList.last.address; if (isLastAddressUsed) { @@ -349,6 +532,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } + @action + void addSilentAddresses(Iterable addresses) { + final addressesSet = this.silentAddresses.toSet(); + addressesSet.addAll(addresses); + this.silentAddresses.clear(); + this.silentAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + void _validateSideHdAddresses(List addrWithTransactions) { addrWithTransactions.forEach((element) { if (element.address != @@ -371,4 +563,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { bitcoin.HDWallet _getHd(bool isHidden) => isHidden ? sideHd : mainHd; bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => + !addr.isHidden && !addr.isUsed && addr.type == type; + + @action + void deleteSilentPaymentAddress(String address) { + final addressRecord = silentAddresses.firstWhere((addressRecord) => + addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address); + + silentAddresses.remove(addressRecord); + updateAddressesByMatch(); + } } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 340b17cfb..3e3f39131 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -19,6 +19,8 @@ class ElectrumWalletSnapshot { required this.regularAddressIndex, required this.changeAddressIndex, required this.addressPageType, + required this.silentAddresses, + required this.silentAddressIndex, this.passphrase, this.derivationType, this.derivationPath, @@ -32,9 +34,11 @@ class ElectrumWalletSnapshot { String? mnemonic; String? xpub; List addresses; + List silentAddresses; ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; + int silentAddressIndex; String? passphrase; DerivationType? derivationType; String? derivationPath; @@ -50,15 +54,23 @@ class ElectrumWalletSnapshot { final passphrase = data['passphrase'] as String? ?? ''; final addresses = addressesTmp .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr, network)) + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) .toList(); - final balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? + + final silentAddressesTmp = data['silent_addresses'] as List? ?? []; + final silentAddresses = silentAddressesTmp + .whereType() + .map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network)) + .toList(); + + final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; + var silentAddressIndex = 0; - final derivationType = - DerivationType.values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; + final derivationType = DerivationType + .values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; final derivationPath = data['derivationPath'] as String? ?? "m/0'/0"; try { @@ -69,6 +81,7 @@ class ElectrumWalletSnapshot { SegwitAddresType.p2wpkh.toString(): int.parse(data['change_address_index'] as String? ?? '0') }; + silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); } catch (_) { try { regularAddressIndexByType = data["account_index"] as Map? ?? {}; @@ -90,6 +103,8 @@ class ElectrumWalletSnapshot { addressPageType: data['address_page_type'] as String?, derivationType: derivationType, derivationPath: derivationPath, + silentAddresses: silentAddresses, + silentAddressIndex: silentAddressIndex, ); } } diff --git a/cw_bitcoin/lib/exceptions.dart b/cw_bitcoin/lib/exceptions.dart index 979c1a433..3307bfeed 100644 --- a/cw_bitcoin/lib/exceptions.dart +++ b/cw_bitcoin/lib/exceptions.dart @@ -2,7 +2,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/exceptions.dart'; class BitcoinTransactionWrongBalanceException extends TransactionWrongBalanceException { - BitcoinTransactionWrongBalanceException() : super(CryptoCurrency.btc); + BitcoinTransactionWrongBalanceException({super.amount}) : super(CryptoCurrency.btc); } class BitcoinTransactionNoInputsException extends TransactionNoInputsException {} @@ -27,3 +27,7 @@ class BitcoinTransactionCommitFailedDustOutputSendAll extends TransactionCommitFailedDustOutputSendAll {} class BitcoinTransactionCommitFailedVoutNegative extends TransactionCommitFailedVoutNegative {} + +class BitcoinTransactionCommitFailedBIP68Final extends TransactionCommitFailedBIP68Final {} + +class BitcoinTransactionSilentPaymentsNotSupported extends TransactionInputNotSupported {} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 9cc2072ca..209ddc774 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -44,7 +44,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { currency: CryptoCurrency.ltc) { walletAddresses = LitecoinWalletAddresses( walletInfo, - electrumClient: electrumClient, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 993d17933..99b7445fc 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -15,7 +15,6 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required super.mainHd, required super.sideHd, required super.network, - required super.electrumClient, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index a59b4f429..6a4cd1741 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -73,6 +73,11 @@ class PendingBitcoinTransaction with PendingTransaction { if (error.contains("bad-txns-vout-negative")) { throw BitcoinTransactionCommitFailedVoutNegative(); } + + if (error.contains("non-BIP68-final")) { + throw BitcoinTransactionCommitFailedBIP68Final(); + } + throw BitcoinTransactionCommitFailed(errorMessage: error); } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 7690c9c85..3eadcb112 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: c9c85fedbe2188b95133cbe960e16f5f448860f7133330e272edbbca5893ddc6 + sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "1.5.3" async: dependency: transitive description: @@ -79,11 +79,11 @@ packages: dependency: "direct main" description: path: "." - ref: cake-update-v2 - resolved-ref: "01d844a5f5a520a31df5254e34169af4664aa769" - url: "https://github.com/cake-tech/bitcoin_base.git" + ref: cake-update-v3 + resolved-ref: cc99eedb1d28ee9376dda0465ef72aa627ac6149 + url: "https://github.com/cake-tech/bitcoin_base" source: git - version: "4.2.0" + version: "4.2.1" bitcoin_flutter: dependency: "direct main" description: @@ -96,11 +96,12 @@ packages: blockchain_utils: dependency: "direct main" description: - name: blockchain_utils - sha256: "38ef5f4a22441ac4370aed9071dc71c460acffc37c79b344533f67d15f24c13c" - url: "https://pub.dev" - source: hosted - version: "2.1.1" + path: "." + ref: cake-update-v1 + resolved-ref: cabd7e0e16c4da9920338c76eff3aeb8af0211f3 + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "2.1.2" boolean_selector: dependency: transitive description: @@ -197,6 +198,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" clock: dependency: transitive description: @@ -241,10 +250,10 @@ packages: dependency: "direct main" description: name: cryptography - sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35 + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" cw_core: dependency: "direct main" description: @@ -288,10 +297,18 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" + ffigen: + dependency: transitive + description: + name: ffigen + sha256: d3e76c2ad48a4e7f93a29a162006f00eba46ce7c08194a77bb5c5e97d1b5ff0a + url: "https://pub.dev" + source: hosted + version: "8.0.2" file: dependency: transitive description: @@ -346,10 +363,10 @@ packages: dependency: transitive description: name: functional_data - sha256: aefdec4365452283b2a7cf420a3169654d51d3e9553069a22d76680d7a9d7c3d + sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" glob: dependency: transitive description: @@ -394,10 +411,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -442,10 +459,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -531,10 +548,10 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mobx: dependency: "direct main" description: @@ -579,26 +596,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -643,10 +660,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.9.1" pool: dependency: transitive description: @@ -687,6 +704,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" reactive_ble_mobile: dependency: transitive description: @@ -736,10 +761,10 @@ packages: dependency: transitive description: name: socks5_proxy - sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + sha256: "045cbba84f6e2b01c1c77634a63e926352bf110ef5f07fc462c6d43bbd4b6a83" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5+dev.2" source_gen: dependency: transitive description: @@ -764,6 +789,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sp_scanner: + dependency: "direct main" + description: + path: "." + ref: "sp_v1.0.0" + resolved-ref: a9a4c6d051f37a15a3a52cc2a0094f24c68b62c5 + url: "https://github.com/cake-tech/sp_scanner" + source: git + version: "0.0.1" stack_trace: dependency: transitive description: @@ -860,22 +894,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" win32: dependency: transitive description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.5.0" xdg_directories: dependency: transitive description: @@ -892,6 +934,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: e9c1a3543d2da0db3e90270dbb1e4eebc985ee5e3ffe468d83224472b2194a5f + url: "https://pub.dev" + source: hosted + version: "2.2.1" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.16.6" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 265d2f9a2..40f3c6e29 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -32,13 +32,21 @@ dependencies: cryptography: ^2.0.5 bitcoin_base: git: - url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v2 - blockchain_utils: ^2.1.1 + url: https://github.com/cake-tech/bitcoin_base + ref: cake-update-v3 + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v1 ledger_flutter: ^1.0.1 ledger_bitcoin: git: url: https://github.com/cake-tech/ledger-bitcoin + sp_scanner: + git: + url: https://github.com/cake-tech/sp_scanner + ref: sp_v2.0.0 + dev_dependencies: flutter_test: diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index d58144f1e..51bd3612d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -46,7 +46,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { currency: CryptoCurrency.bch) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, - electrumClient: electrumClient, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index 3164651f3..d543e944c 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -15,7 +15,6 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required super.mainHd, required super.sideHd, required super.network, - required super.electrumClient, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index ceef539c3..a0ce889c1 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -31,10 +31,12 @@ dependencies: ref: Add-Support-For-OP-Return-data bitcoin_base: git: - url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v2 - - + url: https://github.com/cake-tech/bitcoin_base + ref: cake-update-v3 + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v1 dev_dependencies: flutter_test: diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index fb702eca1..2bd4eaf91 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -104,6 +104,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.digibyte, CryptoCurrency.usdtSol, CryptoCurrency.usdcTrc20, + CryptoCurrency.tbtc, ]; static const havenCurrencies = [ @@ -218,7 +219,8 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); - static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8); static final Map _rawCurrencyMap = diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 92e78b2e6..8cf438769 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -1,9 +1,12 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_type.dart'; -CryptoCurrency currencyForWalletType(WalletType type) { +CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { switch (type) { case WalletType.bitcoin: + if (isTestnet == true) { + return CryptoCurrency.tbtc; + } return CryptoCurrency.btc; case WalletType.monero: return CryptoCurrency.xmr; diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index d07da8109..dccacd799 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -1,9 +1,10 @@ import 'package:cw_core/crypto_currency.dart'; class TransactionWrongBalanceException implements Exception { - TransactionWrongBalanceException(this.currency); + TransactionWrongBalanceException(this.currency, {this.amount}); final CryptoCurrency currency; + final int? amount; } class TransactionNoInputsException implements Exception {} @@ -32,3 +33,7 @@ class TransactionCommitFailedDustOutput implements Exception {} class TransactionCommitFailedDustOutputSendAll implements Exception {} class TransactionCommitFailedVoutNegative implements Exception {} + +class TransactionCommitFailedBIP68Final implements Exception {} + +class TransactionInputNotSupported implements Exception {} diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 6f3ccaf68..a3dd51b68 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -242,3 +242,57 @@ Future getHavenCurrentHeight() async { throw Exception('Failed to load current blockchain height'); } } + +// Data taken from https://timechaincalendar.com/ +const bitcoinDates = { + "2024-05": 841590, + "2024-04": 837182, + "2024-03": 832623, + "2024-02": 828319, + "2024-01": 823807, + "2023-12": 819206, + "2023-11": 814765, + "2023-10": 810098, + "2023-09": 805675, + "2023-08": 801140, + "2023-07": 796640, + "2023-06": 792330, + "2023-05": 787733, + "2023-04": 783403, + "2023-03": 778740, + "2023-02": 774525, + "2023-01": 769810, +}; + +int getBitcoinHeightByDate({required DateTime date}) { + String dateKey = '${date.year}-${date.month.toString().padLeft(2, '0')}'; + final closestKey = bitcoinDates.keys + .firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => bitcoinDates.keys.last); + final beginningBlock = bitcoinDates[dateKey] ?? bitcoinDates[closestKey]!; + + final startOfMonth = DateTime(date.year, date.month); + final daysDifference = date.difference(startOfMonth).inDays; + + // approximately 6 blocks per hour, 24 hours per day + int estimatedBlocksSinceStartOfMonth = (daysDifference * 24 * 6); + + return beginningBlock + estimatedBlocksSinceStartOfMonth; +} + +DateTime getDateByBitcoinHeight(int height) { + final closestEntry = bitcoinDates.entries + .lastWhere((entry) => entry.value >= height, orElse: () => bitcoinDates.entries.first); + final beginningBlock = closestEntry.value; + + final startOfMonth = formatMapKey(closestEntry.key); + final blocksDifference = height - beginningBlock; + final hoursDifference = blocksDifference / 5.5; + + final estimatedDate = startOfMonth.add(Duration(hours: hoursDifference.ceil())); + + if (estimatedDate.isAfter(DateTime.now())) { + return DateTime.now(); + } + + return estimatedDate; +} diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 1195b6819..00b2c51f1 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -244,8 +244,12 @@ class Node extends HiveObject with Keyable { Future requestElectrumServer() async { try { - await SecureSocket.connect(uri.host, uri.port, - timeout: Duration(seconds: 5), onBadCertificate: (_) => true); + if (useSSL == true) { + await SecureSocket.connect(uri.host, uri.port, + timeout: Duration(seconds: 5), onBadCertificate: (_) => true); + } else { + await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + } return true; } catch (_) { return false; diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 4983967d0..55c31877f 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -14,6 +14,16 @@ class SyncingSyncStatus extends SyncStatus { @override String toString() => '$blocksLeft'; + + factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) { + final track = chainTip - initialSyncHeight; + final diff = track - (chainTip - syncHeight); + final ptc = diff <= 0 ? 0.0 : diff / track; + final left = chainTip - syncHeight; + + // sum 1 because if at the chain tip, will say "0 blocks left" + return SyncingSyncStatus(left + 1, ptc); + } } class SyncedSyncStatus extends SyncStatus { @@ -21,6 +31,17 @@ class SyncedSyncStatus extends SyncStatus { double progress() => 1.0; } +class SyncedTipSyncStatus extends SyncedSyncStatus { + SyncedTipSyncStatus(this.tip); + + final int tip; +} + +class SyncronizingSyncStatus extends SyncStatus { + @override + double progress() => 0.0; +} + class NotConnectedSyncStatus extends SyncStatus { const NotConnectedSyncStatus(); @@ -33,10 +54,7 @@ class AttemptingSyncStatus extends SyncStatus { double progress() => 0.0; } -class FailedSyncStatus extends SyncStatus { - @override - double progress() => 1.0; -} +class FailedSyncStatus extends NotConnectedSyncStatus {} class ConnectingSyncStatus extends SyncStatus { @override @@ -48,7 +66,14 @@ class ConnectedSyncStatus extends SyncStatus { double progress() => 0.0; } -class LostConnectionSyncStatus extends SyncStatus { +class UnsupportedSyncStatus extends NotConnectedSyncStatus {} + +class TimedOutSyncStatus extends NotConnectedSyncStatus { @override - double progress() => 1.0; -} \ No newline at end of file + String toString() => 'Timed out'; +} + +class LostConnectionSyncStatus extends NotConnectedSyncStatus { + @override + String toString() => 'Reconnecting'; +} diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 25abd3e48..ed09e17e0 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -16,7 +16,8 @@ class UnspentCoinsInfo extends HiveObject { required this.value, this.keyImage = null, this.isChange = false, - this.accountIndex = 0 + this.accountIndex = 0, + this.isSilentPayment = false, }); static const typeId = UNSPENT_COINS_INFO_TYPE_ID; @@ -49,13 +50,16 @@ class UnspentCoinsInfo extends HiveObject { @HiveField(8, defaultValue: null) String? keyImage; - + @HiveField(9, defaultValue: false) bool isChange; @HiveField(10, defaultValue: 0) int accountIndex; + @HiveField(11, defaultValue: false) + bool? isSilentPayment; + String get note => noteRaw ?? ''; set note(String value) => noteRaw = value; diff --git a/cw_core/lib/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart index 595df18f4..d225493e9 100644 --- a/cw_core/lib/unspent_transaction_output.dart +++ b/cw_core/lib/unspent_transaction_output.dart @@ -17,5 +17,6 @@ class Unspent { int? confirmations; String note; - bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); + bool get isP2wpkh => + address.startsWith('bc') || address.startsWith('tb') || address.startsWith('ltc'); } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 709462fa1..a616b0bfd 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -24,7 +24,7 @@ abstract class WalletBase walletInfo.type; - CryptoCurrency get currency => currencyForWalletType(type); + CryptoCurrency get currency => currencyForWalletType(type, isTestnet: isTestnet); String get id => walletInfo.id; diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 57cdad81b..ff0c011bb 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -66,21 +66,21 @@ class DerivationInfo extends HiveObject { @HiveType(typeId: WalletInfo.typeId) class WalletInfo extends HiveObject { WalletInfo( - this.id, - this.name, - this.type, - this.isRecovery, - this.restoreHeight, - this.timestamp, - this.dirPath, - this.path, - this.address, - this.yatEid, - this.yatLastUsedAddressRaw, - this.showIntroCakePayCard, - this.derivationInfo, - this.hardwareWalletType, - ): _yatLastUsedAddressController = StreamController.broadcast(); + this.id, + this.name, + this.type, + this.isRecovery, + this.restoreHeight, + this.timestamp, + this.dirPath, + this.path, + this.address, + this.yatEid, + this.yatLastUsedAddressRaw, + this.showIntroCakePayCard, + this.derivationInfo, + this.hardwareWalletType, + ) : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ required String id, @@ -207,4 +207,9 @@ class WalletInfo extends HiveObject { Stream get yatLastUsedAddressStream => _yatLastUsedAddressController.stream; StreamController _yatLastUsedAddressController; + + Future updateRestoreHeight(int height) async { + restoreHeight = height; + await save(); + } } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index e846093d0..b3e41a989 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -173,11 +173,14 @@ String walletTypeToDisplayName(WalletType type) { } } -CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { +CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = false}) { switch (type) { case WalletType.monero: return CryptoCurrency.xmr; case WalletType.bitcoin: + if (isTestnet) { + return CryptoCurrency.tbtc; + } return CryptoCurrency.btc; case WalletType.litecoin: return CryptoCurrency.ltc; diff --git a/cw_shared_external/pubspec.lock b/cw_shared_external/pubspec.lock deleted file mode 100644 index ef01c9f9a..000000000 --- a/cw_shared_external/pubspec.lock +++ /dev/null @@ -1,147 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.5.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.19" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" -sdks: - dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index 9d32c4290..f27e1b460 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -15,8 +15,14 @@ dependencies: path: ../cw_core cw_evm: path: ../cw_evm - on_chain: ^3.0.1 - blockchain_utils: ^2.1.1 + on_chain: + git: + url: https://github.com/cake-tech/On_chain + ref: cake-update-v1 + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v1 mobx: ^2.3.0+1 bip39: ^1.0.6 hive: ^2.2.3 diff --git a/howto-build-android.md b/howto-build-android.md index a2a4e4d9f..c3fe415ee 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -142,27 +142,9 @@ Then we need to generate localization files. `$ flutter packages pub run tool/generate_localization.dart` -Lastly, we will generate mobx models for the project. - -Generate mobx models for `cw_core`: - -`cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_monero`: - -`cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_bitcoin`: - -`cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_haven`: - -`cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - Finally build mobx models for the app: -`$ flutter packages pub run build_runner build --delete-conflicting-outputs` +`$ ./model_generator.sh` ### 9. Build! diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c4ee98c37..170db929c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -147,6 +147,8 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sp_scanner (0.0.1): + - Flutter - SwiftProtobuf (1.25.2) - SwiftyGif (5.4.4) - Toast (4.1.0) @@ -188,6 +190,7 @@ DEPENDENCIES: - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -259,6 +262,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sp_scanner: + :path: ".symlinks/plugins/sp_scanner/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: @@ -302,6 +307,7 @@ SPEC CHECKSUMS: sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f Toast: ec33c32b8688982cecc6348adeae667c1b9938da diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 082e9b22f..634919952 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -121,20 +121,12 @@ class CWBitcoin extends Bitcoin { priority: priority != null ? priority as BitcoinTransactionPriority : null, feeRate: feeRate); - @override - List getAddresses(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.walletAddresses.addressesByReceiveType - .map((BitcoinAddressRecord addr) => addr.address) - .toList(); - } - @override @computed List getSubAddresses(Object wallet) { final electrumWallet = wallet as ElectrumWallet; return electrumWallet.walletAddresses.addressesByReceiveType - .map((BitcoinAddressRecord addr) => ElectrumSubAddress( + .map((BaseBitcoinAddressRecord addr) => ElectrumSubAddress( id: addr.index, name: addr.name, address: addr.address, @@ -207,12 +199,12 @@ class CWBitcoin extends Bitcoin { Future updateUnspents(Object wallet) async { final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.updateUnspent(); + await bitcoinWallet.updateAllUnspents(); } WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource) { - return BitcoinWalletService(walletInfoSource, unspentCoinSource); + Box walletInfoSource, Box unspentCoinSource, bool alwaysScan) { + return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan); } WalletService createLitecoinWalletService( @@ -247,6 +239,12 @@ class CWBitcoin extends Bitcoin { return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType); } + @override + bool hasSelectedSilentPayments(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.addressPageType == SilentPaymentsAddresType.p2sp; + } + @override List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; @@ -465,4 +463,137 @@ class CWBitcoin extends Bitcoin { throw err; } } + + @override + List getSilentPaymentAddresses(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.silentAddresses + .where((addr) => addr.type != SegwitAddresType.p2tr) + .map((addr) => ElectrumSubAddress( + id: addr.index, + name: addr.name, + address: addr.address, + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isHidden)) + .toList(); + } + + @override + List getSilentPaymentReceivedAddresses(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.silentAddresses + .where((addr) => addr.type == SegwitAddresType.p2tr) + .map((addr) => ElectrumSubAddress( + id: addr.index, + name: addr.name, + address: addr.address, + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isHidden)) + .toList(); + } + + @override + bool isBitcoinReceivePageOption(ReceivePageOption option) { + return option is BitcoinReceivePageOption; + } + + @override + BitcoinAddressType getOptionToType(ReceivePageOption option) { + return (option as BitcoinReceivePageOption).toType(); + } + + @override + @computed + bool getScanningActive(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.silentPaymentsScanningActive; + } + + @override + Future setScanningActive(Object wallet, bool active) async { + final bitcoinWallet = wallet as ElectrumWallet; + + if (active && !(await getNodeIsElectrsSPEnabled(wallet))) { + final node = Node( + useSSL: false, + uri: 'electrs.cakewallet.com:${(wallet.network == BitcoinNetwork.testnet ? 50002 : 50001)}', + ); + node.type = WalletType.bitcoin; + + await bitcoinWallet.connectToNode(node: node); + } + + bitcoinWallet.setSilentPaymentsScanning(active); + } + + @override + bool isTestnet(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.isTestnet ?? false; + } + + @override + int getHeightByDate({required DateTime date}) => getBitcoinHeightByDate(date: date); + + @override + Future rescan(Object wallet, {required int height, bool? doSingleScan}) async { + final bitcoinWallet = wallet as ElectrumWallet; + if (!(await getNodeIsElectrsSPEnabled(wallet))) { + final node = Node( + useSSL: false, + uri: 'electrs.cakewallet.com:${(wallet.network == BitcoinNetwork.testnet ? 50002 : 50001)}', + ); + node.type = WalletType.bitcoin; + await bitcoinWallet.connectToNode(node: node); + } + bitcoinWallet.rescan(height: height, doSingleScan: doSingleScan); + } + + Future getNodeIsElectrs(Object wallet) async { + final bitcoinWallet = wallet as ElectrumWallet; + + final version = await bitcoinWallet.electrumClient.version(); + + if (version.isEmpty) { + return false; + } + + final server = version[0]; + + if (server.toLowerCase().contains('electrs')) { + return true; + } + + return false; + } + + @override + Future getNodeIsElectrsSPEnabled(Object wallet) async { + if (!(await getNodeIsElectrs(wallet))) { + return false; + } + + final bitcoinWallet = wallet as ElectrumWallet; + final tweaksResponse = await bitcoinWallet.electrumClient.getTweaks(height: 0); + + if (tweaksResponse != null) { + return true; + } + + return false; + } + + @override + void deleteSilentPaymentAddress(Object wallet, String address) { + final bitcoinWallet = wallet as ElectrumWallet; + bitcoinWallet.walletAddresses.deleteSilentPaymentAddress(address); + } + + @override + Future updateFeeRates(Object wallet) async { + final bitcoinWallet = wallet as ElectrumWallet; + await bitcoinWallet.updateFeeRates(); + } } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index e507f5212..fe6629f51 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -26,7 +26,7 @@ class AddressValidator extends TextValidator { return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; case CryptoCurrency.btc: - return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$'; + return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$'; case CryptoCurrency.nano: return '[0-9a-zA-Z_]'; case CryptoCurrency.banano: @@ -274,7 +274,9 @@ class AddressValidator extends TextValidator { '|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type '|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type - '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)'; //P2trAddress type + '|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type + '|${SilentPaymentAddress.regex.pattern}\$'; + case CryptoCurrency.ltc: return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 66094de2b..c4cc3929f 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -3,7 +3,13 @@ import 'package:cw_core/sync_status.dart'; String syncStatusTitle(SyncStatus syncStatus) { if (syncStatus is SyncingSyncStatus) { - return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); + return syncStatus.blocksLeft == 1 + ? S.current.block_remaining + : S.current.Blocks_remaining('${syncStatus.blocksLeft}'); + } + + if (syncStatus is SyncedTipSyncStatus) { + return S.current.silent_payments_scanned_tip(syncStatus.tip.toString()); } if (syncStatus is SyncedSyncStatus) { @@ -34,5 +40,17 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_failed_connect; } + if (syncStatus is UnsupportedSyncStatus) { + return S.current.sync_status_unsupported; + } + + if (syncStatus is TimedOutSyncStatus) { + return S.current.sync_status_timed_out; + } + + if (syncStatus is SyncronizingSyncStatus) { + return S.current.sync_status_syncronizing; + } + return ''; -} \ No newline at end of file +} diff --git a/lib/di.dart b/lib/di.dart index 6a97cf62c..bbad4a636 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; import 'package:cake_wallet/anypay/anypay_api.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/order.dart'; @@ -15,6 +16,7 @@ import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; @@ -102,12 +104,14 @@ import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/send/send_template_page.dart'; import 'package:cake_wallet/src/screens/settings/connection_sync_page.dart'; +import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/domain_lookups_page.dart'; import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; +import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; @@ -159,10 +163,10 @@ import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; @@ -199,6 +203,7 @@ import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; @@ -235,10 +240,6 @@ import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'buy/dfx/dfx_buy_provider.dart'; -import 'core/totp_request_details.dart'; -import 'src/screens/settings/desktop_settings/desktop_settings_page.dart'; - final getIt = GetIt.instance; var _isSetupFinished = false; @@ -745,6 +746,9 @@ Future setup({ return DisplaySettingsViewModel(getIt.get()); }); + getIt.registerFactory(() => + SilentPaymentsSettingsViewModel(getIt.get(), getIt.get().wallet!)); + getIt.registerFactory(() { return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); @@ -806,6 +810,9 @@ Future setup({ getIt.registerFactory(() => DisplaySettingsPage(getIt.get())); + getIt.registerFactory( + () => SilentPaymentsSettingsPage(getIt.get())); + getIt.registerFactory(() => OtherSettingsPage(getIt.get())); getIt.registerFactory(() => NanoChangeRepPage( @@ -893,7 +900,11 @@ Future setup({ case WalletType.monero: return monero!.createMoneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.bitcoin: - return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource); + return bitcoin!.createBitcoinWalletService( + _walletInfoSource, + _unspentCoinsInfoSource, + getIt.get().silentPaymentsAlwaysScan, + ); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.ethereum: @@ -1089,7 +1100,7 @@ Future setup({ getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get())); - getIt.registerFactory(() => MarketPlaceViewModel(getIt.get())); + getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get())); diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index e67bd2fc6..697685767 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -24,8 +24,9 @@ import 'package:collection/collection.dart'; const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; -const publicBitcoinTestnetElectrumAddress = 'electrum.blockstream.info'; -const publicBitcoinTestnetElectrumPort = '60002'; +const cakeWalletSilentPaymentsElectrsUri = 'electrs.cakewallet.com:50001'; +const publicBitcoinTestnetElectrumAddress = 'electrs.cakewallet.com'; +const publicBitcoinTestnetElectrumPort = '50002'; const publicBitcoinTestnetElectrumUri = '$publicBitcoinTestnetElectrumAddress:$publicBitcoinTestnetElectrumPort'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; @@ -224,6 +225,9 @@ Future defaultSettingsMigration( await addTronNodeList(nodes: nodes); await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); break; + case 34: + await _addElectRsNode(nodes, sharedPreferences); + break; default: break; } @@ -790,7 +794,8 @@ Future changeDefaultBitcoinNode( final needToReplaceCurrentBitcoinNode = currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - final newCakeWalletBitcoinNode = Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin); + final newCakeWalletBitcoinNode = + Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: false); await nodeSource.add(newCakeWalletBitcoinNode); @@ -800,6 +805,26 @@ Future changeDefaultBitcoinNode( } } +Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { + const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; + final currentBitcoinNodeId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); + final currentBitcoinNode = + nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); + final needToReplaceCurrentBitcoinNode = + currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); + + final newElectRsBitcoinNode = + Node(uri: cakeWalletSilentPaymentsElectrsUri, type: WalletType.bitcoin, useSSL: false); + + await nodeSource.add(newElectRsBitcoinNode); + + if (needToReplaceCurrentBitcoinNode) { + await sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, newElectRsBitcoinNode.key as int); + } +} + Future checkCurrentNodes( Box nodeSource, Box powNodeSource, SharedPreferences sharedPreferences) async { final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); @@ -845,14 +870,19 @@ Future checkCurrentNodes( } if (currentBitcoinElectrumServer == null) { - final cakeWalletElectrum = Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); + final cakeWalletElectrum = + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false); await nodeSource.add(cakeWalletElectrum); + final cakeWalletElectrumTestnet = + Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false); + await nodeSource.add(cakeWalletElectrumTestnet); await sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int); } if (currentLitecoinElectrumServer == null) { - final cakeWalletElectrum = Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin); + final cakeWalletElectrum = + Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin, useSSL: false); await nodeSource.add(cakeWalletElectrum); await sharedPreferences.setInt( PreferencesKey.currentLitecoinElectrumSererIdKey, cakeWalletElectrum.key as int); @@ -887,7 +917,8 @@ Future checkCurrentNodes( } if (currentBitcoinCashNodeServer == null) { - final node = Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash); + final node = + Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash, useSSL: false); await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int); } @@ -921,7 +952,11 @@ Future resetBitcoinElectrumServer( .firstWhereOrNull((node) => node.uriRaw.toString() == cakeWalletBitcoinElectrumUri); if (cakeWalletNode == null) { - cakeWalletNode = Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); + cakeWalletNode = + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin, useSSL: false); + // final cakeWalletElectrumTestnet = + // Node(uri: publicBitcoinTestnetElectrumUri, type: WalletType.bitcoin, useSSL: false); + // await nodeSource.add(cakeWalletElectrumTestnet); await nodeSource.add(cakeWalletNode); } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index d184c74b1..aebf9ccd5 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -44,6 +44,8 @@ class PreferencesKey { static const polygonTransactionPriority = 'current_fee_priority_polygon'; static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; + static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; + static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowRepWarning = 'should_show_rep_warning'; diff --git a/lib/main.dart b/lib/main.dart index eeee4fbc3..776c2aa69 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -202,7 +202,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 33, + initialMigrationVersion: 34, ); } diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index 9185ffe15..3252797dd 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -3,15 +3,19 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/store/settings_store.dart'; + Timer? _checkConnectionTimer; -void startCheckConnectionReaction( - WalletBase wallet, SettingsStore settingsStore, +void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore, {int timeInterval = 5}) { _checkConnectionTimer?.cancel(); - _checkConnectionTimer = - Timer.periodic(Duration(seconds: timeInterval), (_) async { + _checkConnectionTimer = Timer.periodic(Duration(seconds: timeInterval), (_) async { + if (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus) { + return; + } + try { final connectivityResult = await (Connectivity().checkConnectivity()); @@ -20,14 +24,11 @@ void startCheckConnectionReaction( return; } - if (wallet.syncStatus is LostConnectionSyncStatus || - wallet.syncStatus is FailedSyncStatus) { - final alive = - await settingsStore.getCurrentNode(wallet.type).requestNode(); + if (wallet.syncStatus is LostConnectionSyncStatus || wallet.syncStatus is FailedSyncStatus) { + final alive = await settingsStore.getCurrentNode(wallet.type).requestNode(); if (alive) { - await wallet.connectToNode( - node: settingsStore.getCurrentNode(wallet.type)); + await wallet.connectToNode(node: settingsStore.getCurrentNode(wallet.type)); } } } catch (e) { diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 9a13db597..96305de04 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -12,12 +12,10 @@ import 'package:wakelock_plus/wakelock_plus.dart'; ReactionDisposer? _onWalletSyncStatusChangeReaction; void startWalletSyncStatusChangeReaction( - WalletBase, - TransactionInfo> wallet, + WalletBase, TransactionInfo> wallet, FiatConversionStore fiatConversionStore) { _onWalletSyncStatusChangeReaction?.reaction.dispose(); - _onWalletSyncStatusChangeReaction = - reaction((_) => wallet.syncStatus, (SyncStatus status) async { + _onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async { try { if (status is ConnectedSyncStatus) { await wallet.startSync(); @@ -32,7 +30,7 @@ void startWalletSyncStatusChangeReaction( if (status is SyncedSyncStatus || status is FailedSyncStatus) { await WakelockPlus.disable(); } - } catch(e) { + } catch (e) { print(e.toString()); } }); diff --git a/lib/router.dart b/lib/router.dart index 741597731..e113e42f9 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -76,6 +76,7 @@ import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; +import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; @@ -366,6 +367,10 @@ Route createRoute(RouteSettings settings) { param1: settings.arguments as OnAuthenticationFinished, param2: false), onWillPop: () async => false)); + case Routes.silentPaymentsSettings: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.connectionSync: return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); diff --git a/lib/routes.dart b/lib/routes.dart index 1b518d328..b5208416f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -81,6 +81,7 @@ class Routes { static const ioniaMoreOptionsPage = '/ionia_more_options_page'; static const ioniaCustomRedeemPage = '/ionia_custom_redeem_page'; static const webViewPage = '/web_view_page'; + static const silentPaymentsSettings = '/silent_payments_settings'; static const connectionSync = '/connection_sync_page'; static const securityBackupPage = '/security_and_backup_page'; static const privacyPage = '/privacy_page'; diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index ec97d191f..bec10435e 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -4,7 +4,7 @@ import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/main_actions.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/market_place_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/src/widgets/services_updates_widget.dart'; @@ -12,7 +12,7 @@ import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/version_comparator.dart'; -import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/yat_emoji_id.dart'; @@ -330,10 +330,10 @@ class _DashboardPageView extends BasePage { if (dashboardViewModel.shouldShowMarketPlaceInDashboard) { pages.add( Semantics( - label: S.of(context).market_place, - child: MarketPlacePage( + label: 'Cake ${S.of(context).features}', + child: CakeFeaturesPage( dashboardViewModel: dashboardViewModel, - marketPlaceViewModel: getIt.get(), + cakeFeaturesViewModel: getIt.get(), ), ), ); diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart index 20ddea361..d36c06013 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart @@ -1,9 +1,9 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/main_actions.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_action_button.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/market_place_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -74,9 +74,9 @@ class DesktopDashboardActions extends StatelessWidget { ], ), Expanded( - child: MarketPlacePage( + child: CakeFeaturesPage( dashboardViewModel: dashboardViewModel, - marketPlaceViewModel: getIt.get(), + cakeFeaturesViewModel: getIt.get(), ), ), ], diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index adf0840c9..663675849 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -30,6 +30,7 @@ class DesktopWalletSelectionDropDown extends StatefulWidget { class _DesktopWalletSelectionDropDownState extends State { final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24); final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24); final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24); @@ -68,8 +69,11 @@ class _DesktopWalletSelectionDropDownState extends State _onSelectedWallet(wallet), )) @@ -120,16 +124,16 @@ class _DesktopWalletSelectionDropDownState extends State( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).change_wallet_alert_title, - alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name), - leftButtonText: S.of(context).cancel, - rightButtonText: S.of(context).change, - actionLeftButton: () => Navigator.of(dialogContext).pop(false), - actionRightButton: () => Navigator.of(dialogContext).pop(true)); - }) ?? + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).change_wallet_alert_title, + alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name), + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).change, + actionLeftButton: () => Navigator.of(dialogContext).pop(false), + actionRightButton: () => Navigator.of(dialogContext).pop(true)); + }) ?? false; if (confirmed) { @@ -138,9 +142,12 @@ class _DesktopWalletSelectionDropDownState extends State _loadWallet(WalletListItem wallet) async { - widget._authService.authenticateAction(context, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) { - return; - } + widget._authService.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (!isAuthenticatedSuccessfully) { + return; + } - try { - if (context.mounted) { - changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); + try { + if (context.mounted) { + changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); + } + await widget.walletListViewModel.loadWallet(wallet); + hideProgressText(); + setState(() {}); + } catch (e) { + if (context.mounted) { + changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); + } } - await widget.walletListViewModel.loadWallet(wallet); - hideProgressText(); - setState(() {}); - } catch (e) { - if (context.mounted) { - changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); - } - } }, conditionToDetermineIfToUse2FA: widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, @@ -198,17 +206,16 @@ class _DesktopWalletSelectionDropDownState extends State receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) { + if (bitcoin!.isBitcoinReceivePageOption(option)) { + addressListViewModel.setAddressType(bitcoin!.getOptionToType(option)); + return; + } + switch (option) { case ReceivePageOption.anonPayInvoice: Navigator.pushNamed( diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 7f9256c51..7ffcf918d 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -1,15 +1,18 @@ import 'dart:math'; import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; import 'package:cake_wallet/src/widgets/introducing_card.dart'; +import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; @@ -21,6 +24,7 @@ import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:url_launcher/url_launcher.dart'; class BalancePage extends StatelessWidget { BalancePage({ @@ -221,30 +225,136 @@ class CryptoBalanceWidget extends StatelessWidget { itemBuilder: (__, index) { final balance = dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); - return BalanceRowWidget( - availableBalanceLabel: - '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', - availableBalance: balance.availableBalance, - availableFiatBalance: balance.fiatAvailableBalance, - additionalBalanceLabel: - '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', - additionalBalance: balance.additionalBalance, - additionalFiatBalance: balance.fiatAdditionalBalance, - frozenBalance: balance.frozenBalance, - frozenFiatBalance: balance.fiatFrozenBalance, - currency: balance.asset, - hasAdditionalBalance: - dashboardViewModel.balanceViewModel.hasAdditionalBalance, - ); + return Observer(builder: (_) { + return BalanceRowWidget( + availableBalanceLabel: + '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', + availableBalance: balance.availableBalance, + availableFiatBalance: balance.fiatAvailableBalance, + additionalBalanceLabel: + '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', + additionalBalance: balance.additionalBalance, + additionalFiatBalance: balance.fiatAdditionalBalance, + frozenBalance: balance.frozenBalance, + frozenFiatBalance: balance.fiatFrozenBalance, + currency: balance.asset, + hasAdditionalBalance: + dashboardViewModel.balanceViewModel.hasAdditionalBalance, + isTestnet: dashboardViewModel.isTestnet, + ); + }); }, ); }, - ) + ), + Observer(builder: (context) { + return Column( + children: [ + if (dashboardViewModel.showSilentPaymentsCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DashBoardRoundedCardWidget( + customBorder: 30, + title: S.of(context).silent_payments, + subTitle: S.of(context).enable_silent_payments_scanning, + hint: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse( + "https://guides.cakewallet.com/docs/cryptos/bitcoin/#silent-payments"), + mode: LaunchMode.externalApplication, + ), + child: Row( + children: [ + Text( + S.of(context).what_is_silent_payments, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .extension()! + .labelTextColor, + height: 1, + ), + softWrap: true, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .extension()! + .labelTextColor), + ) + ], + ), + ), + Observer( + builder: (_) => StandardSwitch( + value: dashboardViewModel.silentPaymentsScanningActive, + onTaped: () => _toggleSilentPaymentsScanning(context), + ), + ) + ], + ), + ], + ), + onTap: () => _toggleSilentPaymentsScanning(context), + icon: Icon( + Icons.lock, + color: + Theme.of(context).extension()!.pageTitleTextColor, + size: 50, + ), + ), + ), + ] + ], + ); + }), ], ), ), ); } + + Future _toggleSilentPaymentsScanning(BuildContext context) async { + final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive; + final newValue = !isSilentPaymentsScanningActive; + + dashboardViewModel.silentPaymentsScanningActive = newValue; + + final needsToSwitch = !isSilentPaymentsScanningActive && + await bitcoin!.getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) == false; + + if (needsToSwitch) { + return showPopUp( + context: context, + builder: (BuildContext context) => AlertWithTwoActions( + alertTitle: S.of(context).change_current_node_title, + alertContent: S.of(context).confirm_silent_payments_switch_node, + rightButtonText: S.of(context).ok, + leftButtonText: S.of(context).cancel, + actionRightButton: () { + dashboardViewModel.setSilentPaymentsScanning(newValue); + Navigator.of(context).pop(); + }, + actionLeftButton: () { + dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive; + Navigator.of(context).pop(); + }, + )); + } + + return dashboardViewModel.setSilentPaymentsScanning(newValue); + } } class BalanceRowWidget extends StatelessWidget { @@ -259,6 +369,7 @@ class BalanceRowWidget extends StatelessWidget { required this.frozenFiatBalance, required this.currency, required this.hasAdditionalBalance, + required this.isTestnet, super.key, }); @@ -272,6 +383,7 @@ class BalanceRowWidget extends StatelessWidget { final String frozenFiatBalance; final CryptoCurrency currency; final bool hasAdditionalBalance; + final bool isTestnet; // void _showBalanceDescription(BuildContext context) { // showPopUp( @@ -346,14 +458,24 @@ class BalanceRowWidget extends StatelessWidget { maxLines: 1, textAlign: TextAlign.start), SizedBox(height: 6), - Text('${availableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor, - height: 1)), + if (isTestnet) + Text(S.current.testnet_coins_no_value, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1)), + if (!isTestnet) + Text('${availableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.textColor, + height: 1)), ], ), ), @@ -362,27 +484,23 @@ class BalanceRowWidget extends StatelessWidget { child: Center( child: Column( children: [ - Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration(shape: BoxShape.circle), - child: CakeImageWidget( - imageUrl: currency.iconPath, - height: 40, - width: 40, - displayOnError: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: TextStyle(fontSize: 11), - ), - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade400, + CakeImageWidget( + imageUrl: currency.iconPath, + height: 40, + width: 40, + displayOnError: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: TextStyle(fontSize: 11), ), ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), ), ), const SizedBox(height: 10), @@ -449,17 +567,18 @@ class BalanceRowWidget extends StatelessWidget { textAlign: TextAlign.center, ), SizedBox(height: 4), - Text( - frozenFiatBalance, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, + if (!isTestnet) + Text( + frozenFiatBalance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), ), - ), ], ), ), @@ -493,17 +612,18 @@ class BalanceRowWidget extends StatelessWidget { textAlign: TextAlign.center, ), SizedBox(height: 4), - Text( - '${additionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, + if (!isTestnet) + Text( + '${additionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.textColor, + height: 1, + ), ), - ), ], ), ], diff --git a/lib/src/screens/dashboard/pages/market_place_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart similarity index 60% rename from lib/src/screens/dashboard/pages/market_place_page.dart rename to lib/src/screens/dashboard/pages/cake_features_page.dart index d28048844..9ccb7833c 100644 --- a/lib/src/screens/dashboard/pages/market_place_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -1,23 +1,20 @@ -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; -import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:flutter_svg/flutter_svg.dart'; -class MarketPlacePage extends StatelessWidget { - MarketPlacePage({ +class CakeFeaturesPage extends StatelessWidget { + CakeFeaturesPage({ required this.dashboardViewModel, - required this.marketPlaceViewModel, + required this.cakeFeaturesViewModel, }); final DashboardViewModel dashboardViewModel; - final MarketPlaceViewModel marketPlaceViewModel; + final CakeFeaturesViewModel cakeFeaturesViewModel; final _scrollController = ScrollController(); @override @@ -37,7 +34,7 @@ class MarketPlacePage extends StatelessWidget { children: [ SizedBox(height: 50), Text( - S.of(context).market_place, + 'Cake ${S.of(context).features}', style: TextStyle( fontSize: 24, fontWeight: FontWeight.w500, @@ -59,15 +56,21 @@ class MarketPlacePage extends StatelessWidget { // ), SizedBox(height: 20), DashBoardRoundedCardWidget( + onTap: () => _launchUrl("buy.cakepay.com"), title: S.of(context).cake_pay_web_cards_title, subTitle: S.of(context).cake_pay_web_cards_subtitle, - onTap: () => _launchMarketPlaceUrl("buy.cakepay.com"), + svgPicture: SvgPicture.asset( + 'assets/images/cards.svg', + height: 125, + width: 125, + fit: BoxFit.cover, + ), ), - const SizedBox(height: 20), + SizedBox(height: 10), DashBoardRoundedCardWidget( title: "NanoGPT", subTitle: S.of(context).nanogpt_subtitle, - onTap: () => _launchMarketPlaceUrl("cake.nano-gpt.com"), + onTap: () => _launchUrl("cake.nano-gpt.com"), ), ], ), @@ -79,41 +82,12 @@ class MarketPlacePage extends StatelessWidget { ); } - void _launchMarketPlaceUrl(String url) async { + void _launchUrl(String url) { try { launchUrl( Uri.https(url), mode: LaunchMode.externalApplication, ); - } catch (e) { - print(e); - } - } - - // TODO: Remove ionia flow/files if we will discard it - void _navigatorToGiftCardsPage(BuildContext context) { - final walletType = dashboardViewModel.type; - - switch (walletType) { - case WalletType.haven: - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: S.of(context).gift_cards_unavailable, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - break; - default: - marketPlaceViewModel.isIoniaUserAuthenticated().then((value) { - if (value) { - Navigator.pushNamed(context, Routes.ioniaManageCardsPage); - return; - } - Navigator.of(context).pushNamed(Routes.ioniaWelcomePage); - }); - } + } catch (_) {} } } diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index d9e03dbf9..7eda20bff 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/setting_action_button.dart'; import 'package:cake_wallet/src/widgets/setting_actions.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; @@ -180,6 +181,11 @@ class MenuWidgetState extends State { final item = SettingActions.all[index]; + if (!widget.dashboardViewModel.hasSilentPayments && + item.name(context) == S.of(context).silent_payments_settings) { + return Container(); + } + final isLastTile = index == itemCount - 1; return SettingActionButton( diff --git a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart index 33bceeb5c..bebb58107 100644 --- a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart +++ b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart @@ -10,8 +10,7 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; class PresentReceiveOptionPicker extends StatelessWidget { - PresentReceiveOptionPicker( - {required this.receiveOptionViewModel, required this.color}); + PresentReceiveOptionPicker({required this.receiveOptionViewModel, required this.color}); final ReceiveOptionViewModel receiveOptionViewModel; final Color color; @@ -43,17 +42,17 @@ class PresentReceiveOptionPicker extends StatelessWidget { Text( S.current.receive, style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - fontFamily: 'Lato', - color: color), + fontSize: 18.0, fontWeight: FontWeight.bold, fontFamily: 'Lato', color: color), ), Observer( - builder: (_) => Text(receiveOptionViewModel.selectedReceiveOption.toString(), - style: TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.w500, - color: color))) + builder: (_) => Text( + receiveOptionViewModel.selectedReceiveOption + .toString() + .replaceAll(RegExp(r'silent payments', caseSensitive: false), + S.current.silent_payments) + .replaceAll( + RegExp(r'default', caseSensitive: false), S.current.string_default), + style: TextStyle(fontSize: 10.0, fontWeight: FontWeight.w500, color: color))) ], ), SizedBox(width: 5), @@ -73,65 +72,75 @@ class PresentReceiveOptionPicker extends StatelessWidget { backgroundColor: Colors.transparent, body: Stack( alignment: AlignmentDirectional.center, - children:[ AlertBackground( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Spacer(), - Container( - margin: EdgeInsets.symmetric(horizontal: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Theme.of(context).colorScheme.background, - ), - child: Padding( - padding: const EdgeInsets.only(top: 24, bottom: 24), - child: (ListView.separated( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: receiveOptionViewModel.options.length, - itemBuilder: (_, index) { - final option = receiveOptionViewModel.options[index]; - return InkWell( - onTap: () { - Navigator.pop(popUpContext); + children: [ + AlertBackground( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Spacer(), + Container( + margin: EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).colorScheme.background, + ), + child: Padding( + padding: const EdgeInsets.only(top: 24, bottom: 24), + child: (ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: receiveOptionViewModel.options.length, + itemBuilder: (_, index) { + final option = receiveOptionViewModel.options[index]; + return InkWell( + onTap: () { + Navigator.pop(popUpContext); - receiveOptionViewModel.selectReceiveOption(option); - }, - child: Padding( - padding: const EdgeInsets.only(left: 24, right: 24), - child: Observer(builder: (_) { - final value = receiveOptionViewModel.selectedReceiveOption; + receiveOptionViewModel.selectReceiveOption(option); + }, + child: Padding( + padding: const EdgeInsets.only(left: 24, right: 24), + child: Observer(builder: (_) { + final value = receiveOptionViewModel.selectedReceiveOption; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(option.toString(), - textAlign: TextAlign.left, - style: textSmall( - color: Theme.of(context).extension()!.titleColor, - ).copyWith( - fontWeight: - value == option ? FontWeight.w800 : FontWeight.w500, - )), - RoundedCheckbox( - value: value == option, - ) - ], - ); - }), - ), - ); - }, - separatorBuilder: (_, index) => SizedBox(height: 30), - )), + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + option + .toString() + .replaceAll( + RegExp(r'silent payments', caseSensitive: false), + S.current.silent_payments) + .replaceAll(RegExp(r'default', caseSensitive: false), + S.current.string_default), + textAlign: TextAlign.left, + style: textSmall( + color: Theme.of(context) + .extension()! + .titleColor, + ).copyWith( + fontWeight: + value == option ? FontWeight.w800 : FontWeight.w500, + )), + RoundedCheckbox( + value: value == option, + ) + ], + ); + }), + ), + ); + }, + separatorBuilder: (_, index) => SizedBox(height: 30), + )), + ), ), - ), - Spacer() - ], + Spacer() + ], + ), ), - ), AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40) ], ), diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index ecba4acf5..789fb42bf 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -67,8 +67,7 @@ class ReceivePage extends BasePage { @override Widget Function(BuildContext, Widget) get rootWrapper => - (BuildContext context, Widget scaffold) => - GradientBackground(scaffold: scaffold); + (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold); @override Widget trailing(BuildContext context) { @@ -99,115 +98,144 @@ class ReceivePage extends BasePage { @override Widget body(BuildContext context) { - return KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _cryptoAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ]), - child: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(24, 50, 24, 24), - child: QRWidget( - addressListViewModel: addressListViewModel, - formKey: _formKey, - heroTag: _heroTag, - amountTextFieldFocusNode: _cryptoAmountFocus, - amountController: _amountController, - isLight: currentTheme.type == ThemeType.light), - ), - Observer( - builder: (_) => ListView.separated( - padding: EdgeInsets.all(0), - separatorBuilder: (context, _) => const HorizontalSectionDivider(), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: addressListViewModel.items.length, - itemBuilder: (context, index) { - final item = addressListViewModel.items[index]; - Widget cell = Container(); - - if (item is WalletAccountListHeader) { - cell = HeaderTile( - showTrailingButton: true, - walletAddressListViewModel: addressListViewModel, - trailingButtonTap: () async { - if (addressListViewModel.type == WalletType.monero || - addressListViewModel.type == WalletType.haven) { - await showPopUp( - context: context, - builder: (_) => getIt.get()); - } else { - await showPopUp( - context: context, - builder: (_) => getIt.get()); - } - }, - title: S.of(context).accounts, - trailingIcon: Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).extension()!.iconsColor, - )); - } - - if (item is WalletAddressListHeader) { - cell = HeaderTile( - title: S.of(context).addresses, - walletAddressListViewModel: addressListViewModel, - showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled, - showSearchButton: true, - trailingButtonTap: () => - Navigator.of(context).pushNamed(Routes.newSubaddress), - trailingIcon: Icon( - Icons.add, - size: 20, - color: Theme.of(context) - .extension()! - .iconsColor, - )); - } - - if (item is WalletAddressListItem) { - cell = Observer(builder: (_) { - final isCurrent = - item.address == addressListViewModel.address.address; - final backgroundColor = isCurrent - ? Theme.of(context).extension()!.currentTileBackgroundColor - : Theme.of(context).extension()!.tilesBackgroundColor; - final textColor = isCurrent - ? Theme.of(context).extension()!.currentTileTextColor - : Theme.of(context).extension()!.tilesTextColor; - - return AddressCell.fromItem(item, - isCurrent: isCurrent, - hasBalance: addressListViewModel.isElectrumWallet, - backgroundColor: backgroundColor, - textColor: textColor, - onTap: (_) => addressListViewModel.setAddress(item), - onEdit: () => Navigator.of(context) - .pushNamed(Routes.newSubaddress, arguments: item)); - }); - } - - return index != 0 - ? cell - : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30)), - child: cell, - ); - })), - ], + return KeyboardActions( + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _cryptoAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ]), + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(24, 50, 24, 24), + child: QRWidget( + addressListViewModel: addressListViewModel, + formKey: _formKey, + heroTag: _heroTag, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: currentTheme.type == ThemeType.light), ), - )); + Observer( + builder: (_) => ListView.separated( + padding: EdgeInsets.all(0), + separatorBuilder: (context, _) => const HorizontalSectionDivider(), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: addressListViewModel.items.length, + itemBuilder: (context, index) { + final item = addressListViewModel.items[index]; + Widget cell = Container(); + + if (item is WalletAccountListHeader) { + cell = HeaderTile( + showTrailingButton: true, + walletAddressListViewModel: addressListViewModel, + trailingButtonTap: () async { + if (addressListViewModel.type == WalletType.monero || + addressListViewModel.type == WalletType.haven) { + await showPopUp( + context: context, + builder: (_) => getIt.get()); + } else { + await showPopUp( + context: context, + builder: (_) => getIt.get()); + } + }, + title: S.of(context).accounts, + trailingIcon: Icon( + Icons.arrow_forward_ios, + size: 14, + color: Theme.of(context).extension()!.iconsColor, + )); + } + + if (item is WalletAddressListHeader) { + final hasTitle = item.title != null; + + cell = HeaderTile( + title: hasTitle ? item.title! : S.of(context).addresses, + walletAddressListViewModel: addressListViewModel, + showTrailingButton: + !addressListViewModel.isAutoGenerateSubaddressEnabled && !hasTitle, + showSearchButton: true, + trailingButtonTap: () => + Navigator.of(context).pushNamed(Routes.newSubaddress), + trailingIcon: hasTitle + ? null + : Icon( + Icons.add, + size: 20, + color: + Theme.of(context).extension()!.iconsColor, + ), + ); + } + + if (item is WalletAddressListItem) { + cell = Observer(builder: (_) { + final isCurrent = item.address == addressListViewModel.address.address; + final backgroundColor = isCurrent + ? Theme.of(context) + .extension()! + .currentTileBackgroundColor + : Theme.of(context) + .extension()! + .tilesBackgroundColor; + final textColor = isCurrent + ? Theme.of(context) + .extension()! + .currentTileTextColor + : Theme.of(context).extension()!.tilesTextColor; + + return AddressCell.fromItem( + item, + isCurrent: isCurrent, + hasBalance: addressListViewModel.isElectrumWallet, + backgroundColor: backgroundColor, + textColor: textColor, + onTap: item.isOneTimeReceiveAddress == true + ? null + : (_) => addressListViewModel.setAddress(item), + onEdit: item.isOneTimeReceiveAddress == true || item.isPrimary + ? null + : () => Navigator.of(context) + .pushNamed(Routes.newSubaddress, arguments: item), + onDelete: !addressListViewModel.isSilentPayments || item.isPrimary + ? null + : () => addressListViewModel.deleteAddress(item), + ); + }); + } + + return index != 0 + ? cell + : ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), topRight: Radius.circular(30)), + child: cell, + ); + })), + Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + child: Text( + addressListViewModel.isSilentPayments + ? S.of(context).silent_payments_disclaimer + : S.of(context).electrum_address_disclaimer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: Theme.of(context).extension()!.labelTextColor)), + ), + ], + ), + )); } } diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 9385a4df8..850c08209 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -15,18 +15,22 @@ class AddressCell extends StatelessWidget { required this.textColor, this.onTap, this.onEdit, + this.onDelete, this.txCount, this.balance, this.isChange = false, this.hasBalance = false}); - factory AddressCell.fromItem(WalletAddressListItem item, - {required bool isCurrent, - required Color backgroundColor, - required Color textColor, - Function(String)? onTap, - bool hasBalance = false, - Function()? onEdit}) => + factory AddressCell.fromItem( + WalletAddressListItem item, { + required bool isCurrent, + required Color backgroundColor, + required Color textColor, + Function(String)? onTap, + bool hasBalance = false, + Function()? onEdit, + Function()? onDelete, + }) => AddressCell( address: item.address, name: item.name ?? '', @@ -36,6 +40,7 @@ class AddressCell extends StatelessWidget { textColor: textColor, onTap: onTap, onEdit: onEdit, + onDelete: onDelete, txCount: item.txCount, balance: item.balance, isChange: item.isChange, @@ -49,6 +54,7 @@ class AddressCell extends StatelessWidget { final Color textColor; final Function(String)? onTap; final Function()? onEdit; + final Function()? onDelete; final int? txCount; final String? balance; final bool isChange; @@ -64,7 +70,8 @@ class AddressCell extends StatelessWidget { } else { return formatIfCashAddr.substring(0, addressPreviewLength) + '...' + - formatIfCashAddr.substring(formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length); + formatIfCashAddr.substring( + formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length); } } @@ -139,7 +146,7 @@ class AddressCell extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Text( - 'Balance: $balance', + '${S.of(context).balance}: $balance', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -180,7 +187,7 @@ class AddressCell extends StatelessWidget { ActionPane _actionPane(BuildContext context) => ActionPane( motion: const ScrollMotion(), - extentRatio: 0.3, + extentRatio: onDelete != null ? 0.4 : 0.3, children: [ SlidableAction( onPressed: (_) => onEdit?.call(), @@ -189,6 +196,14 @@ class AddressCell extends StatelessWidget { icon: Icons.edit, label: S.of(context).edit, ), + if (onDelete != null) + SlidableAction( + onPressed: (_) => onDelete!.call(), + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: Icons.delete, + label: S.of(context).delete, + ), ], ); } diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 3a0ba2473..c59ae4ad0 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -1,3 +1,7 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart'; @@ -11,7 +15,8 @@ class RescanPage extends BasePage { : _blockchainHeightWidgetKey = GlobalKey(); @override - String get title => S.current.rescan; + String get title => + _rescanViewModel.isSilentPaymentsScan ? S.current.silent_payments_scanning : S.current.rescan; final GlobalKey _blockchainHeightWidgetKey; final RescanViewModel _rescanViewModel; @@ -19,20 +24,28 @@ class RescanPage extends BasePage { Widget body(BuildContext context) { return Padding( padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - child: - Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - BlockchainHeightWidget(key: _blockchainHeightWidgetKey, - onHeightOrDateEntered: (value) => - _rescanViewModel.isButtonEnabled = value), + child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Observer( + builder: (_) => BlockchainHeightWidget( + key: _blockchainHeightWidgetKey, + onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value, + isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, + doSingleScan: _rescanViewModel.doSingleScan, + toggleSingleScan: () => + _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, + )), Observer( builder: (_) => LoadingPrimaryButton( - isLoading: - _rescanViewModel.state == RescanWalletState.rescaning, + isLoading: _rescanViewModel.state == RescanWalletState.rescaning, text: S.of(context).rescan, onPressed: () async { - await _rescanViewModel.rescanCurrentWallet( - restoreHeight: - _blockchainHeightWidgetKey.currentState!.height); + if (_rescanViewModel.isSilentPaymentsScan) { + return _toggleSilentPaymentsScanning(context); + } + + _rescanViewModel.rescanCurrentWallet( + restoreHeight: _blockchainHeightWidgetKey.currentState!.height); + Navigator.of(context).pop(); }, color: Theme.of(context).primaryColor, @@ -42,4 +55,32 @@ class RescanPage extends BasePage { ]), ); } + + Future _toggleSilentPaymentsScanning(BuildContext context) async { + final height = _blockchainHeightWidgetKey.currentState!.height; + + Navigator.of(context).pop(); + + final needsToSwitch = + await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == false; + + if (needsToSwitch) { + return showPopUp( + context: navigatorKey.currentState!.context, + builder: (BuildContext _dialogContext) => AlertWithTwoActions( + alertTitle: S.of(_dialogContext).change_current_node_title, + alertContent: S.of(_dialogContext).confirm_silent_payments_switch_node, + rightButtonText: S.of(_dialogContext).ok, + leftButtonText: S.of(_dialogContext).cancel, + actionRightButton: () async { + Navigator.of(_dialogContext).pop(); + + _rescanViewModel.rescanCurrentWallet(restoreHeight: height); + }, + actionLeftButton: () => Navigator.of(_dialogContext).pop(), + )); + } + + _rescanViewModel.rescanCurrentWallet(restoreHeight: height); + } } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 65c5a07f6..438c22c1d 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/core/execution_state.dart'; @@ -420,6 +421,10 @@ class SendPage extends BasePage { return; } + if (sendViewModel.isElectrumWallet) { + bitcoin!.updateFeeRates(sendViewModel.wallet); + } + reaction((_) => sendViewModel.state, (ExecutionState state) { if (dialogContext != null && dialogContext?.mounted == true) { Navigator.of(dialogContext!).pop(); diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index 7b4fb3b1c..c4d85a3a5 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -39,7 +39,9 @@ class ConnectionSyncPage extends BasePage { ), if (dashboardViewModel.hasRescan) ...[ SettingsCellWithArrow( - title: S.current.rescan, + title: dashboardViewModel.hasSilentPayments + ? S.current.silent_payments_scanning + : S.current.rescan, handler: (context) => Navigator.of(context).pushNamed(Routes.rescan), ), if (DeviceInfo.instance.isMobile && FeatureFlag.isBackgroundSyncEnabled) ...[ diff --git a/lib/src/screens/settings/silent_payments_settings.dart b/lib/src/screens/settings/silent_payments_settings.dart new file mode 100644 index 000000000..bc0ecece1 --- /dev/null +++ b/lib/src/screens/settings/silent_payments_settings.dart @@ -0,0 +1,50 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class SilentPaymentsSettingsPage extends BasePage { + SilentPaymentsSettingsPage(this._silentPaymentsSettingsViewModel); + + @override + String get title => S.current.silent_payments_settings; + + final SilentPaymentsSettingsViewModel _silentPaymentsSettingsViewModel; + + @override + Widget body(BuildContext context) { + return SingleChildScrollView( + child: Observer(builder: (_) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + SettingsSwitcherCell( + title: S.current.silent_payments_display_card, + value: _silentPaymentsSettingsViewModel.silentPaymentsCardDisplay, + onValueChange: (_, bool value) { + _silentPaymentsSettingsViewModel.setSilentPaymentsCardDisplay(value); + }, + ), + SettingsSwitcherCell( + title: S.current.silent_payments_always_scan, + value: _silentPaymentsSettingsViewModel.silentPaymentsAlwaysScan, + onValueChange: (_, bool value) { + _silentPaymentsSettingsViewModel.setSilentPaymentsAlwaysScan(value); + }, + ), + SettingsCellWithArrow( + title: S.current.silent_payments_scanning, + handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), + ), + ], + ), + ); + }), + ); + } +} diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index 70ae7ce3f..ee6d6dc73 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -57,6 +57,7 @@ class UnspentCoinsListFormState extends State { isSending: item.isSending, isFrozen: item.isFrozen, isChange: item.isChange, + isSilentPayment: item.isSilentPayment, onCheckBoxTap: item.isFrozen ? null : () async { diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart index e16026073..60a23c99b 100644 --- a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -12,6 +12,7 @@ class UnspentCoinsListItem extends StatelessWidget { required this.isSending, required this.isFrozen, required this.isChange, + required this.isSilentPayment, this.onCheckBoxTap, }); @@ -21,18 +22,16 @@ class UnspentCoinsListItem extends StatelessWidget { final bool isSending; final bool isFrozen; final bool isChange; + final bool isSilentPayment; final Function()? onCheckBoxTap; @override Widget build(BuildContext context) { final unselectedItemColor = Theme.of(context).cardColor; final selectedItemColor = Theme.of(context).primaryColor; - final itemColor = isSending - ? selectedItemColor - : unselectedItemColor; - final amountColor = isSending - ? Colors.white - : Theme.of(context).extension()!.buttonTextColor; + final itemColor = isSending ? selectedItemColor : unselectedItemColor; + final amountColor = + isSending ? Colors.white : Theme.of(context).extension()!.buttonTextColor; final addressColor = isSending ? Colors.white.withOpacity(0.5) : Theme.of(context).extension()!.buttonSecondaryTextColor; @@ -121,6 +120,23 @@ class UnspentCoinsListItem extends StatelessWidget { ), ), ), + if (isSilentPayment) + Container( + height: 17, + padding: EdgeInsets.only(left: 6, right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: Colors.white), + alignment: Alignment.center, + child: Text( + S.of(context).silent_payments, + style: TextStyle( + color: itemColor, + fontSize: 7, + fontWeight: FontWeight.w600, + ), + ), + ), ], ), ), diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 9a0c29564..a89d5e66f 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -96,6 +96,7 @@ class WalletListBody extends StatefulWidget { class WalletListBodyState extends State { final moneroIcon = Image.asset('assets/images/monero_logo.png', height: 24, width: 24); final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24); final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); @@ -162,7 +163,10 @@ class WalletListBodyState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ wallet.isEnabled - ? _imageFor(type: wallet.type) + ? _imageFor( + type: wallet.type, + isTestnet: wallet.isTestnet, + ) : nonWalletTypeIcon, SizedBox(width: 10), Flexible( @@ -297,9 +301,12 @@ class WalletListBodyState extends State { ); } - Image _imageFor({required WalletType type}) { + Image _imageFor({required WalletType type, bool? isTestnet}) { switch (type) { case WalletType.bitcoin: + if (isTestnet == true) { + return tBitcoinIcon; + } return bitcoinIcon; case WalletType.monero: return moneroIcon; diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 221f87446..d85680cc8 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/date_picker.dart'; import 'package:flutter/material.dart'; @@ -12,13 +14,19 @@ class BlockchainHeightWidget extends StatefulWidget { this.onHeightChange, this.focusNode, this.onHeightOrDateEntered, - this.hasDatePicker = true}) - : super(key: key); + this.hasDatePicker = true, + this.isSilentPaymentsScan = false, + this.toggleSingleScan, + this.doSingleScan = false, + }) : super(key: key); final Function(int)? onHeightChange; final Function(bool)? onHeightOrDateEntered; final FocusNode? focusNode; final bool hasDatePicker; + final bool isSilentPaymentsScan; + final bool doSingleScan; + final Function()? toggleSingleScan; @override State createState() => BlockchainHeightState(); @@ -64,9 +72,10 @@ class BlockchainHeightState extends State { child: BaseTextFormField( focusNode: widget.focusNode, controller: restoreHeightController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: false), - hintText: S.of(context).widgets_restore_from_blockheight, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false), + hintText: widget.isSilentPaymentsScan + ? S.of(context).silent_payments_scan_from_height + : S.of(context).widgets_restore_from_blockheight, ))) ], ), @@ -78,8 +87,7 @@ class BlockchainHeightState extends State { style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, - color: - Theme.of(context).extension()!.titleColor), + color: Theme.of(context).extension()!.titleColor), ), ), Row( @@ -91,22 +99,47 @@ class BlockchainHeightState extends State { child: IgnorePointer( child: BaseTextFormField( controller: dateController, - hintText: S.of(context).widgets_restore_from_date, + hintText: widget.isSilentPaymentsScan + ? S.of(context).silent_payments_scan_from_date + : S.of(context).widgets_restore_from_date, )), ), )) ], ), + if (widget.isSilentPaymentsScan) + Padding( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).scan_one_block, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 8), + child: StandardSwitch( + value: widget.doSingleScan, + onTaped: () => widget.toggleSingleScan?.call(), + ), + ) + ], + ), + ), Padding( padding: EdgeInsets.only(left: 40, right: 40, top: 24), child: Text( - S.of(context).restore_from_date_or_blockheight, + widget.isSilentPaymentsScan + ? S.of(context).silent_payments_scan_from_date_or_blockheight + : S.of(context).restore_from_date_or_blockheight, textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.normal, - color: Theme.of(context).hintColor - ), + fontSize: 12, fontWeight: FontWeight.normal, color: Theme.of(context).hintColor), ), ) ] @@ -123,7 +156,12 @@ class BlockchainHeightState extends State { lastDate: now); if (date != null) { - final height = monero!.getHeightByDate(date: date); + int height; + if (widget.isSilentPaymentsScan) { + height = bitcoin!.getHeightByDate(date: date); + } else { + height = monero!.getHeightByDate(date: date); + } setState(() { dateController.text = DateFormat('yyyy-MM-dd').format(date); restoreHeightController.text = '$height'; diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index 74f2d598b..5a8ca14a4 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -2,19 +2,28 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class DashBoardRoundedCardWidget extends StatelessWidget { DashBoardRoundedCardWidget({ required this.onTap, required this.title, required this.subTitle, + this.hint, + this.svgPicture, + this.icon, this.onClose, + this.customBorder, }); final VoidCallback onTap; final VoidCallback? onClose; final String title; final String subTitle; + final Widget? hint; + final SvgPicture? svgPicture; + final Icon? icon; + final double? customBorder; @override Widget build(BuildContext context) { @@ -26,34 +35,56 @@ class DashBoardRoundedCardWidget extends StatelessWidget { child: Stack( children: [ Container( - padding: EdgeInsets.fromLTRB(20, 20, 40, 20), + padding: EdgeInsets.all(20), width: double.infinity, decoration: BoxDecoration( color: Theme.of(context).extension()!.syncedBackgroundColor, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(customBorder ?? 20), border: Border.all( color: Theme.of(context).extension()!.cardBorderColor, ), ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: TextStyle( - color: Theme.of(context).extension()!.cardTextColor, - fontSize: 24, - fontWeight: FontWeight.w900, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: + Theme.of(context).extension()!.cardTextColor, + fontSize: 24, + fontWeight: FontWeight.w900, + ), + softWrap: true, + ), + SizedBox(height: 5), + Text( + subTitle, + style: TextStyle( + color: Theme.of(context) + .extension()! + .cardTextColor, + fontWeight: FontWeight.w500, + fontFamily: 'Lato'), + softWrap: true, + ), + ], + ), + ), + if (svgPicture != null) svgPicture!, + if (icon != null) icon! + ], ), - SizedBox(height: 5), - Text( - subTitle, - style: TextStyle( - color: Theme.of(context).extension()!.cardTextColor, - fontWeight: FontWeight.w500, - fontFamily: 'Lato'), - ) + if (hint != null) ...[ + SizedBox(height: 10), + hint!, + ] ], ), ), diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index e12397ed0..6fbdb6868 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -17,6 +17,7 @@ class SettingActions { connectionSettingAction, walletSettingAction, addressBookSettingAction, + silentPaymentsSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, @@ -35,6 +36,15 @@ class SettingActions { supportSettingAction, ]; + static SettingActions silentPaymentsSettingAction = SettingActions._( + name: (context) => S.of(context).silent_payments_settings, + image: 'assets/images/bitcoin_menu.png', + onTap: (BuildContext context) { + Navigator.pop(context); + Navigator.of(context).pushNamed(Routes.silentPaymentsSettings); + }, + ); + static SettingActions connectionSettingAction = SettingActions._( name: (context) => S.of(context).connection_sync, image: 'assets/images/nodes_menu.png', diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index ec9820944..05af3f3b1 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; @@ -108,6 +107,8 @@ abstract class SettingsStoreBase with Store { required this.lookupsOpenAlias, required this.lookupsENS, required this.customBitcoinFeeRate, + required this.silentPaymentsCardDisplay, + required this.silentPaymentsAlwaysScan, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialHavenTransactionPriority, @@ -518,6 +519,16 @@ abstract class SettingsStoreBase with Store { (int customBitcoinFeeRate) => _sharedPreferences.setInt(PreferencesKey.customBitcoinFeeRate, customBitcoinFeeRate)); + reaction((_) => silentPaymentsCardDisplay, (bool silentPaymentsCardDisplay) { + _sharedPreferences.setBool( + PreferencesKey.silentPaymentsCardDisplay, silentPaymentsCardDisplay); + }); + + reaction( + (_) => silentPaymentsAlwaysScan, + (bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool( + PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -713,6 +724,12 @@ abstract class SettingsStoreBase with Store { @observable int customBitcoinFeeRate; + @observable + bool silentPaymentsCardDisplay; + + @observable + bool silentPaymentsAlwaysScan; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final BackgroundTasks _backgroundTasks; @@ -859,6 +876,10 @@ abstract class SettingsStoreBase with Store { final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1; + final silentPaymentsCardDisplay = + sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; + final silentPaymentsAlwaysScan = + sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; // If no value if (pinLength == null || pinLength == 0) { @@ -1103,6 +1124,8 @@ abstract class SettingsStoreBase with Store { lookupsOpenAlias: lookupsOpenAlias, lookupsENS: lookupsENS, customBitcoinFeeRate: customBitcoinFeeRate, + silentPaymentsCardDisplay: silentPaymentsCardDisplay, + silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, initialMoneroTransactionPriority: moneroTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialHavenTransactionPriority: havenTransactionPriority, @@ -1242,6 +1265,10 @@ abstract class SettingsStoreBase with Store { lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1; + silentPaymentsCardDisplay = + sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; + silentPaymentsAlwaysScan = + sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final bitcoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index 6c3169be1..8dbd97bb9 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -40,16 +41,19 @@ abstract class ContactListViewModelBase with Store { }); } else if (info.addresses?.isNotEmpty == true) { info.addresses!.forEach((address, label) { + if (label.isEmpty) { + return; + } final name = _createName(info.name, label); walletContacts.add(WalletContact( address, name, - walletTypeToCryptoCurrency(info.type), + walletTypeToCryptoCurrency(info.type, + isTestnet: + info.network == null ? false : info.network!.toLowerCase().contains("testnet")), )); - // Only one contact address per wallet - return; }); - } else if (info.address != null) { + } else { walletContacts.add(WalletContact( info.address, info.name, @@ -64,7 +68,9 @@ abstract class ContactListViewModelBase with Store { } String _createName(String walletName, String label) { - return label.isNotEmpty ? '$walletName ($label)' : walletName; + return label.isNotEmpty + ? '$walletName (${label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments)})' + : walletName; } final bool isAutoGenerateEnabled; diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 6f4db52a6..5ae532bb6 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -60,6 +60,9 @@ abstract class BalanceViewModelBase with Store { @observable WalletBase, TransactionInfo> wallet; + @computed + bool get hasSilentPayments => wallet.type == WalletType.bitcoin; + @computed double get price { final price = fiatConvertationStore.prices[appStore.wallet!.currency]; diff --git a/lib/view_model/dashboard/cake_features_view_model.dart b/lib/view_model/dashboard/cake_features_view_model.dart new file mode 100644 index 000000000..0a8fbc640 --- /dev/null +++ b/lib/view_model/dashboard/cake_features_view_model.dart @@ -0,0 +1,16 @@ +import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:mobx/mobx.dart'; + +part 'cake_features_view_model.g.dart'; + +class CakeFeaturesViewModel = CakeFeaturesViewModelBase with _$CakeFeaturesViewModel; + +abstract class CakeFeaturesViewModelBase with Store { + final IoniaService _ioniaService; + + CakeFeaturesViewModelBase(this._ioniaService); + + Future isIoniaUserAuthenticated() async { + return await _ioniaService.isLogined(); + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index f438c5724..b59dd1592 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -45,6 +45,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; @@ -201,6 +202,14 @@ abstract class DashboardViewModelBase with Store { return true; }); + + if (hasSilentPayments) { + silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet); + + reaction((_) => wallet.syncStatus, (SyncStatus syncStatus) { + silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet); + }); + } } @observable @@ -287,11 +296,36 @@ abstract class DashboardViewModelBase with Store { @observable WalletBase, TransactionInfo> wallet; - bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven; + @computed + bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet); + + @computed + bool get hasRescan => + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.monero || + wallet.type == WalletType.haven; + + @computed + bool get hasSilentPayments => wallet.type == WalletType.bitcoin; + + @computed + bool get showSilentPaymentsCard => hasSilentPayments && settingsStore.silentPaymentsCardDisplay; final KeyService keyService; final SharedPreferences sharedPreferences; + @observable + bool silentPaymentsScanningActive = false; + + @action + void setSilentPaymentsScanning(bool active) { + silentPaymentsScanningActive = active; + + if (hasSilentPayments) { + bitcoin!.setScanningActive(wallet, active); + } + } + BalanceViewModel balanceViewModel; AppStore appStore; diff --git a/lib/view_model/dashboard/market_place_view_model.dart b/lib/view_model/dashboard/market_place_view_model.dart deleted file mode 100644 index 470041127..000000000 --- a/lib/view_model/dashboard/market_place_view_model.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:mobx/mobx.dart'; - -part 'market_place_view_model.g.dart'; - -class MarketPlaceViewModel = MarketPlaceViewModelBase with _$MarketPlaceViewModel; - -abstract class MarketPlaceViewModelBase with Store { - final IoniaService _ioniaService; - - MarketPlaceViewModelBase(this._ioniaService); - - - Future isIoniaUserAuthenticated() async { - return await _ioniaService.isLogined(); - } -} \ No newline at end of file diff --git a/lib/view_model/rescan_view_model.dart b/lib/view_model/rescan_view_model.dart index c973b7b3f..dcc81c0a0 100644 --- a/lib/view_model/rescan_view_model.dart +++ b/lib/view_model/rescan_view_model.dart @@ -1,4 +1,6 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'rescan_view_model.g.dart'; @@ -8,11 +10,12 @@ class RescanViewModel = RescanViewModelBase with _$RescanViewModel; enum RescanWalletState { rescaning, none } abstract class RescanViewModelBase with Store { - RescanViewModelBase(this._wallet) - : state = RescanWalletState.none, - isButtonEnabled = false; + RescanViewModelBase(this.wallet) + : state = RescanWalletState.none, + isButtonEnabled = false, + doSingleScan = false; - final WalletBase _wallet; + final WalletBase wallet; @observable RescanWalletState state; @@ -20,11 +23,21 @@ abstract class RescanViewModelBase with Store { @observable bool isButtonEnabled; + @observable + bool doSingleScan; + + @computed + bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin; + @action Future rescanCurrentWallet({required int restoreHeight}) async { state = RescanWalletState.rescaning; - await _wallet.rescan(height: restoreHeight); - _wallet.transactionHistory.clear(); + if (wallet.type != WalletType.bitcoin) { + wallet.rescan(height: restoreHeight); + wallet.transactionHistory.clear(); + } else { + bitcoin!.rescan(wallet, height: restoreHeight, doSingleScan: doSingleScan); + } state = RescanWalletState.none; } -} \ No newline at end of file +} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 534e501dd..a00cfe0cc 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -229,7 +229,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title; @computed - bool get isReadyForSend => wallet.syncStatus is SyncedSyncStatus; + bool get isReadyForSend => + wallet.syncStatus is SyncedSyncStatus || + // If silent payments scanning, can still send payments + (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus); @computed List