CW-1581-trezor-fixes-enhancements (#3462)

* feat: add Trezor auto connect

* chore: update `trezor_flutter`

* fix: correct method name for retrieving Trezor auto-pairing credentials

* fix: update device connection prompt text across all languages [skip ci]

* feat: add Trezor key image synchronization ui and improve hardware wallet UX

* feat: add sync-balance icon asset for settings row visuals [skip ci]

* feat: add sync-balance icon asset for settings row visuals [skip ci]

* fix: remove unused Monero and sync key images settings actions from menus [skip ci]

* fix: re-enable routing for UR QR Animated Page [skip ci]
This commit is contained in:
Konstantin Ullrich
2026-08-06 14:49:18 +02:00
committed by GitHub
parent 76b04580f3
commit feeec2e425
46 changed files with 854 additions and 188 deletions
@@ -1,43 +1,43 @@
import 'dart:async';
import 'dart:io';
import "dart:async";
import "dart:convert";
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;
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/core/secure_storage.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/encryption_file_utils.dart";
import "package:cw_core/hardware/device_connection_type.dart";
import "package:cw_core/hardware/hardware_wallet_service.dart";
import "package:cw_core/key.dart";
import "package:cw_core/root_dir.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';
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) {
TrezorConnectViewModelBase(this.trezorConnect, this._secureStorage) {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) _initBLE();
if (isBleEnabled) {
_initBLE();
}
});
updateBleState();
@@ -47,11 +47,21 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
}
}
final connect_sdk.TrezorConnect trezorConnect;
final SecureStorage _secureStorage;
late final sdk.TrezorInterface trezorBLE;
late final sdk.TrezorInterface trezorUSB;
sdk.ThpState? _state;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero, HardwareWalletType.trezor, Platform.isIOS)
.isNotEmpty;
WalletType.monero,
HardwareWalletType.trezor,
Platform.isIOS,
).isNotEmpty;
}
return true;
@@ -63,9 +73,11 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
if (isBleEnabled && !_bleIsInitialized) {
trezorBLE = sdk.TrezorInterface.ble(
onPermissionRequest: (_) async {
if (Platform.isMacOS) return true;
if (Platform.isMacOS) {
return true;
}
Map<Permission, PermissionStatus> statuses = await [
final Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
@@ -73,7 +85,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
return statuses.values.where((status) => status.isDenied).isEmpty;
},
bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
);
_bleIsInitialized = true;
}
@@ -99,21 +111,27 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
final newState = bleState == sdk.AvailabilityState.poweredOn;
if (newState != isBleEnabled) isBleEnabled = newState;
if (newState != isBleEnabled) {
isBleEnabled = newState;
}
}
@override
Stream<HardwareWalletDevice> scanForBleDevices() =>
trezorBLE.scan().map((d) => TrezorHardwareWalletDevice(d));
trezorBLE.scan().map(TrezorHardwareWalletDevice.new);
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => trezorUSB.devices
.then((devices) => devices.map((d) => TrezorHardwareWalletDevice(d)).toList());
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
trezorUSB.devices.then((devices) => devices.map(TrezorHardwareWalletDevice.new).toList());
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) await trezorBLE.stopScanning();
if (!Platform.isIOS) await trezorUSB.stopScanning();
if (_bleIsInitialized) {
await trezorBLE.stopScanning();
}
if (!Platform.isIOS) {
await trezorUSB.stopScanning();
}
}
sdk.TrezorClient? _client;
@@ -127,10 +145,17 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type,
[bool isRetry = false]) async {
if (!(device is TrezorHardwareWalletDevice)) return false;
if (isConnecting) return false;
Future<bool> connectDevice(
HardwareWalletDevice device,
WalletType type, [
bool isRetry = false,
]) async {
if (device is! TrezorHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
paringState = TrezorParingState.initial;
@@ -140,16 +165,19 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
final connection = await trezorInterface.connect(device.device);
if (!isRetry) {
showModalBottomSheet(
context: navigatorKey.currentContext!,
isScrollControlled: true,
isDismissible: false,
enableDrag: false,
useSafeArea: true,
builder: (context) => HardwareWalletProceedOnDeviceSheet(
unawaited(
showModalBottomSheet(
context: navigatorKey.currentContext!,
isScrollControlled: true,
isDismissible: false,
enableDrag: false,
useSafeArea: true,
builder: (_) => HardwareWalletProceedOnDeviceSheet(
hardwareWalletType: hardwareWalletType,
trezorConnectVM: this,
onRetry: () => connectDevice(device, type, true)),
onRetry: () => connectDevice(device, type, true),
),
),
);
}
@@ -159,15 +187,18 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
final res = await _pinCompleter!.future;
paringState = TrezorParingState.verifyingPin;
if (res == null) throw Exception();
if (res == null) {
throw Exception();
}
return res;
}
final deviceInfo = await _deviceName;
_state ??= await _getState();
_client = sdk.TrezorClient.getClientForConnection(
connection,
_state,
_state!,
"Cake Wallet",
deviceInfo,
onPinCode,
@@ -175,6 +206,16 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
await _client!.createChannel();
if (!_state!.pairingCredentials.any((c) => c.autoconnect == true)) {
if (_client case final sdk.TrezorClientV2 clientV2) {
try {
final auto = await clientV2.getAutoPairingCredentials();
_state!.setPairingCredentials([auto]);
await _saveState();
} catch (_) {}
}
}
paringState = TrezorParingState.success;
return true;
} catch (e) {
@@ -192,8 +233,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
Future<String> get _deviceName async {
final deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) return (await deviceInfo.androidInfo).model;
if (Platform.isIOS) return (await deviceInfo.iosInfo).name;
if (Platform.isAndroid) {
return (await deviceInfo.androidInfo).model;
}
if (Platform.isIOS) {
return (await deviceInfo.iosInfo).name;
}
return "Computer";
}
@@ -231,9 +276,61 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
throw Exception("Unexpected wallet type: ${wallet.type} for trezor");
}
}
final EncryptionFileUtils _encryptionFileUtils = encryptionFileUtilsFor(true);
static const String _secureStorageKey = "com.cakewallet.trezor/thp_state";
Future<String> get _thpJsonFile async => "${(await getAppDir()).path}/thp_state.json.enc";
Future<sdk.ThpState> _getState() async {
final password = await _secureStorage.read(key: _secureStorageKey);
if (password == null) {
return _saveState();
}
try {
final state = await _encryptionFileUtils.read(path: await _thpJsonFile, password: password);
return sdk.ThpState.fromJson(state);
} catch (_) {
return _saveState();
}
}
Future<sdk.ThpState> _saveState() async {
try {
final password = generateKey();
await _secureStorage.write(key: _secureStorageKey, value: password);
final file = File(await _thpJsonFile);
if (!file.existsSync()) {
file.createSync();
}
final state = _state ?? sdk.ThpState();
await _encryptionFileUtils.write(
path: file.path,
password: password,
data: jsonEncode(state.toMap()),
);
_state = state;
return state;
} catch (_) {
throw Exception("Unable to save Trezor State");
}
}
Future<bool> syncKeyImages(WalletBase wallet) async {
if (wallet.type == WalletType.monero) {
try {
await monero!.syncTrezor(wallet);
} catch (_) {
return false;
}
}
return true;
}
}
abstract class TrezorParingState {
@@ -241,6 +338,7 @@ abstract class TrezorParingState {
static TrezorParingState enterPin = EnterPinTrezorParingState();
static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
static TrezorParingState success = SuccessTrezorParingState();
static TrezorParingState fail(String message) => FailTrezorParingState(message);
}
@@ -253,7 +351,7 @@ class VerifyingPinTrezorParingState extends TrezorParingState {}
class SuccessTrezorParingState extends TrezorParingState {}
class FailTrezorParingState extends TrezorParingState {
final String message;
FailTrezorParingState(this.message);
final String message;
}