dev
dart 275 lines 8.25 KB
Raw
1 import "dart:async";
2 import "dart:io";
3
4 import "package:cake_wallet/bitcoin/bitcoin.dart";
5 import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
6 import "package:cake_wallet/evm/evm.dart";
7 import "package:cake_wallet/generated/i18n.dart";
8 import "package:cake_wallet/main.dart";
9 import "package:cake_wallet/monero/monero.dart";
10 import "package:cake_wallet/routes.dart";
11 import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
12 import "package:cake_wallet/store/app_store.dart";
13 import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
14 import "package:cake_wallet/wallet_type_utils.dart";
15 import "package:cw_core/hardware/device_connection_type.dart";
16 import "package:cw_core/hardware/hardware_wallet_service.dart";
17 import "package:cw_core/utils/print_verbose.dart";
18 import "package:cw_core/wallet_base.dart";
19 import "package:cw_core/wallet_info.dart";
20 import "package:cw_core/wallet_type.dart";
21 import "package:flutter/widgets.dart";
22 import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk;
23 import "package:mobx/mobx.dart";
24 import "package:permission_handler/permission_handler.dart";
25
26 part "ledger_view_model.g.dart";
27
28 class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel;
29
30 abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
31 LedgerViewModelBase(AppStore appStore) {
32 if (_doesSupportHardwareWallets) {
33 reaction((_) => isBleEnabled, (_) {
34 if (isBleEnabled) {
35 _initBLE();
36 }
37 });
38
39 reaction((_) => appStore.wallet, (wallet) async {
40 if (wallet != null && wallet.type != _connectingWalletType) {
41 await close();
42 }
43 });
44
45 if (!Platform.isIOS) {
46 ledgerPlusUSB = sdk.LedgerInterface.usb();
47 }
48 }
49 }
50
51 late final sdk.LedgerInterface ledgerPlusBLE;
52 late final sdk.LedgerInterface ledgerPlusUSB;
53
54 bool get _doesSupportHardwareWallets {
55 if (isMoneroOnly) {
56 return DeviceConnectionType.supportedConnectionTypes(
57 WalletType.monero,
58 HardwareWalletType.ledger,
59 Platform.isIOS,
60 ).isNotEmpty;
61 }
62
63 return true;
64 }
65
66 @override
67 HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger;
68
69 @override
70 @observable
71 bool isBleEnabled = false;
72
73 @override
74 @observable
75 bool isConnecting = false;
76
77 @override
78 bool get hasBluetooth => true;
79
80 bool _bleIsInitialized = false;
81
82 Future<void> _initBLE() async {
83 if (isBleEnabled && !_bleIsInitialized) {
84 ledgerPlusBLE = sdk.LedgerInterface.ble(
85 onPermissionRequest: (_) async {
86 if (Platform.isMacOS) {
87 return true;
88 }
89
90 final statuses = await [
91 Permission.bluetoothScan,
92 Permission.bluetoothConnect,
93 Permission.bluetoothAdvertise,
94 ].request();
95
96 return statuses.values.where((status) => status.isDenied).isEmpty;
97 },
98 bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
99 );
100 _bleIsInitialized = true;
101 }
102 }
103
104 @override
105 Future<void> updateBleState() async {
106 final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState();
107
108 final newState = bleState == sdk.AvailabilityState.poweredOn;
109
110 if (newState != isBleEnabled) {
111 isBleEnabled = newState;
112 }
113 }
114
115 @override
116 Stream<HardwareWalletDevice> scanForBleDevices() =>
117 ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new);
118
119 @override
120 Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
121 ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList());
122
123 @override
124 Future<void> stopScanning() async {
125 if (_bleIsInitialized) {
126 await ledgerPlusBLE.stopScanning();
127 }
128 if (!Platform.isIOS) {
129 await ledgerPlusUSB.stopScanning();
130 }
131 }
132
133 @override
134 @action
135 Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
136 if (device is! LedgerHardwareWalletDevice) {
137 return false;
138 }
139
140 if (isConnecting) {
141 return false;
142 }
143
144 isConnecting = true;
145 _connectingWalletType = type;
146 if (isConnected(type)) {
147 try {
148 await _connection!.disconnect().catchError((_) {});
149 } catch (_) {}
150 }
151
152 final ledger =
153 device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB;
154
155 _connectionChangeSubscription ??=
156 ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
157
158 try {
159 _connection = await ledger.connect(device.device);
160 isConnecting = false;
161 return true;
162 } catch (e) {
163 printV(e);
164 }
165 isConnecting = false;
166 return false;
167 }
168
169 StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
170 sdk.LedgerConnection? _connection;
171 WalletType? _connectingWalletType;
172 bool suppressMoneroReconnectPrompt = false;
173
174 void _connectionChangeListener(sdk.BleConnectionState event) {
175 printV("Ledger Device State Changed: $event");
176 if (event == sdk.BleConnectionState.disconnected && !isConnecting) {
177 _connection = null;
178 if (_connectingWalletType == WalletType.monero) {
179 monero!.resetLedgerConnection();
180
181 if (suppressMoneroReconnectPrompt) {
182 return;
183 }
184
185 Navigator.of(navigatorKey.currentContext!).pushNamed(
186 Routes.connectDevices,
187 arguments: ConnectDevicePageParams(
188 walletType: WalletType.monero,
189 hardwareWalletType: HardwareWalletType.ledger,
190 allowChangeWallet: true,
191 isReconnect: true,
192 onConnectDevice: (context, ledgerVM) {
193 if (context.mounted && Navigator.of(context).canPop()) {
194 Navigator.of(context).pop();
195 }
196 },
197 ),
198 );
199 }
200 }
201 }
202
203 @override
204 bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected;
205
206 sdk.LedgerConnection get connection => _connection!;
207
208 @override
209 Future<void> initWallet(WalletBase wallet) {
210 switch (wallet.type) {
211 case WalletType.monero:
212 return monero!.setLedgerConnection(wallet, connection);
213 case WalletType.bitcoin:
214 return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
215 case WalletType.litecoin:
216 return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
217 case WalletType.ethereum:
218 case WalletType.polygon:
219 return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
220 default:
221 throw Exception("Unexpected wallet type: ${wallet.type} for ledger");
222 }
223 }
224
225 @override
226 HardwareWalletService getHardwareWalletService(WalletType type) {
227 switch (type) {
228 case WalletType.monero:
229 return monero!.getLedgerHardwareWalletService(connection);
230 case WalletType.bitcoin:
231 return bitcoin!.getLedgerHardwareWalletService(connection, true);
232 case WalletType.litecoin:
233 return bitcoin!.getLedgerHardwareWalletService(connection, false);
234 case WalletType.ethereum:
235 case WalletType.polygon:
236 return evm!.getLedgerHardwareWalletService(connection);
237 default:
238 throw UnimplementedError();
239 }
240 }
241
242 @override
243 String? interpretErrorCode(String error) {
244 if (error.contains("Make sure no other program is communicating with the Ledger")) {
245 return error;
246 }
247
248 final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString());
249
250 final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
251 if (errorCode.contains("6985")) {
252 return S.current.ledger_error_tx_rejected_by_user;
253 } else if (errorCode.contains("5515")) {
254 return S.current.ledger_error_device_locked;
255 } else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) {
256 return S.current.ledger_error_wrong_app;
257 }
258 return null;
259 }
260
261 @override
262 Future<void> close() async {
263 try {
264 printV("Resetting ledger VM");
265 await _connectionChangeSubscription?.cancel();
266 await _connection?.disconnect().catchError((_) {});
267
268 _connectingWalletType = null;
269 _connectionChangeSubscription = null;
270 _connection = null;
271 isConnecting = false;
272 await stopScanning();
273 } catch (_) {}
274 }
275 }