CW-1291-ledger-fixes (#3390)

* feat: add connecting indicator in hardware wallet connect screen

* chore: bump ledger related dependencies

* chore: bump ledger related dependencies

* feat: add additional guard against gLedger being null

* auto-reformat

* feat: improve ledger connection and state management

* fix: connect_device_page padding on iOS

* chore: update universal_ble dependency reference

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Konstantin Ullrich
2026-07-16 21:39:49 +03:00
committed by GitHub
co-authored by Robert Malikowski Omar Hatem
parent 0822c7d976
commit ed540bae04
12 changed files with 417 additions and 282 deletions
@@ -35,6 +35,10 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
@observable
bool isBleEnabled = false;
@override
@observable
bool isConnecting = false;
@override
bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth
@@ -52,10 +56,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
Future<void> stopScanning() async {}
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
if (!(device is BitboxHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (isConnecting) return false;
isConnecting = true;
try {
final appDocDir = await getAppDir();
@@ -66,17 +71,15 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt');
await bitboxManager.channelHashVerify();
printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt');
_isConnecting = false;
isConnecting = false;
return true;
} catch (e) {
printV(e);
}
_isConnecting = false;
isConnecting = false;
return false;
}
bool _isConnecting = false;
@override
bool isConnected(WalletType type) => false;
@@ -1,13 +1,14 @@
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.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:cake_wallet/entities/hardware_wallet/hardware_wallet_device.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";
abstract class HardwareWalletViewModel {
HardwareWalletType get hardwareWalletType;
bool get isBleEnabled;
bool get hasBluetooth;
bool get isConnecting;
bool isConnected(WalletType type);
@@ -26,4 +27,6 @@ abstract class HardwareWalletViewModel {
Future<void> initWallet(WalletBase wallet);
String? interpretErrorCode(String error) => null;
Future<void> close() async {}
}
@@ -1,50 +1,47 @@
import 'dart:async';
import 'dart:io';
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/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.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:flutter/widgets.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk;
import 'package:mobx/mobx.dart';
import 'package:permission_handler/permission_handler.dart';
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/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import "package:cake_wallet/monero/monero.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
import "package:cake_wallet/store/app_store.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:flutter/widgets.dart";
import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk;
import "package:mobx/mobx.dart";
import "package:permission_handler/permission_handler.dart";
part 'ledger_view_model.g.dart';
part "ledger_view_model.g.dart";
class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel;
abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
late final sdk.LedgerInterface ledgerPlusBLE;
late final sdk.LedgerInterface ledgerPlusUSB;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero, HardwareWalletType.ledger, Platform.isIOS)
.isNotEmpty;
}
return true;
}
LedgerViewModelBase() {
LedgerViewModelBase(AppStore appStore) {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) _initBLE();
if (isBleEnabled) {
_initBLE();
}
});
reaction((_) => appStore.wallet, (wallet) async {
if (wallet != null && wallet.type != _connectingWalletType) {
await close();
}
});
updateBleState();
if (!Platform.isIOS) {
@@ -53,6 +50,21 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
}
late final sdk.LedgerInterface ledgerPlusBLE;
late final sdk.LedgerInterface ledgerPlusUSB;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero,
HardwareWalletType.ledger,
Platform.isIOS,
).isNotEmpty;
}
return true;
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger;
@@ -60,6 +72,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
@observable
bool isBleEnabled = false;
@override
@observable
bool isConnecting = false;
@override
bool get hasBluetooth => true;
@@ -69,9 +85,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
if (isBleEnabled && !_bleIsInitialized) {
ledgerPlusBLE = sdk.LedgerInterface.ble(
onPermissionRequest: (_) async {
if (Platform.isMacOS) return true;
if (Platform.isMacOS) {
return true;
}
Map<Permission, PermissionStatus> statuses = await [
final statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
@@ -79,7 +97,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
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;
}
@@ -91,28 +109,41 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
final newState = bleState == sdk.AvailabilityState.poweredOn;
if (newState != isBleEnabled) isBleEnabled = newState;
if (newState != isBleEnabled) {
isBleEnabled = newState;
}
}
@override
Stream<HardwareWalletDevice> scanForBleDevices() =>
ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d));
ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new);
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => ledgerPlusUSB.devices
.then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList());
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList());
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) await ledgerPlusBLE.stopScanning();
if (!Platform.isIOS) await ledgerPlusUSB.stopScanning();
if (_bleIsInitialized) {
await ledgerPlusBLE.stopScanning();
}
if (!Platform.isIOS) {
await ledgerPlusUSB.stopScanning();
}
}
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
if (!(device is LedgerHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (device is! LedgerHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
_connectingWalletType = type;
if (isConnected(type)) {
try {
@@ -123,34 +154,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
final ledger =
device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB;
if (_connectionChangeSubscription == null) {
_connectionChangeSubscription =
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
}
_connectionChangeSubscription ??=
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
try {
_connection = await ledger.connect(device.device);
_isConnecting = false;
isConnecting = false;
return true;
} catch (e) {
printV(e);
}
_isConnecting = false;
isConnecting = false;
return false;
}
StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
sdk.LedgerConnection? _connection;
bool _isConnecting = false;
WalletType? _connectingWalletType;
bool suppressMoneroReconnectPrompt = false;
void _connectionChangeListener(sdk.BleConnectionState event) {
printV('Ledger Device State Changed: $event');
if (event == sdk.BleConnectionState.disconnected && !_isConnecting) {
printV("Ledger Device State Changed: $event");
if (event == sdk.BleConnectionState.disconnected && !isConnecting) {
_connection = null;
if (_connectingWalletType == WalletType.monero) {
monero!.resetLedgerConnection();
if (suppressMoneroReconnectPrompt) {
return;
}
Navigator.of(navigatorKey.currentContext!).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
@@ -158,7 +191,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
hardwareWalletType: HardwareWalletType.ledger,
allowChangeWallet: true,
isReconnect: true,
onConnectDevice: (context, ledgerVM) async {
onConnectDevice: (context, ledgerVM) {
if (context.mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
@@ -170,26 +203,24 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
@override
bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected);
bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected;
sdk.LedgerConnection get connection => _connection!;
@override
Future<void> initWallet(WalletBase wallet) async {
Future<void> initWallet(WalletBase wallet) {
switch (wallet.type) {
case WalletType.monero:
return monero!.setLedgerConnection(wallet, connection);
case WalletType.bitcoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.litecoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for ledger');
throw Exception("Unexpected wallet type: ${wallet.type} for ledger");
}
}
@@ -216,16 +247,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
return error;
}
var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString());
final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString());
String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
if (errorCode.contains("6985")) {
return S.current.ledger_error_tx_rejected_by_user;
} else if (errorCode.contains("5515")) {
return S.current.ledger_error_device_locked;
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) {
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) {
return S.current.ledger_error_wrong_app;
}
return null;
}
@override
Future<void> close() async {
try {
printV("Resetting ledger VM");
await _connectionChangeSubscription?.cancel();
await _connection?.disconnect().catchError((_) {});
_connectingWalletType = null;
_connectionChangeSubscription = null;
_connection = null;
isConnecting = false;
await stopScanning();
} catch (_) {}
}
}
@@ -82,6 +82,10 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
@override
@observable
bool isConnecting = false;
@override
@observable
bool isBleEnabled = false;
@@ -112,7 +116,6 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
if (!Platform.isIOS) await trezorUSB.stopScanning();
}
bool _isConnecting = false;
sdk.TrezorClient? _client;
Completer<String?>? _pinCompleter;
@@ -123,11 +126,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
void setParingPin(String pin) => _pinCompleter?.complete(pin);
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type,
[bool isRetry = false]) async {
if (!(device is TrezorHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (isConnecting) return false;
isConnecting = true;
paringState = TrezorParingState.initial;
try {
@@ -182,7 +186,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
printV(e);
return false;
} finally {
_isConnecting = false;
isConnecting = false;
}
}
@@ -77,8 +77,7 @@ abstract class WalletListViewModelBase with Store {
if (walletItem.type == WalletType.haven) {
return;
}
// bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type;
// await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType);
final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name);
await _appStore.changeCurrentWallet(wallet);
updateList();