diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 36fb28a25..99a489bb1 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -118,7 +118,7 @@ jobs: pushd scripts/monero_c/release mkdir -p $(git describe --tags) pushd $(git describe --tags) - wget https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip + wget https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC2/release-bundle.zip unzip release-bundle.zip rm release-bundle.zip popd diff --git a/.gitignore b/.gitignore index 9fce14fcf..268167bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -225,3 +225,5 @@ scripts/zcash_lib scripts/android/yttrium scripts/bitbox_flutter scripts/reown_flutter + +*.salive \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 6c1540222..26f5544c9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -60,7 +60,7 @@ android { defaultConfig { applicationId appProperties['id'] - minSdkVersion 24 + minSdkVersion 26 targetSdkVersion 36 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index a7364d670..0f15c75bc 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -128,6 +128,14 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/images/hardware_wallet/device_ledger_nano_gen_5.svg b/assets/images/hardware_wallet/device_ledger_nano_gen_5.svg deleted file mode 100644 index 9d2fc8bbd..000000000 --- a/assets/images/hardware_wallet/device_ledger_nano_gen_5.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/images/hardware_wallet/device_trezor_safe_5.svg b/assets/images/hardware_wallet/device_trezor_safe_5.svg deleted file mode 100644 index 14c0329be..000000000 --- a/assets/images/hardware_wallet/device_trezor_safe_5.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/images/hardware_wallet/ledger_flex.png b/assets/images/hardware_wallet/ledger_flex.png deleted file mode 100644 index fa39f241f..000000000 Binary files a/assets/images/hardware_wallet/ledger_flex.png and /dev/null differ diff --git a/assets/images/hardware_wallet/ledger_nano_s.png b/assets/images/hardware_wallet/ledger_nano_s.png deleted file mode 100644 index 02777aeb6..000000000 Binary files a/assets/images/hardware_wallet/ledger_nano_s.png and /dev/null differ diff --git a/assets/images/hardware_wallet/ledger_stax.png b/assets/images/hardware_wallet/ledger_stax.png deleted file mode 100644 index 06c9c848e..000000000 Binary files a/assets/images/hardware_wallet/ledger_stax.png and /dev/null differ diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 39e0400c9..3b1dd9349 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -29,7 +29,7 @@ enum DeviceConnectionType { break; case HardwareWalletType.trezor: isSupported = [ - // WalletType.monero, + WalletType.monero, WalletType.bitcoin, WalletType.litecoin, WalletType.ethereum, diff --git a/cw_core/lib/hardware/hardware_wallet_service.dart b/cw_core/lib/hardware/hardware_wallet_service.dart index 19125ba84..94cbdc443 100644 --- a/cw_core/lib/hardware/hardware_wallet_service.dart +++ b/cw_core/lib/hardware/hardware_wallet_service.dart @@ -4,7 +4,8 @@ import 'dart:typed_data'; import 'package:cw_core/hardware/hardware_account_data.dart'; abstract class HardwareWalletService { - Future> getAvailableAccounts({int index = 0, int limit = 5}); + Future> getAvailableAccounts({int index = 0, int limit = 5}) => + throw UnimplementedError(); Future signTransaction({required String transaction}) => throw UnimplementedError(); diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 1ce397267..5877ad2da 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -44,7 +44,7 @@ enum HardwareWalletType { coldcard, seedsigner, keystone, - trezor, + trezor; } enum WalletInfoAddressType { diff --git a/cw_monero/lib/ledger.dart b/cw_monero/lib/ledger.dart index 7707acd48..7c1742ada 100644 --- a/cw_monero/lib/ledger.dart +++ b/cw_monero/lib/ledger.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:typed_data'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; @@ -118,3 +119,10 @@ void _logLedgerCommand(Uint8List command, [bool isResponse = true]) { "> ${_ledgerMoneroCommands[command[1]]} ${toHexString(command.sublist(2))}"); } } + +class MoneroLedgerService extends HardwareWalletService { + final LedgerConnection connection; + + MoneroLedgerService(this.connection); +} + diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index e315f24fa..30c550721 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -37,6 +37,7 @@ import 'package:cw_monero/monero_unspent.dart'; import 'package:cw_monero/monero_wallet_addresses.dart'; import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; +import 'package:cw_monero/trezor.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; @@ -385,8 +386,42 @@ abstract class MoneroWalletBase extends WalletBase syncTrezor() async { + if (trezorService == null) throw Exception("Trezor not connected"); + + final ptr = Pointer.fromAddress(currentWallet!.ffiAddress()); + final tdis = monero.Wallet_exportTrezorTdis(ptr); + + final response = await Trezor(trezorService!).keyImageSync(tdis); + + final success = monero.Wallet_importTrezorEncryptedKeyImagesJson(ptr, response); + + if (!success) throw Exception(monero.Wallet_errorString(ptr)); + } + + Future signTrezorTransaction(String json) async { + if (trezorService == null) throw Exception("Trezor not connected"); + return Trezor(trezorService!).signTransaction(json); + } + @override Future createTransaction(Object credentials) async { + if (hardwareWalletType == HardwareWalletType.trezor) { + for (int i = 0; i < 2; i++) { + try { + return await _createTransaction(credentials); + } catch (e) { + printV(e); + } + await save(); + await Future.delayed(Duration(seconds: i)); + } + } + return await _createTransaction(credentials); + } + + Future _createTransaction(Object credentials) async { final _credentials = credentials as MoneroTransactionCreationCredentials; final inputs = []; final outputs = _credentials.outputs; diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 2a153c5e1..1b0e9e347 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -3,8 +3,8 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; -import 'package:collection/collection.dart'; import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -20,8 +20,8 @@ import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/bip39_seed.dart'; import 'package:cw_monero/ledger.dart'; import 'package:cw_monero/monero_wallet.dart'; +import 'package:cw_monero/trezor.dart'; import 'package:hive/hive.dart'; -import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:monero/monero.dart' as monero; import 'package:polyseed/polyseed.dart'; @@ -46,11 +46,11 @@ class MoneroNewWalletCredentials extends WalletCredentials { class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials { MoneroRestoreWalletFromHardwareCredentials( {required String name, - required this.ledgerConnection, + required this.hardwareWalletService, int height = 0, String? password}) : super(name: name, password: password, height: height); - LedgerConnection ledgerConnection; + HardwareWalletService hardwareWalletService; } class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { @@ -193,7 +193,7 @@ class MoneroWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, password: password); - if (wallet.isHardwareWallet) { + if (wallet.hardwareWalletType == HardwareWalletType.ledger) { wallet.setLedgerConnection(gLedger!); gLedger = null; } @@ -301,15 +301,29 @@ class MoneroWalletService extends WalletService< try { final path = await pathForWallet(name: credentials.name, type: getType()); final password = credentials.password; - final height = credentials.height; - enableLedgerExchange(credentials.ledgerConnection); + if (credentials.hardwareWalletService case MoneroLedgerService service) { + enableLedgerExchange(service.connection); - await monero_wallet_manager.restoreWalletFromHardwareWallet( + await monero_wallet_manager.restoreWalletFromHardwareWallet( path: path, password: password!, - restoreHeight: height!, - deviceName: 'Ledger'); + restoreHeight: credentials.height!, + deviceName: 'Ledger', + ); + } else if (credentials.hardwareWalletService case MoneroTrezorService service) { + final watchCredentials = await Trezor(service).getWatchCredentials(); + + monero_wallet_manager.restoreWalletFromKeys( + path: path, + password: credentials.password!, + language: "English", + restoreHeight: credentials.height!, + address: watchCredentials.address, + viewKey: watchCredentials.watchKey, + spendKey: "", + ); + } final wallet = MoneroWallet( walletInfo: credentials.walletInfo!, @@ -567,7 +581,7 @@ class MoneroWalletService extends WalletService< if (walletInfo == null) { return false; } - return walletInfo.isHardwareWallet; + return walletInfo.hardwareWalletType == HardwareWalletType.ledger; } } @@ -585,4 +599,4 @@ Future closeWalletAwaitIfShould(int wmaddr, int waddr) async { monero.WalletManager_errorString(Pointer.fromAddress(wmaddr)); })); } -} \ No newline at end of file +} diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index e1d191eac..c32a2cc9c 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'dart:ffi'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/transaction_history.dart' @@ -10,6 +13,7 @@ import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_wallet.dart'; +import 'package:monero/monero.dart' as monero; class DoubleSpendException implements Exception { DoubleSpendException(); @@ -43,22 +47,39 @@ class PendingMoneroTransaction with PendingTransaction { CryptoCurrency.xmr, pendingTransactionDescription.fee); @override - bool shouldCommitUR() => isViewOnly; + bool shouldCommitUR() => isViewOnly && wallet.hardwareWalletType != HardwareWalletType.trezor; @override Future commit() async { - try { - await monero_transaction_history.commitTransactionFromPointerAddress( - address: pendingTransactionDescription.pointerAddress, - useUR: false); - } catch (e) { - final message = e.toString(); + if (wallet.hardwareWalletType == HardwareWalletType.trezor) { + final ptr = Pointer.fromAddress(pendingTransactionDescription.pointerAddress); + final ret = await monero.PendingTransaction_commitTrezor(ptr, 0); - if (message.contains('Reason: double spend')) { - throw DoubleSpendException(); + final json = await wallet.signTrezorTransaction(ret); + + final wptr = Pointer.fromAddress(currentWallet!.ffiAddress()); + + final suc = await monero.Wallet_submitTransactionHex(wptr, json); + + if (!suc) { + final err = monero.UnsignedTransaction_errorString(ptr); + printV(err); + throw err; } + } else { + try { + await monero_transaction_history.commitTransactionFromPointerAddress( + address: pendingTransactionDescription.pointerAddress, + useUR: false); + } catch (e) { + final message = e.toString(); - rethrow; + if (message.contains('Reason: double spend')) { + throw DoubleSpendException(); + } + + rethrow; + } } storeSync(force: true); unawaited(() async { diff --git a/cw_monero/lib/trezor.dart b/cw_monero/lib/trezor.dart new file mode 100644 index 000000000..f2651d062 --- /dev/null +++ b/cw_monero/lib/trezor.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:cw_core/hardware/hardware_wallet_service.dart'; +import 'package:trezor_flutter/trezor_flutter.dart'; + +class MoneroTrezorService extends HardwareWalletService { + final TrezorClient client; + + MoneroTrezorService(this.client); +} + +class MoneroTrezorWatchCredentials { + final String watchKey; + final String address; + + const MoneroTrezorWatchCredentials(this.watchKey, this.address); +} + +class Trezor { + final MoneroTrezorService service; + + Trezor(this.service); + + Future getWatchCredentials() async { + final credentials = await TrezorMonero(service.client).getWatchCredentials(); + + return MoneroTrezorWatchCredentials(credentials.$1, credentials.$2); + } + + Future keyImageSync(String tdis) async { + final tdisMap = jsonDecode(tdis) as Map; + final tdisList = tdisMap["tdis"] as List; + + final txIds = []; + for (final tdi in tdisList) { + txIds.add(MoneroKeyImageTxData( + outKey: tdi["out_key"], + txPubKey: tdi["tx_pub_key"], + internalOutputIndex: tdi["internal_output_index"], + subAddrMajor: tdi["sub_addr_major"], + subAddrMinor: tdi["sub_addr_minor"], + additionalTxPubKeys: (tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [] + )); + } + final keyImages = await TrezorMonero(service.client).syncKeyImages(txIds); + + return jsonEncode(keyImages.toMap()); + } + + Future signTransaction(String json) async { + return TrezorMonero(service.client).signTransaction(jsonDecode(json)); + } +} diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 6f6174d08..d00f2149e 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -296,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + fast_base58: + dependency: transitive + description: + name: fast_base58 + sha256: "611f65633b734f27a850b51371b3eba993a5165650e12e8e7b02959f3768ba06" + url: "https://pub.dev" + source: hosted + version: "0.2.1" ffi: dependency: "direct main" description: @@ -342,10 +350,10 @@ packages: dependency: transitive description: name: flutter_web_bluetooth - sha256: "1363831def5eed1e1064d1eca04e8ccb35446e8f758579c3c519e156b77926da" + sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" frontend_server_client: dependency: transitive description: @@ -494,18 +502,18 @@ packages: dependency: "direct main" description: name: ledger_flutter_plus - sha256: "1c03f3c4a9754b5f0170a9eb9552ec54fa86e985f8ee71a255ee2c5629b53d31" + sha256: "59c5a6062505c721c2ca909fa085c52cffe93de995174b8418b54a6acf52eb70" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.6" ledger_usb_plus: dependency: transitive description: name: ledger_usb_plus - sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + sha256: "911df2d1d55cdae612ff2c9db140cf71a9b28a67ba16adfdc4983cd9f02c531e" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.8" logging: dependency: transitive description: @@ -582,9 +590,9 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 - resolved-ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 - url: "https://github.com/mrcyjanek/monero_c" + ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d" + resolved-ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d" + url: "http://github.com/mrcyjanek/monero_c.git" source: git version: "0.0.0" mutex: @@ -724,6 +732,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" provider: dependency: transitive description: @@ -962,6 +978,24 @@ packages: relative: true source: path version: "0.0.1" + trezor_flutter: + dependency: "direct main" + description: + path: trezor-flutter + ref: "4bf54339a1caefd4c53a4a8873a9686ec13a9107" + resolved-ref: "4bf54339a1caefd4c53a4a8873a9686ec13a9107" + url: "https://github.com/cake-tech/trezor-flutter.git" + source: git + version: "0.1.0" + trezor_usb_transport: + dependency: transitive + description: + path: trezor_usb_transport + ref: "4bf54339a1caefd4c53a4a8873a9686ec13a9107" + resolved-ref: "4bf54339a1caefd4c53a4a8873a9686ec13a9107" + url: "https://github.com/cake-tech/trezor-flutter.git" + source: git + version: "0.0.1" tuple: dependency: transitive description: @@ -982,10 +1016,10 @@ packages: dependency: transitive description: name: universal_ble - sha256: "1fad089150a29db82b3b7d60327e18c5ad6b3a5bb509defc1c690b0a76b9c098" + sha256: "8325ca9f5cce2fba2b4efef74a4fcfe7144f303b409d6fc6ed16197b00a1a241" url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "1.2.0" universal_platform: dependency: transitive description: diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 7f38c0ea0..4c10e94d7 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -25,11 +25,16 @@ dependencies: path: ../cw_core monero: git: - url: https://github.com/mrcyjanek/monero_c - ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 + url: https://github.com/mrcyjanek/monero_c.git + ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d path: impls/monero.dart mutex: ^3.1.0 ledger_flutter_plus: ^1.4.1 + trezor_flutter: + git: + url: https://github.com/cake-tech/trezor-flutter.git + ref: 4bf54339a1caefd4c53a4a8873a9686ec13a9107 + path: trezor-flutter dev_dependencies: flutter_test: diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 734af9338..55cfb8fdf 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -502,9 +502,9 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 - resolved-ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 - url: "https://github.com/mrcyjanek/monero_c" + ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d" + resolved-ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d" + url: "http://github.com/mrcyjanek/monero_c.git" source: git version: "0.0.0" mutex: diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index a23fc3955..ca9c70bea 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -23,8 +23,8 @@ dependencies: path: ../cw_core monero: git: - url: https://github.com/mrcyjanek/monero_c - ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 # monero_c hash + url: https://github.com/mrcyjanek/monero_c.git + ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 662ccded6..6e2916d71 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -507,9 +507,9 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 - resolved-ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 - url: "https://github.com/mrcyjanek/monero_c" + ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d" + resolved-ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d" + url: "http://github.com/mrcyjanek/monero_c.git" source: git version: "0.0.0" nested: diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml index 8eb5d3e14..16585b715 100644 --- a/cw_zano/pubspec.yaml +++ b/cw_zano/pubspec.yaml @@ -24,8 +24,8 @@ dependencies: fluttertoast: ^8.2.12 monero: git: - url: https://github.com/mrcyjanek/monero_c - ref: bc8d1a0b75b97156d71579581b4cdfe58c777ed2 # monero_c hash + url: https://github.com/mrcyjanek/monero_c.git + ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d path: impls/monero.dart dev_dependencies: flutter_test: diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart index d885e3e14..888c2ce03 100644 --- a/integration_test/components/common_test_flows.dart +++ b/integration_test/components/common_test_flows.dart @@ -231,12 +231,9 @@ class CommonTestFlows { await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type); + // Tapping the wallet type navigates directly (no separate Next button). _tester.printToConsole('Tapping wallet type: ${type.name}'); await _newWalletTypePageRobot.selectWalletType(type); - await _tester.pumpAndSettle(Duration(milliseconds: 500)); - - _tester.printToConsole('Pressing next button'); - await _newWalletTypePageRobot.onNextButtonPressed(); await _tester.pumpAndSettle(Duration(milliseconds: 2000)); diff --git a/integration_test/robots/new_wallet_type_page_robot.dart b/integration_test/robots/new_wallet_type_page_robot.dart index fc3e94f59..11785b67d 100644 --- a/integration_test/robots/new_wallet_type_page_robot.dart +++ b/integration_test/robots/new_wallet_type_page_robot.dart @@ -77,11 +77,9 @@ class NewWalletTypePageRobot { tester.printToConsole('Wallet type ${type.name} is now visible and tappable'); } + // Tapping a wallet type now navigates directly (the page no longer has a + // separate "Next" button — selection happens on tap). Future selectWalletType(WalletType type) async { await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key'); } - - Future onNextButtonPressed() async { - await commonTestCases.tapItemByKey('new_wallet_type_next_button_key'); - } } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e661632c8..2d32cb833 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -295,6 +295,7 @@ SPEC CHECKSUMS: universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e + warp_api: 997d8fb3e777669bc6260d0c12e56cd40b9db1d1 YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966 PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index f927b702d..582cccd5d 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -336,7 +336,7 @@ class DFXBuyProvider extends BuyProvider { required String cryptoCurrencyAddress, String? countryCode}) async { if (wallet.isHardwareWallet) { - if (!hardwareWalletVM!.isConnected) { + if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) { await Navigator.of(context).pushNamed(Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: wallet.walletInfo.type, diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 7cd992812..2c26fa4ca 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -173,7 +173,7 @@ class RobinhoodBuyProvider extends BuyProvider { required String cryptoCurrencyAddress, String? countryCode}) async { if (wallet.isHardwareWallet) { - if (!hardwareWalletVM!.isConnected) { + if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) { await Navigator.of(context).pushNamed(Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: wallet.walletInfo.type, diff --git a/lib/di.dart b/lib/di.dart index 991e2f7ee..cd7ee62d1 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -240,7 +240,7 @@ import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/trezor_view_model.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart'; import 'package:cake_wallet/view_model/integrations/deuro_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'; @@ -414,7 +414,7 @@ Future setup({ switch(type) { case HardwareWalletType.bitbox: return getIt(); case HardwareWalletType.ledger: return getIt(); - case HardwareWalletType.trezor: return getIt(); + case HardwareWalletType.trezor: return getIt(); case HardwareWalletType.cupcake: case HardwareWalletType.coldcard: case HardwareWalletType.seedsigner: @@ -429,7 +429,7 @@ Future setup({ getIt.registerLazySingleton(() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet")); - getIt.registerLazySingleton(() => TrezorViewModel(getIt())); + getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt())); getIt.registerFactory(() => KeyService(getIt.get())); diff --git a/lib/entities/hardware_wallet/hardware_wallet_device.dart b/lib/entities/hardware_wallet/hardware_wallet_device.dart index 646f5a2c2..757e68099 100644 --- a/lib/entities/hardware_wallet/hardware_wallet_device.dart +++ b/lib/entities/hardware_wallet/hardware_wallet_device.dart @@ -1,5 +1,6 @@ import 'package:bitbox_flutter/usb/bitbox_device.dart' as bitbox; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; +import 'package:trezor_flutter/trezor_flutter.dart' as trezor; abstract class HardwareWalletDevice { String get name; @@ -41,6 +42,21 @@ class BitboxHardwareWalletDevice extends HardwareWalletDevice { HardwareWalletConnectionType get connectionType => HardwareWalletConnectionType.usb; } +class TrezorHardwareWalletDevice extends HardwareWalletDevice { + final trezor.TrezorDevice device; + + TrezorHardwareWalletDevice(this.device); + + @override + String get name => device.name; + + @override + HardwareWalletDeviceType get type => device.deviceInfo.toGeneric(); + + @override + HardwareWalletConnectionType get connectionType => device.connectionType.toGeneric(); +} + enum HardwareWalletDeviceType { ledgerBlue, ledgerNanoS, @@ -50,7 +66,12 @@ enum HardwareWalletDeviceType { ledgerStax, ledgerFlex, BitBox02, - BitBox02Nova; + BitBox02Nova, + trezorModelOne, + trezorModelT, + trezorSafe3, + trezorSafe5, + trezorSafe7; } enum HardwareWalletConnectionType { @@ -60,7 +81,7 @@ enum HardwareWalletConnectionType { qr; } -extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType { +extension LedgerToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType { HardwareWalletDeviceType toGeneric() { switch (this) { case ledger.LedgerDeviceType.blue: @@ -81,7 +102,24 @@ extension ToGenericHardwareWalletDeviceType on ledger.LedgerDeviceType { } } -extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType { +extension TrezorToGenericHardwareWalletDeviceType on trezor.TrezorDeviceType { + HardwareWalletDeviceType toGeneric() { + switch (this) { + case trezor.TrezorDeviceType.modelOne: + return HardwareWalletDeviceType.trezorModelOne; + case trezor.TrezorDeviceType.modelT: + return HardwareWalletDeviceType.trezorModelT; + case trezor.TrezorDeviceType.safe3: + return HardwareWalletDeviceType.trezorSafe3; + case trezor.TrezorDeviceType.safe5: + return HardwareWalletDeviceType.trezorSafe5; + case trezor.TrezorDeviceType.safe7: + return HardwareWalletDeviceType.trezorSafe7; + } + } +} + +extension LedgerToGenericHardwareWalletConnectionType on ledger.ConnectionType { HardwareWalletConnectionType toGeneric() { switch (this) { case ledger.ConnectionType.usb: @@ -91,3 +129,14 @@ extension ToGenericHardwareWalletConnectionType on ledger.ConnectionType { } } } + +extension TrezorToGenericHardwareWalletConnectionType on trezor.ConnectionType { + HardwareWalletConnectionType toGeneric() { + switch (this) { + case trezor.ConnectionType.usb: + return HardwareWalletConnectionType.usb; + case trezor.ConnectionType.ble: + return HardwareWalletConnectionType.ble; + } + } +} diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 08143b3b3..0a378e219 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -236,13 +236,14 @@ class CWMonero extends Monero { required String name, required String password, required int height, - required ledger.LedgerConnection ledgerConnection, + required HardwareWalletService hardwareWalletService, }) => MoneroRestoreWalletFromHardwareCredentials( - name: name, - password: password, - height: height, - ledgerConnection: ledgerConnection); + name: name, + password: password, + height: height, + hardwareWalletService: hardwareWalletService, + ); @override WalletCredentials createMoneroRestoreWalletFromSeedCredentials( @@ -416,9 +417,7 @@ class CWMonero extends Monero { } @override - void monerocCheck() { - checkIfMoneroCIsFine(); - } + void monerocCheck() => checkIfMoneroCIsFine(); @override Future setLedgerConnection(Object wallet, ledger.LedgerConnection connection) async { @@ -427,29 +426,41 @@ class CWMonero extends Monero { } @override - void resetLedgerConnection() { - disableLedgerExchange(); - } + void resetLedgerConnection() => disableLedgerExchange(); @override - void setGlobalLedgerConnection(ledger.LedgerConnection connection) { - gLedger = connection; - } + void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection; @override String? getLastLedgerCommand() => latestLedgerCommand; - bool isViewOnly() { - return isViewOnlyBySpendKey(null); + @override + void setHardwareWalletService(Object wallet, HardwareWalletService service) { + final moneroWallet = wallet as MoneroWallet; + moneroWallet.trezorService = service as MoneroTrezorService; } - @override - Map> debugCallLength() { - return monero_wallet_api.debugCallLength(); - } @override - Map getWalletCacheDebug() { - return monero_wallet_api.getWalletCacheDebug(); + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) => + MoneroLedgerService(connection); + + @override + HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorClient client) => + MoneroTrezorService(client); + + @override + Future syncTrezor(Object wallet) async { + final moneroWallet = wallet as MoneroWallet; + + return moneroWallet.syncTrezor(); } + + bool isViewOnly() => isViewOnlyBySpendKey(null); + + @override + Map> debugCallLength() => monero_wallet_api.debugCallLength(); + + @override + Map getWalletCacheDebug() => monero_wallet_api.getWalletCacheDebug(); } diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index c3f5da957..9292d1af2 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -726,12 +726,13 @@ class _NewSendPageState extends State { } if (widget.sendViewModel.wallet.isHardwareWallet) { - if (!widget.sendViewModel.hardwareWalletViewModel!.isConnected) { + if (!widget.sendViewModel.hardwareWalletViewModel! + .isConnected(widget.sendViewModel.walletType)) { await Navigator.of(context).pushNamed(Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: widget.sendViewModel.walletType, hardwareWalletType: widget.sendViewModel.wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (BuildContext context, _) { + onConnectDevice: (_, __) { widget.sendViewModel.hardwareWalletViewModel! .initWallet(widget.sendViewModel.wallet); Navigator.of(context).pop(); @@ -748,8 +749,12 @@ class _NewSendPageState extends State { amount += item.formattedCryptoAmount; } if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { - await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, - arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet)); + if (widget.sendViewModel.wallet.hardwareWalletType == HardwareWalletType.trezor) { + await monero!.syncTrezor(widget.sendViewModel.wallet); + } else { + await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, + arguments: monero!.exportOutputsUR(widget.sendViewModel.wallet)); + } await Future.delayed(Duration(seconds: 1)); // wait for monero to refresh the state } if (monero!.needExportOutputs(widget.sendViewModel.wallet, amount)) { diff --git a/lib/new-ui/widgets/coins_page/wallet_info.dart b/lib/new-ui/widgets/coins_page/wallet_info.dart index 21c83db44..4963513fc 100644 --- a/lib/new-ui/widgets/coins_page/wallet_info.dart +++ b/lib/new-ui/widgets/coins_page/wallet_info.dart @@ -86,11 +86,11 @@ class WalletInfoBar extends StatelessWidget { case null: return null; case HardwareWalletType.bitbox: - return "assets/images/hardware_wallet/device_bitbox.svg"; + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; case HardwareWalletType.ledger: - return "assets/images/hardware_wallet/device_ledger_nano_x.svg"; + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; case HardwareWalletType.trezor: - return "assets/images/hardware_wallet/device_trezor_safe_5.svg"; + return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg"; case HardwareWalletType.cupcake: return "assets/images/cupcake.svg"; case HardwareWalletType.coldcard: diff --git a/lib/new-ui/widgets/digit_input.dart b/lib/new-ui/widgets/digit_input.dart new file mode 100644 index 000000000..d6889db67 --- /dev/null +++ b/lib/new-ui/widgets/digit_input.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class DigitInputController { + String _text = ""; + + String get text => _text; + + set text(String newText) { + _text = newText; + for (final listener in _listeners) { + listener.call(); + } + } + + final List _listeners = []; + + void addListener(void Function() listener) => _listeners.add(listener); +} + +class DigitInputPill extends StatelessWidget { + const DigitInputPill({super.key, this.digit, required this.highlighted}); + + final String? digit; + final bool highlighted; + + @override + Widget build(BuildContext context) { + return Container( + width: 51, + height: 84, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + border: + highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null, + ), + child: Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Text(digit ?? " ", style: TextStyle(fontSize: 40)), + ), + ), + ); + } +} + +class DigitInput extends StatefulWidget { + const DigitInput({ + super.key, + required this.controller, + required this.desiredLength, + this.breakAt = 3, + }); + + final DigitInputController controller; + final int desiredLength; + final int breakAt; + + @override + State createState() => _DigitInputState(); +} + +class _DigitInputState extends State implements TextInputClient { + TextInputConnection? _textInputConnection; + + @override + void initState() { + super.initState(); + _openVirtualKeyboard(); + } + + void _openVirtualKeyboard() { + if (_textInputConnection == null || !_textInputConnection!.attached) { + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + inputType: TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + inputAction: TextInputAction.none, + ), + ); + } + + _textInputConnection!.setEditingState( + TextEditingValue( + text: widget.controller.text, + selection: TextSelection.collapsed(offset: widget.controller.text.length), + ), + ); + _textInputConnection!.show(); + } + + void _closeVirtualKeyboard() => _textInputConnection?.close(); + + @override + void dispose() { + _closeVirtualKeyboard(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _openVirtualKeyboard, + child: SizedBox( + height: 84, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => DigitInputPill( + digit: widget.controller.text.length > index ? widget.controller.text[index] : null, + highlighted: widget.controller.text.length == index, + ), + separatorBuilder: (context, index) => + SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4), + itemCount: widget.desiredLength, + ), + ), + ); + } + + @override + void updateEditingValue(TextEditingValue value) { + final RegExp numberRegExp = RegExp(r'^\d{0,6}$'); + + if (numberRegExp.hasMatch(value.text)) { + setState(() => widget.controller.text = value.text); + } else { + // flutter keeps the text editing state regardless of whether you actually used it or not. + // so, if validation fails, we gotta set it back to what it was explicitly + _textInputConnection?.setEditingState( + TextEditingValue( + text: widget.controller.text, + selection: TextSelection.collapsed(offset: widget.controller.text.length), + ), + ); + } + } + + @override + void performAction(TextInputAction action) {} + + @override + TextEditingValue? get currentTextEditingValue => TextEditingValue(text: widget.controller.text); + + @override + void connectionClosed() => _textInputConnection = null; + + @override + AutofillScope? get currentAutofillScope => null; + + @override + void performPrivateCommand(String action, Map data) {} + + @override + void showAutocorrectionPromptRect(int start, int end) {} + + @override + void updateFloatingCursor(RawFloatingCursorPoint point) {} + + @override + void insertTextPlaceholder(Size size) {} + + @override + void removeTextPlaceholder() {} + + @override + void showToolbar() {} + + @override + void performSelector(String selectorName) {} + + @override + void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {} + + @override + void insertContent(KeyboardInsertedContent content) {} +} diff --git a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart new file mode 100644 index 000000000..100325dbe --- /dev/null +++ b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart @@ -0,0 +1,60 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/material.dart'; + +class HardwareWalletProceedOnDeviceMessage extends StatelessWidget { + const HardwareWalletProceedOnDeviceMessage({super.key, required this.hardwareWalletType}); + + final HardwareWalletType hardwareWalletType; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column(spacing: 8, children: [ + if (hardwareWalletIcon != null) + CakeImageWidget( + imageUrl: hardwareWalletIcon!, + width: 36, + height: 36, + colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + Text( + S.of(context).proceed_on_device, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ) + ]), + ), + ); + } + + String? get hardwareWalletIcon { + switch (hardwareWalletType) { + case HardwareWalletType.bitbox: + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; + case HardwareWalletType.ledger: + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; + case HardwareWalletType.trezor: + return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg"; + case HardwareWalletType.cupcake: + return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + return "assets/images/hardware_wallet/device_qr.svg"; + } + } +} diff --git a/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart new file mode 100644 index 000000000..2a06f37e6 --- /dev/null +++ b/lib/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart @@ -0,0 +1,338 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/digit_input.dart'; +import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:cake_wallet/new-ui/widgets/send_page/directional_switcher.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; + +class HardwareWalletProceedOnDeviceSheet extends StatefulWidget { + const HardwareWalletProceedOnDeviceSheet({ + super.key, + required this.hardwareWalletType, + required this.trezorConnectVM, + required this.onRetry, + }); + + final HardwareWalletType hardwareWalletType; + final TrezorConnectViewModelBase trezorConnectVM; + final void Function() onRetry; + + @override + State createState() => + _HardwareWalletProceedOnDeviceSheetState(); +} + +class _HardwareWalletProceedOnDeviceSheetState extends State { + TrezorParingState _paringState = TrezorParingState.initial; + + final DigitInputController _controller = DigitInputController(); + + static const pinOpenDuration = Duration(milliseconds: 300); + + // you should probably check if there can be other pin lengths + static const pinLength = 6; + + ReactionDisposer? paringStateReaction; + + @override + void initState() { + super.initState(); + _controller.addListener(() => setState(() {})); + + _paringState = widget.trezorConnectVM.paringState; + paringStateReaction = reaction((_) => widget.trezorConnectVM.paringState, (paringState) { + if (paringState is SuccessTrezorParingState) { + if (mounted) Navigator.of(context).pop(); + return; + } + setState(() => _paringState = paringState); + }); + } + + @override + void dispose() { + super.dispose(); + paringStateReaction?.reaction.dispose(); + } + + bool get _isAwaitingPin => _paringState == TrezorParingState.enterPin; + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: SafeArea( + bottom: false, + child: Container( + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SafeArea( + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Column( + children: [ + ModalTopBar( + title: "", + leadingWidget: AnimatedSwitcher( + layoutBuilder: (currentChild, previousChildren) { + return Stack( + alignment: Alignment.centerLeft, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + duration: pinOpenDuration, + child: Text( + key: ValueKey(pageTitle), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + pageTitle, + ), + ), + trailingIcon: Icon( + Icons.close, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onTrailingPressed: () => showPopUp( + context: context, + builder: (context) => AlertWithTwoActions( + alertTitle: S.of(context).are_you_sure_exit, + alertContent: S.of(context).hww_exit_desc, + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).yes_exit, + rightAlertButtonStyle: AlertButtonStyle.error(context), + actionLeftButton: Navigator.of(context).pop, + actionRightButton: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ), + ), + ), + Expanded( + child: DirectionalAnimatedSwitcher( + duration: Duration(milliseconds: 400), + child: content, + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 24, right: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(), + AnimatedOpacity( + curve: Curves.easeOutQuad, + opacity: hasFullPin ? 1 : 0, + duration: pinOpenDuration, + child: ModernButton( + backgroundColor: Theme.of(context).colorScheme.primary, + iconColor: Theme.of(context).colorScheme.onPrimary, + size: 36, + icon: Icon(Icons.arrow_forward), + onPressed: () => + widget.trezorConnectVM.setParingPin(_controller.text.trim()), + ), + ), + ], + ), + ) + ], + ), + )), + ), + ), + ); + } + + Widget get content { + if (_paringState is InitialTrezorParingState || _paringState is EnterPinTrezorParingState) { + return _enterPinCode(); + } + + if (_paringState is VerifyingPinTrezorParingState) return _verifyingCode(); + + if (_paringState is FailTrezorParingState) + return _errorBox((_paringState as FailTrezorParingState).message); + + return SizedBox.shrink(key: ValueKey(3)); + } + + void retry() { + _controller.text = ""; + widget.onRetry(); + } + + String get pageTitle { + if (_paringState is InitialTrezorParingState) return S.of(context).device_confirmation; + if (_isAwaitingPin) return S.of(context).pairing_code; + return ""; + } + + bool get hasFullPin => _controller.text.length == pinLength && _isAwaitingPin; + + String? get hardwareWalletIcon { + switch (widget.hardwareWalletType) { + case HardwareWalletType.bitbox: + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; + case HardwareWalletType.ledger: + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; + case HardwareWalletType.trezor: + return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg"; + case HardwareWalletType.cupcake: + return "assets/images/cupcake.svg"; + case HardwareWalletType.coldcard: + case HardwareWalletType.seedsigner: + case HardwareWalletType.keystone: + return "assets/images/hardware_wallet/device_qr.svg"; + } + } + + Widget _verifyingCode() => Container( + key: ValueKey(1), + width: MediaQuery.of(context).size.width, + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CupertinoActivityIndicator(radius: 36), + const SizedBox(), + Text( + "${S.of(context).verifying_code}...", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), + ), + Text( + S.of(context).this_can_take_few_seconds, + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + ); + + Widget _errorBox(String errorText) => Container( + key: ValueKey(2), + width: MediaQuery.of(context).size.width, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 12, + children: [ + const Spacer(), + Icon( + Icons.error_outline, + size: 80, + color: Theme.of(context).colorScheme.error, + ), + Text( + S.of(context).pairing_error, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + Text( + errorText, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const Spacer(), + NewPrimaryButton( + onPressed: retry, + text: S.of(context).try_again, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ) + ], + ), + ), + ); + + Widget _enterPinCode() => Column( + key: ValueKey(0), + spacing: 12, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + duration: pinOpenDuration, + curve: Curves.easeOutCubic, + width: _isAwaitingPin ? 40 : 100, + height: _isAwaitingPin ? 40 : 100, + child: CakeImageWidget( + imageUrl: hardwareWalletIcon, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + ), + AnimatedSwitcher( + duration: pinOpenDuration, + switchInCurve: Curves.easeInCubic, + switchOutCurve: Curves.easeOutCubic, + child: _isAwaitingPin + ? Column( + spacing: 24, + key: ValueKey(1), + children: [ + Text( + textAlign: TextAlign.center, + "${S.of(context).pairing_code_desc_1}\n${S.of(context).pairing_code_desc_2}", + ), + DigitInput( + controller: _controller, + desiredLength: pinLength, + ) + ], + ) + : Column( + key: ValueKey(0), + spacing: 12, + children: [ + Text( + S.of(context).proceed_on_device, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 20, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Text( + S.of(context).proceed_on_device_description, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + ], + ), + ) + ], + ); +} diff --git a/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart b/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart new file mode 100644 index 000000000..9b429edc4 --- /dev/null +++ b/lib/new-ui/widgets/hardware_wallet/trezor_paring_sheet.dart @@ -0,0 +1,106 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; +import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; +import 'package:flutter/material.dart'; + +class HardwareWalletTrezorParingSheet extends StatefulWidget { + const HardwareWalletTrezorParingSheet({super.key}); + + @override + State createState() => _HardwareWalletTrezorParingSheetState(); +} + +class _HardwareWalletTrezorParingSheetState extends State { + final TextEditingController _paringCodeController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: SafeArea( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ModalTopBar( + title: "Paring code", + onLeadingPressed: Navigator.of(context).pop, + onTrailingPressed: () {}, + leadingIcon: Icon(Icons.close), + ), + Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + SizedBox(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 75, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + height: 60, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainer, + width: 2, + ), + ), + child: TextField( + textAlign: TextAlign.left, + textAlignVertical: TextAlignVertical.center, + controller: _paringCodeController, + keyboardType: + TextInputType.numberWithOptions(signed: false, decimal: false), + decoration: InputDecoration( + hint: Text( + "000 000", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), + ), + border: InputBorder.none, + filled: true, + fillColor: Colors.transparent, + ), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), + ), + ), + ], + ), + SizedBox(), + NewPrimaryButton( + text: S.of(context).continue_text, + onPressed: () => Navigator.of(context).pop(_paringCodeController.text), + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ) + ], + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart b/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart index 68f8c6889..671579914 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_bottom_widget.dart @@ -1,16 +1,14 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/new-ui/widgets/confirm_swiper.dart'; +import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_message.dart'; import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cw_core/wallet_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:vector_graphics/vector_graphics.dart'; class SendConfirmBottomWidget extends StatelessWidget { const SendConfirmBottomWidget({super.key, required this.sendViewModel}); @@ -61,7 +59,7 @@ class SendConfirmBottomWidget extends StatelessWidget { text: "${S.of(context).signing_transaction}...", ); case IsAwaitingDeviceResponseState: - return HardwareWalletConfirmationMessage( + return HardwareWalletProceedOnDeviceMessage( hardwareWalletType: sendViewModel.wallet.hardwareWalletType!); case TransactionCommitting: return LoadingBottomWidget( @@ -173,58 +171,3 @@ class TransactionErrorActions extends StatelessWidget { ); } } - -class HardwareWalletConfirmationMessage extends StatelessWidget { - const HardwareWalletConfirmationMessage({super.key, required this.hardwareWalletType}); - - final HardwareWalletType hardwareWalletType; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column(spacing: 8, children: [ - if (hardwareWalletIcon != null) - CakeImageWidget(imageUrl: - hardwareWalletIcon!, - width: 36, - height: 36, - colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), - ), - Text( - S.of(context).proceed_on_device, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface), - ) - ]), - ), - ); - } - - String? get hardwareWalletIcon { - switch (hardwareWalletType) { - case HardwareWalletType.bitbox: - return "assets/images/hardware_wallet/device_bitbox.svg"; - case HardwareWalletType.ledger: - return "assets/images/hardware_wallet/device_ledger_nano_x.svg"; - case HardwareWalletType.trezor: - return "assets/images/hardware_wallet/device_trezor_safe_5.svg"; - case HardwareWalletType.cupcake: - return "assets/images/cupcake.svg"; - case HardwareWalletType.coldcard: - case HardwareWalletType.seedsigner: - case HardwareWalletType.keystone: - return "assets/images/hardware_wallet/device_qr.svg"; - } - } -} diff --git a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart index 9ccfb1453..3676cd6e4 100644 --- a/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart +++ b/lib/new-ui/widgets/swap_page/swap_confirm_sheet.dart @@ -41,7 +41,7 @@ class _SwapConfirmSheetState extends State { final sendVM = widget.exchangeTradeViewModel.sendViewModel; if (sendVM.wallet.isHardwareWallet) { - if (!sendVM.hardwareWalletViewModel!.isConnected) { + if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) { await Navigator.of(context).pushNamed(Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: sendVM.walletType, diff --git a/lib/router.dart b/lib/router.dart index a5af558eb..89365171a 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -371,7 +371,7 @@ Route createRoute(RouteSettings settings) { (_) => getIt.get( param1: NewWalletTypeArguments( onTypeSelected: (BuildContext context, WalletType type) { - if (hardwareWalletType == HardwareWalletType.trezor) { + if (hardwareWalletType == HardwareWalletType.trezor && type != WalletType.monero) { Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount, arguments: [type, hardwareWalletType]); return; diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 2122db44a..22e466cf3 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -3,10 +3,13 @@ import 'dart:io'; import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; @@ -91,6 +94,8 @@ class ConnectDevicePageBodyState extends State { var bleDevices = []; var usbDevices = []; + List get allDevices => [...bleDevices, ...usbDevices]; + late Timer? _usbRefreshTimer = null; late Timer? _bleRefreshTimer = null; late Timer? _bleStateTimer = null; @@ -158,24 +163,38 @@ class ConnectDevicePageBodyState extends State { Future _connectToDevice(HardwareWalletDevice device) async { final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType); - if (isConnected) widget.onConnectDevice(context, widget.hardwareWalletVM); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (isConnected) widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM); + }); } String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) { switch (deviceType) { case HardwareWalletDeviceType.ledgerNanoX: - return 'assets/images/hardware_wallet/ledger_nano_x.png'; + return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; case HardwareWalletDeviceType.ledgerNanoGen5: - return 'assets/images/hardware_wallet/device_ledger_nano_gen_5.svg'; + return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg'; case HardwareWalletDeviceType.ledgerStax: - return 'assets/images/hardware_wallet/ledger_stax.png'; + return 'assets/new-ui/hardware_wallets/ledger_stax.svg'; case HardwareWalletDeviceType.ledgerFlex: - return 'assets/images/hardware_wallet/ledger_flex.png'; + return 'assets/new-ui/hardware_wallets/ledger_flex.svg'; case HardwareWalletDeviceType.BitBox02: - return 'assets/images/hardware_wallet/device_bitbox.svg'; + case HardwareWalletDeviceType.BitBox02Nova: + return 'assets/new-ui/hardware_wallets/device_bitbox.svg'; + case HardwareWalletDeviceType.trezorModelOne: + return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg'; + case HardwareWalletDeviceType.trezorModelT: + return 'assets/new-ui/hardware_wallets/device_trezor_model_t.svg'; + case HardwareWalletDeviceType.trezorSafe3: + return 'assets/new-ui/hardware_wallets/device_trezor_safe_3.svg'; + case HardwareWalletDeviceType.trezorSafe5: + return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg'; + case HardwareWalletDeviceType.trezorSafe7: + return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg'; default: - return 'assets/images/hardware_wallet/ledger_nano_x.png'; + return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; } } @@ -197,25 +216,15 @@ class ConnectDevicePageBodyState extends State { Expanded( child: SingleChildScrollView( child: Column( + spacing: 24, children: [ - Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Text( - description, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), + CakeImageWidget( + imageUrl: "assets/new-ui/hardware_wallet.svg", ), - Offstage( - offstage: !longWait, - child: Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Text( - S.of(context).if_you_dont_see_your_device, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), + Text( + description, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, ), Observer( builder: (_) => Offstage( @@ -231,56 +240,69 @@ class ConnectDevicePageBodyState extends State { ), ), ), - if (bleDevices.length > 0) ...[ - Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Container( - width: double.infinity, - child: Text( - S.of(context).bluetooth, - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - ), - ...bleDevices - .map( - (device) => Padding( - padding: EdgeInsets.only(bottom: 20), - child: DeviceTile( - onPressed: () => _connectToDevice(device), - title: device.name, - leading: _getDeviceTileLeading(device.type), - connectionType: device.connectionType, + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18)), + child: ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) { + final item = allDevices[index]; + return GestureDetector( + onTap: () => _connectToDevice(item), + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + CakeImageWidget( + imageUrl: _getDeviceTileLeading(item.type), + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn), + ), + Text(item.name) + ], + ), + Row( + spacing: 12, + children: [ + if (item.connectionType == + HardwareWalletConnectionType.ble) + CakeImageWidget( + imageUrl: "assets/new-ui/bluetooth.svg", + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn), + ), + CakeImageWidget( + imageUrl: "assets/new-ui/arrow_forward.svg", + height: 16, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn)) + ], + ) + ], + ), + ), ), - ), - ) - .toList() - ], - if (usbDevices.length > 0) ...[ - Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Container( - width: double.infinity, - child: Text( - S.of(context).usb, - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - ), - ...usbDevices - .map( - (device) => Padding( - padding: EdgeInsets.only(bottom: 20), - child: DeviceTile( - onPressed: () => _connectToDevice(device), - title: device.name, - leading: _getDeviceTileLeading(device.type), - connectionType: device.connectionType, - ), - ), - ) - .toList(), - ], + ); + }, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh), + itemCount: allDevices.length), + ), if (widget.allowChangeWallet) ...[ PrimaryButton( text: S.of(context).wallets, @@ -293,10 +315,10 @@ class ConnectDevicePageBodyState extends State { ), ), ), - PrimaryButton( + NewPrimaryButton( text: S.of(context).how_to_connect, color: Theme.of(context).colorScheme.surfaceContainer, - textColor: Theme.of(context).colorScheme.onSecondaryContainer, + textColor: Theme.of(context).colorScheme.primary, onPressed: () => _onHowToConnect(context), ) ], diff --git a/lib/src/screens/connect_device/widgets/device_tile.dart b/lib/src/screens/connect_device/widgets/device_tile.dart index 803a648bb..4014e5040 100644 --- a/lib/src/screens/connect_device/widgets/device_tile.dart +++ b/lib/src/screens/connect_device/widgets/device_tile.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -43,14 +44,12 @@ class DeviceTile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (leading != null && !leading!.endsWith(".svg")) - Image.asset( - leading!, - height: 30, - color: Theme.of(context).colorScheme.onSurface, - ), - if (leading != null && leading!.endsWith(".svg")) - SvgPicture.asset(leading!, height: 30), + CakeImageWidget( + height: 30, + imageUrl: leading, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.onSurface, BlendMode.srcIn), + ), Expanded( child: Padding( padding: EdgeInsets.only(left: 16), diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index a6ea7b340..de05bccf4 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -34,11 +34,11 @@ class CryptoBalanceWidget extends StatelessWidget { case null: return null; case HardwareWalletType.bitbox: - return "assets/images/hardware_wallet/device_bitbox.svg"; + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; case HardwareWalletType.ledger: - return "assets/images/hardware_wallet/device_ledger_nano_x.svg"; + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; case HardwareWalletType.trezor: - return "assets/images/hardware_wallet/device_trezor_safe_5.svg"; + return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg"; case HardwareWalletType.cupcake: return "assets/images/cupcake.svg"; case HardwareWalletType.coldcard: diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index d7fd3196e..6ebd2526f 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -213,7 +213,7 @@ class ExchangeTradeState extends State { final sendVM = widget.exchangeTradeViewModel.sendViewModel; if (sendVM.wallet.isHardwareWallet) { - if (!sendVM.hardwareWalletViewModel!.isConnected) { + if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) { await Navigator.of(context).pushNamed(Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: sendVM.walletType, diff --git a/lib/src/screens/integrations/deuro/savings_page.dart b/lib/src/screens/integrations/deuro/savings_page.dart index 4a5d62a02..a08437822 100644 --- a/lib/src/screens/integrations/deuro/savings_page.dart +++ b/lib/src/screens/integrations/deuro/savings_page.dart @@ -160,7 +160,7 @@ class DEuroSavingsPage extends BasePage { Future _requireHardwareWallet(BuildContext context) async { if (_dEuroViewModel.wallet.isHardwareWallet) { - if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected) { + if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected(_dEuroViewModel.wallet.type)) { await Navigator.of(context).pushNamed(Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: _dEuroViewModel.wallet.type, diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index fff206836..4b170dc03 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -6,14 +6,8 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_utils.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; -import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart'; -import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/hardware/device_connection_type.dart'; @@ -21,10 +15,38 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; +class NewWalletTypePageHeader extends StatelessWidget { + const NewWalletTypePageHeader({super.key}); + + @override + Widget build(BuildContext context) => const SizedBox( + width: 138, + height: 75, + child: Stack( + children: [ + Positioned( + left: 0, + child: CakeImageWidget( + imageUrl: "assets/new-ui/hardware_wallet.svg", + width: 75, + height: 75, + ), + ), + Positioned( + left: 63, + child: CakeImageWidget( + imageUrl: "assets/new-ui/cake_coin.svg", + width: 75, + height: 75, + ), + ), + ], + ), + ); +} + class NewWalletTypePage extends BasePage { - NewWalletTypePage({ - required this.newWalletTypeArguments, - }); + NewWalletTypePage({required this.newWalletTypeArguments}); final NewWalletTypeArguments newWalletTypeArguments; @@ -38,23 +60,21 @@ class NewWalletTypePage extends BasePage { @override Function(BuildContext)? get pushToNextWidget => (context) { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.focusedChild?.unfocus(); - } - }; + final currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) currentFocus.focusedChild?.unfocus(); + }; @override Widget body(BuildContext context) => WalletTypeForm( - walletImage: currentTheme.isDark ? walletTypeImage : walletTypeLightImage, - isCreate: newWalletTypeArguments.isCreate, - onTypeSelected: newWalletTypeArguments.onTypeSelected, - hardwareWalletType: newWalletTypeArguments.hardwareWalletType, - ); + walletImage: currentTheme.isDark ? walletTypeImage : walletTypeLightImage, + isCreate: newWalletTypeArguments.isCreate, + onTypeSelected: newWalletTypeArguments.onTypeSelected, + hardwareWalletType: newWalletTypeArguments.hardwareWalletType, + ); } class WalletTypeForm extends StatefulWidget { - WalletTypeForm({ + const WalletTypeForm({ required this.walletImage, required this.isCreate, this.onTypeSelected, @@ -77,9 +97,6 @@ class WalletTypeFormState extends State { static const aspectRatioImage = 1.22; - final TextEditingController searchController = TextEditingController(); - - WalletType? selected; List types; List filteredTypes = []; @@ -87,21 +104,12 @@ class WalletTypeFormState extends State { void initState() { types = filteredTypes = availableWalletTypes .where((element) => - !widget.isHardwareWallet || - DeviceConnectionType.supportedConnectionTypes( - element, widget.hardwareWalletType!, Platform.isIOS) - .isNotEmpty) + !widget.isHardwareWallet || + DeviceConnectionType.supportedConnectionTypes( + element, widget.hardwareWalletType!, Platform.isIOS) + .isNotEmpty) .toList(); super.initState(); - - searchController.addListener(() { - setState(() { - filteredTypes = List.from(types.where((type) => walletTypeToDisplayName(type) - .toLowerCase() - .contains(searchController.text.toLowerCase()))); - return; - }); - }); } @override @@ -110,96 +118,102 @@ class WalletTypeFormState extends State { child: ConstrainedBox( constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( + spacing: 24, children: [ - Padding( - padding: EdgeInsets.only(top: 48), - child: Text( - S.of(context).choose_wallet_currency, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), + const NewWalletTypePageHeader(), + Text( + S.of(context).choose_wallet_currency, + textAlign: TextAlign.center, ), - Padding( - padding: EdgeInsets.fromLTRB(24, 24, 24, 12), - child: SearchBarWidget(searchController: searchController), - ), - Expanded( - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - scrollableKey: ValueKey('new_wallet_type_scrollable_key'), - content: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...filteredTypes.map( - (type) => Padding( - padding: EdgeInsets.only(top: 12), - child: SelectButton( - key: ValueKey('new_wallet_type_${type.name}_button_key'), - image: CakeImageWidget( - imageUrl: getCryptoCurrencyIconForWalletListItem(type), - height: 24, - width: 24, + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18), + ), + child: ListView.separated( + key: const ValueKey('new_wallet_type_scrollable_key'), + itemCount: filteredTypes.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final item = filteredTypes[index]; + final curr = getCryptoCurrencyForWalletListItem(item); + + return GestureDetector( + key: ValueKey('new_wallet_type_${item.name}_button_key'), + behavior: HitTestBehavior.opaque, + onTap: () { + onTypeSelected(item); + }, + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + CakeImageWidget( + height: 24, + width: 24, + imageUrl: curr.iconPath, + ), + const SizedBox(width: 12), + Text(curr.fullName ?? curr.name), + const SizedBox(width: 4), + Text( + curr.title, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + ], + ), + CakeImageWidget( + imageUrl: "assets/new-ui/arrow_forward.svg", + height: 16, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ) + ], + ), ), - text: walletTypeToDisplayName(type), - showTrailingIcon: false, - height: 54, - isSelected: selected == type, - onTap: () => setState(() => selected = type), - deviceConnectionTypes: widget.isHardwareWallet - ? DeviceConnectionType.supportedConnectionTypes( - type, widget.hardwareWalletType!, Platform.isIOS) - : [], ), - ), + ); + }, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, ), - ], - ), - bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: PrimaryButton( - key: ValueKey('new_wallet_type_next_button_key'), - onPressed: () => onTypeSelected(), - text: S.of(context).seed_language_next, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isDisabled: selected == null, + ), ), ), - ), + ) ], ), ), ); } - Future onTypeSelected() async { - if (selected == null) throw Exception('Wallet Type is not selected yet.'); - - if (selected == WalletType.haven && widget.isCreate) { - return await showPopUp( - context: context, - builder: (BuildContext context) => PopUpCancellableAlertDialog( - contentText: S.of(context).pause_wallet_creation, - actionButtonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ), - ); - } - + Future onTypeSelected(WalletType selected) async { // If it's a restore flow, trigger the external callback // If it's not a BIP39 Wallet or if there are no other wallets, route to the newWallet page // Any other scenario, route to pre-existing seed page final walletList = await WalletInfo.getAll(); if (!widget.isCreate) { - widget.onTypeSelected!(context, selected!); - } else if (!isBIP39Wallet(selected!) || walletList.isEmpty) { + widget.onTypeSelected!(context, selected); + } else if (!isBIP39Wallet(selected) || walletList.isEmpty) { Navigator.of(context).pushNamed( Routes.newWallet, - arguments: NewWalletArguments(type: selected!), + arguments: NewWalletArguments(type: selected), ); } else { - Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected!); + Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected); } } -} \ No newline at end of file +} diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 7e44e413d..db0d5e883 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -426,7 +426,7 @@ class SendPage extends BasePage { } if (sendViewModel.wallet.isHardwareWallet) { - if (!sendViewModel.hardwareWalletViewModel!.isConnected) { + if (!sendViewModel.hardwareWalletViewModel!.isConnected(sendViewModel.walletType)) { await Navigator.of(context).pushNamed(Routes.connectDevices, arguments: ConnectDevicePageParams( walletType: sendViewModel.walletType, diff --git a/lib/view_model/hardware_wallet/bitbox_view_model.dart b/lib/view_model/hardware_wallet/bitbox_view_model.dart index 07a412cc2..b51815451 100644 --- a/lib/view_model/hardware_wallet/bitbox_view_model.dart +++ b/lib/view_model/hardware_wallet/bitbox_view_model.dart @@ -78,7 +78,7 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { bool _isConnecting = false; @override - bool get isConnected => false; + bool isConnected(WalletType type) => false; @override HardwareWalletService getHardwareWalletService(WalletType type) { diff --git a/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart b/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart index 4c79eda71..de64079b5 100644 --- a/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart +++ b/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart @@ -6,10 +6,11 @@ import 'package:cw_core/wallet_type.dart'; abstract class HardwareWalletViewModel { HardwareWalletType get hardwareWalletType; - bool get isConnected; bool get isBleEnabled; bool get hasBluetooth; + bool isConnected(WalletType type); + Future updateBleState(); Stream scanForBleDevices(); diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 793f0df84..c0746223a 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -114,7 +114,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { if (_isConnecting) return false; _isConnecting = true; _connectingWalletType = type; - if (isConnected) { + if (isConnected(type)) { try { await _connection!.disconnect().catchError((_) {}); } catch (_) {} @@ -126,7 +126,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { if (_connectionChangeSubscription == null) { _connectionChangeSubscription = ledger - .deviceStateChanges + .deviceStateChanges(device.device.id) .listen(_connectionChangeListener); } @@ -172,7 +172,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { } @override - bool get isConnected => _connection != null && !(_connection!.isDisconnected); + bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected); sdk.LedgerConnection get connection => _connection!; @@ -196,6 +196,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { @override HardwareWalletService getHardwareWalletService(WalletType type) { switch (type) { + case WalletType.monero: + return monero!.getLedgerHardwareWalletService(connection); case WalletType.bitcoin: return bitcoin!.getLedgerHardwareWalletService(connection, true); case WalletType.litecoin: diff --git a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart new file mode 100644 index 000000000..3fd4514e6 --- /dev/null +++ b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; +import 'package:cake_wallet/evm/evm.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/hardware/device_connection_type.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:trezor_connect/trezor_connect.dart' as connect_sdk; +import 'package:trezor_flutter/trezor_flutter.dart' as sdk; + +part 'trezor_connect_view_model.g.dart'; + +class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel; + +abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store { + final connect_sdk.TrezorConnect trezorConnect; + + late final sdk.TrezorInterface trezorBLE; + late final sdk.TrezorInterface trezorUSB; + + sdk.ThpState _state = sdk.ThpState(); + + TrezorConnectViewModelBase(this.trezorConnect) { + if (_doesSupportHardwareWallets) { + reaction((_) => isBleEnabled, (_) { + if (isBleEnabled) _initBLE(); + }); + updateBleState(); + + if (!Platform.isIOS) { + trezorUSB = sdk.TrezorInterface.usb(); + } + } + } + + bool get _doesSupportHardwareWallets { + if (isMoneroOnly) { + return DeviceConnectionType.supportedConnectionTypes( + WalletType.monero, HardwareWalletType.trezor, Platform.isIOS) + .isNotEmpty; + } + + return true; + } + + bool _bleIsInitialized = false; + + Future _initBLE() async { + if (isBleEnabled && !_bleIsInitialized) { + trezorBLE = sdk.TrezorInterface.ble( + onPermissionRequest: (_) async { + if (Platform.isMacOS) return true; + + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + ].request(); + + return statuses.values.where((status) => status.isDenied).isEmpty; + }, + bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)), + ); + _bleIsInitialized = true; + } + } + + @override + HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor; + + @override + @observable + bool isBleEnabled = false; + + @override + bool get hasBluetooth => true; + + @override + Future updateBleState() async { + final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState(); + + final newState = bleState == sdk.AvailabilityState.poweredOn; + + if (newState != isBleEnabled) isBleEnabled = newState; + } + + @override + Stream scanForBleDevices() => + trezorBLE.scan().map((d) => TrezorHardwareWalletDevice(d)); + + @override + Future> getAllUsbDevices() => trezorUSB.devices + .then((devices) => devices.map((d) => TrezorHardwareWalletDevice(d)).toList()); + + @override + Future stopScanning() async { + if (_bleIsInitialized) await trezorBLE.stopScanning(); + if (!Platform.isIOS) await trezorUSB.stopScanning(); + } + + bool _isConnecting = false; + sdk.TrezorClient? _client; + + Completer? _pinCompleter; + + @observable + TrezorParingState paringState = TrezorParingState.initial; + + void setParingPin(String pin) => _pinCompleter?.complete(pin); + + @override + Future connectDevice(HardwareWalletDevice device, WalletType type, + [bool isRetry = false]) async { + if (!(device is TrezorHardwareWalletDevice)) return false; + if (_isConnecting) return false; + _isConnecting = true; + paringState = TrezorParingState.initial; + + try { + final trezorInterface = + device.connectionType == HardwareWalletConnectionType.ble ? trezorBLE : trezorUSB; + final connection = await trezorInterface.connect(device.device); + + if (!isRetry) { + showModalBottomSheet( + context: navigatorKey.currentContext!, + isScrollControlled: true, + isDismissible: false, + enableDrag: false, + useSafeArea: true, + builder: (context) => + HardwareWalletProceedOnDeviceSheet( + hardwareWalletType: hardwareWalletType, + trezorConnectVM: this, + onRetry: () => connectDevice(device, type, true) + ), + ); + } + + Future onPinCode() async { + _pinCompleter = Completer(); + paringState = TrezorParingState.enterPin; + + final res = await _pinCompleter!.future; + paringState = TrezorParingState.verifyingPin; + if (res == null) throw Exception(); + return res; + } + + final deviceInfo = await _deviceName; + + _client = sdk.TrezorClient.getClientForConnection( + connection, + _state, + "Cake Wallet", + deviceInfo, + onPinCode, + ); + + await _client!.createChannel(); + + paringState = TrezorParingState.success; + return true; + } catch (e) { + await _client?.connection.disconnect(); + _client = null; + _state = sdk.ThpState(); + // rethrow; + paringState = TrezorParingState.fail(e.toString()); + printV(e); + return false; + } finally { + _isConnecting = false; + } + } + + Future get _deviceName async { + final deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid) return (await deviceInfo.androidInfo).model; + if (Platform.isIOS) return (await deviceInfo.iosInfo).name; + return "Computer"; + } + + @override + bool isConnected(WalletType type) => type == WalletType.monero + ? _client != null && _client?.connection.isDisconnected == false + : true; + + @override + HardwareWalletService getHardwareWalletService(WalletType type) { + switch (type) { + case WalletType.monero: + return monero!.getTrezorHardwareWalletService(_client!); + case WalletType.bitcoin: + return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true); + case WalletType.litecoin: + return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false); + case WalletType.ethereum: + case WalletType.polygon: + return evm!.getTrezorHardwareWalletService(trezorConnect); + default: + throw UnimplementedError(); + } + } + + @override + Future initWallet(WalletBase wallet) async { + switch (wallet.type) { + case WalletType.monero: + return monero!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); + case WalletType.bitcoin: + case WalletType.litecoin: + return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); + case WalletType.ethereum: + case WalletType.polygon: + return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); + default: + throw Exception('Unexpected wallet type: ${wallet.type} for trezor'); + } + } +} + + +abstract class TrezorParingState { + static TrezorParingState initial = InitialTrezorParingState(); + static TrezorParingState enterPin = EnterPinTrezorParingState(); + static TrezorParingState verifyingPin = VerifyingPinTrezorParingState(); + static TrezorParingState success = SuccessTrezorParingState(); + static TrezorParingState fail(String message) => FailTrezorParingState(message); +} + +class InitialTrezorParingState extends TrezorParingState {} + +class EnterPinTrezorParingState extends TrezorParingState {} + +class VerifyingPinTrezorParingState extends TrezorParingState {} + +class SuccessTrezorParingState extends TrezorParingState {} + +class FailTrezorParingState extends TrezorParingState { + final String message; + + FailTrezorParingState(this.message); +} diff --git a/lib/view_model/hardware_wallet/trezor_view_model.dart b/lib/view_model/hardware_wallet/trezor_view_model.dart deleted file mode 100644 index 8a56fb1a4..000000000 --- a/lib/view_model/hardware_wallet/trezor_view_model.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:async'; - -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:trezor_connect/trezor_connect.dart' as sdk; -import 'package:mobx/mobx.dart'; - -part 'trezor_view_model.g.dart'; - -class TrezorViewModel = TrezorViewModelBase with _$TrezorViewModel; - -abstract class TrezorViewModelBase extends HardwareWalletViewModel with Store { - final sdk.TrezorConnect trezorConnect; - - TrezorViewModelBase(this.trezorConnect); - - @override - HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor; - - @override - @observable - bool isBleEnabled = false; - - @override - bool get hasBluetooth => false; - - @override - Future updateBleState() async {} - - @override - Stream scanForBleDevices() => throw UnimplementedError(); - - @override - Future> getAllUsbDevices() async => []; - - @override - Future stopScanning() async {} - - @override - Future connectDevice(HardwareWalletDevice device, WalletType type) async => true; - - @override - bool get isConnected => true; - - @override - HardwareWalletService getHardwareWalletService(WalletType type) { - switch (type) { - case WalletType.bitcoin: - return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true); - case WalletType.litecoin: - return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false); - case WalletType.ethereum: - case WalletType.polygon: - return evm!.getTrezorHardwareWalletService(trezorConnect); - default: - throw UnimplementedError(); - } - } - - @override - Future initWallet(WalletBase wallet) async { - switch (wallet.type) { - case WalletType.bitcoin: - case WalletType.litecoin: - return bitcoin!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); - case WalletType.ethereum: - case WalletType.polygon: - return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); - default: - throw Exception('Unexpected wallet type: ${wallet.type} for trezor'); - } - } -} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 35883f502..a51423ec5 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -619,7 +619,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (!(state is IsExecutingState)) state = IsExecutingState(); if (wallet.isHardwareWallet) { - state = IsAwaitingDeviceResponseState(); if (walletType == WalletType.monero) { _ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) { if (monero!.getLastLedgerCommand() == "INS_CLSAG") { @@ -627,6 +626,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = IsDeviceSigningResponseState(); } }); + } else { + state = IsAwaitingDeviceResponseState(); } } @@ -970,8 +971,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor throw Exception("Pending transaction doesn't exist. It should not be happened."); } + try { - state = TransactionCommitting(); + state = wallet.isHardwareWallet && walletType == WalletType.monero + ? IsAwaitingDeviceResponseState() + : TransactionCommitting(); if (ocpRequest != null) { await _handleOcpRequest(); diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index ac89b2adc..1baabd1b3 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -89,7 +89,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with credentials = monero!.createMoneroRestoreWalletFromHardwareCredentials( name: name, - ledgerConnection: (hardwareWalletVM as LedgerViewModel).connection, + hardwareWalletService: hardwareWalletVM.getHardwareWalletService(type), password: password, height: _options['height'] as int? ?? 0, ); diff --git a/pubspec_base.yaml b/pubspec_base.yaml index a04b093d6..fb35f3582 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -125,7 +125,7 @@ dependencies: ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus - ref: 60817d4b20144f9da9029f5034790272795b9d38 + ref: 0ccfbe26298e8974dfd4e891f34719c8348562f9 trezor_connect: git: url: https://github.com/cake-tech/trezor_connect @@ -203,7 +203,7 @@ dependency_overrides: ledger_flutter_plus: git: url: https://github.com/cake-tech/ledger-flutter-plus - ref: 5237e5b3d5f07696eefd0aaee9c32478ed399d20 + ref: 7bcb00380783f8611f68f5cd63fec96261fea2c2 ledger_usb_plus: git: url: https://github.com/konstantinullrich/ledger-usb-plus @@ -227,6 +227,10 @@ dependency_overrides: decimal: ^2.3.3 flutter_rust_bridge: 2.11.1 flutter_secure_storage_windows: 3.1.2 + universal_ble: + git: + url: https://github.com/cake-tech/universal_ble.git + ref: f7044558ad111a156987614971e08c583c80175c flutter_icons: image_path: "assets/images/app_logo.png" @@ -290,6 +294,7 @@ flutter: - assets/new-ui/card_icons/outline_icons/ - assets/new-ui/card_icons/symbol_icons/ - assets/new-ui/crypto_full_icons/ + - assets/new-ui/hardware_wallets/ - assets/new-ui/node_speed_badges/ fonts: diff --git a/res/pictures/bluetooth.svg b/res/pictures/bluetooth.svg new file mode 100644 index 000000000..ddee93cf7 --- /dev/null +++ b/res/pictures/bluetooth.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/cake_coin.svg b/res/pictures/cake_coin.svg new file mode 100644 index 000000000..03d468a3c --- /dev/null +++ b/res/pictures/cake_coin.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/pictures/hardware_wallet.svg b/res/pictures/hardware_wallet.svg new file mode 100644 index 000000000..56f7c8998 --- /dev/null +++ b/res/pictures/hardware_wallet.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/hardware_wallet/device_bitbox.svg b/res/pictures/hardware_wallets/device_bitbox.svg similarity index 87% rename from assets/images/hardware_wallet/device_bitbox.svg rename to res/pictures/hardware_wallets/device_bitbox.svg index 52368aaec..fe41717d5 100644 --- a/assets/images/hardware_wallet/device_bitbox.svg +++ b/res/pictures/hardware_wallets/device_bitbox.svg @@ -1,3 +1,3 @@ - + diff --git a/res/pictures/hardware_wallets/device_ledger_flex.svg b/res/pictures/hardware_wallets/device_ledger_flex.svg new file mode 100644 index 000000000..9c8e9f7d4 --- /dev/null +++ b/res/pictures/hardware_wallets/device_ledger_flex.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/hardware_wallets/device_ledger_nano_gen_5.svg b/res/pictures/hardware_wallets/device_ledger_nano_gen_5.svg new file mode 100644 index 000000000..fe1124e33 --- /dev/null +++ b/res/pictures/hardware_wallets/device_ledger_nano_gen_5.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/hardware_wallets/device_ledger_nano_s.svg b/res/pictures/hardware_wallets/device_ledger_nano_s.svg new file mode 100644 index 000000000..a8032746a --- /dev/null +++ b/res/pictures/hardware_wallets/device_ledger_nano_s.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/hardware_wallet/device_ledger_nano_x.svg b/res/pictures/hardware_wallets/device_ledger_nano_x.svg similarity index 94% rename from assets/images/hardware_wallet/device_ledger_nano_x.svg rename to res/pictures/hardware_wallets/device_ledger_nano_x.svg index c605249dc..538bb3fa3 100644 --- a/assets/images/hardware_wallet/device_ledger_nano_x.svg +++ b/res/pictures/hardware_wallets/device_ledger_nano_x.svg @@ -1,3 +1,3 @@ - + diff --git a/res/pictures/hardware_wallets/device_ledger_stax.svg b/res/pictures/hardware_wallets/device_ledger_stax.svg new file mode 100644 index 000000000..270457001 --- /dev/null +++ b/res/pictures/hardware_wallets/device_ledger_stax.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/hardware_wallets/device_trezor_model_one.svg b/res/pictures/hardware_wallets/device_trezor_model_one.svg new file mode 100644 index 000000000..5d1c779c5 --- /dev/null +++ b/res/pictures/hardware_wallets/device_trezor_model_one.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/hardware_wallets/device_trezor_model_t.svg b/res/pictures/hardware_wallets/device_trezor_model_t.svg new file mode 100644 index 000000000..fb437c9ea --- /dev/null +++ b/res/pictures/hardware_wallets/device_trezor_model_t.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/hardware_wallets/device_trezor_safe_3.svg b/res/pictures/hardware_wallets/device_trezor_safe_3.svg new file mode 100644 index 000000000..338c7275b --- /dev/null +++ b/res/pictures/hardware_wallets/device_trezor_safe_3.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/hardware_wallets/device_trezor_safe_5.svg b/res/pictures/hardware_wallets/device_trezor_safe_5.svg new file mode 100644 index 000000000..5ee539f39 --- /dev/null +++ b/res/pictures/hardware_wallets/device_trezor_safe_5.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/pictures/hardware_wallets/device_trezor_safe_7.svg b/res/pictures/hardware_wallets/device_trezor_safe_7.svg new file mode 100644 index 000000000..8d442f4b6 --- /dev/null +++ b/res/pictures/hardware_wallets/device_trezor_safe_7.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index f8828872e..f0b425652 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -74,6 +74,7 @@ "approve_tokens": "Approve tokens", "apps": "Apps", "arbiscan_history": "ArbiScan History", + "are_you_sure_exit": "Are you sure you want to exit?", "arrive_in_this_address": "${currency} ${tag}will arrive in this address", "ascending": "Ascending", "ask_each_time": "Ask each time", @@ -330,6 +331,7 @@ "deuro_savings_welcome_description": "dEURO is a fully decentralized, collateralized stablecoin pegged to the Euro — secure, versatile, and censorship-resistant without having to rely on oracles. 1 dEURO = 1 EURO.", "deuro_tooltip_no_eth": "In order to enable dEURO, you must have ETH in your wallet.", "deuro_tx_commited_content": "It might take a couple of seconds for the transaction to confirm and be reflected on screen", + "device_confirmation": "Device Confirmation", "device_is_signing": "Device is signing", "dfx_option_description": "Buy crypto with EUR & CHF. For retail and corporate customers in Europe", "did_you_back_up_seeds": "Did you back up all your seeds?", @@ -529,6 +531,7 @@ "how_to_connect": "How to Connect", "how_to_use": "How to use", "how_to_use_card": "How to use this card", + "hww_exit_desc": "This will ed the ongoing pairing process with your hardware device", "i_understand": "I understand", "icon_style": "Icon style", "id": "ID: ", @@ -722,6 +725,10 @@ "overshot": "Overshot", "overwrite_amount": "Overwrite amount", "pairingInvalidEvent": "Pairing Invalid Event", + "pairing_code": "Pairing Code", + "pairing_code_desc_1": "Your device will show a pairing code.", + "pairing_code_desc_2": "Input here to continue", + "pairing_error": "Pairing Error", "passphrase": "Passphrase (Optional)", "passphrase_view_keys": "Passphrase", "passphrases_doesnt_match": "Passphrases do not match, please try again", @@ -1138,6 +1145,7 @@ "testnet_coins_no_value": "Testnet coins have no value", "third_intro_content": "Yats live outside of Cake Wallet, too. Any wallet address on earth can be replaced with a Yat!", "third_intro_title": "Yat plays nicely with others", + "this_can_take_few_seconds": "This can take a few seconds", "this_pair_is_not_supported_warning": "This pair is not supported with the currently selected exchange(s). Please select another exchange.", "thorchain_contract_address_not_supported": "THORChain does not support sending to a contract address", "thorchain_taproot_address_not_supported": "The THORChain provider does not support Taproot addresses. Please change the address or select a different provider.", @@ -1287,6 +1295,7 @@ "value_type": "Value Type", "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges", "verification": "Verification", + "verifying_code": "Verifying Code", "verify_message": "Verify Message", "verify_seed": "Verify Seed", "verify_with_2fa": "Verify with Cake 2FA", @@ -1394,6 +1403,7 @@ "yat_popup_title": "Your wallet address can be emojified.", "yes": "Yes", "yesterday": "Yesterday", + "yes_exit": "Yes. Exit", "you_now_have_debit_card": "You now have a debit card", "you_pay": "You Pay", "you_will_get": "Convert to", @@ -1408,4 +1418,4 @@ "zcash_card_missing_funds": "Missing funds?", "zcash_card_scan": "Scan", "zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch." -} \ No newline at end of file +} diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 2d7b06f0c..0a328f5f3 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -14,9 +14,9 @@ else fi # NOTE: Make sure to update monero_c prebuilds link in workflow files -# https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip +# https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC2/release-bundle.zip git fetch -a -git checkout bc8d1a0b75b97156d71579581b4cdfe58c777ed2 +git checkout 3bfb3856a838f2bf6b729501837bb0295dedf25d git reset --hard git submodule update --init --force --recursive diff --git a/tool/configure.dart b/tool/configure.dart index 80856320d..6a5385f86 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -334,8 +334,10 @@ import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; +import 'package:trezor_flutter/trezor_flutter.dart' as trezor; import 'package:polyseed/polyseed.dart';"""; const moneroCWHeaders = """ +import 'package:cw_core/hardware/hardware_wallet_service.dart'; import 'package:cw_core/account.dart' as monero_account; import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/monero_amount_format.dart'; @@ -345,6 +347,7 @@ import 'package:cw_monero/api/wallet.dart' as monero_wallet_api; import 'package:cw_monero/ledger.dart'; import 'package:cw_monero/monero_unspent.dart'; import 'package:cw_monero/api/account_list.dart'; +import 'package:cw_monero/trezor.dart'; import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/monero_transaction_info.dart'; @@ -463,7 +466,7 @@ abstract class Monero { HardwareWalletType? hardwareWalletType, required int height}); WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required String mnemonic}); - WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required ledger.LedgerConnection ledgerConnection}); + WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService}); WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required int seedType, required String? passphrase, String? password, String? mnemonic}); Map getKeys(Object wallet); int? getRestoreHeight(Object wallet); @@ -484,6 +487,10 @@ WalletCredentials createMoneroNewWalletCredentials({required String name, requir void resetLedgerConnection(); void setGlobalLedgerConnection(ledger.LedgerConnection connection); String? getLastLedgerCommand(); + void setHardwareWalletService(Object wallet, HardwareWalletService service); + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection); + HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorClient client); + Future syncTrezor(Object wallet); Map> debugCallLength(); Map getWalletCacheDebug(); }