dev
dart 356 lines 10.4 KB
Raw
1 import "dart:async";
2 import "dart:convert";
3 import "dart:io";
4
5 import "package:cake_wallet/bitcoin/bitcoin.dart";
6 import "package:cake_wallet/core/secure_storage.dart";
7 import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
8 import "package:cake_wallet/evm/evm.dart";
9 import "package:cake_wallet/main.dart";
10 import "package:cake_wallet/monero/monero.dart";
11 import "package:cake_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart";
12 import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
13 import "package:cake_wallet/wallet_type_utils.dart";
14 import "package:cw_core/encryption_file_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/key.dart";
18 import "package:cw_core/root_dir.dart";
19 import "package:cw_core/utils/print_verbose.dart";
20 import "package:cw_core/wallet_base.dart";
21 import "package:cw_core/wallet_info.dart";
22 import "package:cw_core/wallet_type.dart";
23 import "package:device_info_plus/device_info_plus.dart";
24 import "package:flutter/material.dart";
25 import "package:mobx/mobx.dart";
26 import "package:permission_handler/permission_handler.dart";
27 import "package:trezor_connect/trezor_connect.dart" as connect_sdk;
28 import "package:trezor_flutter/trezor_flutter.dart" as sdk;
29
30 part "trezor_connect_view_model.g.dart";
31
32 class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel;
33
34 abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store {
35 TrezorConnectViewModelBase(this.trezorConnect, this._secureStorage) {
36 if (_doesSupportHardwareWallets) {
37 reaction((_) => isBleEnabled, (_) {
38 if (isBleEnabled) {
39 _initBLE();
40 }
41 });
42
43 if (!Platform.isIOS) {
44 trezorUSB = sdk.TrezorInterface.usb();
45 }
46 }
47 }
48
49 final connect_sdk.TrezorConnect trezorConnect;
50 final SecureStorage _secureStorage;
51
52 late final sdk.TrezorInterface trezorBLE;
53 late final sdk.TrezorInterface trezorUSB;
54
55 sdk.ThpState? _state;
56
57 bool get _doesSupportHardwareWallets {
58 if (isMoneroOnly) {
59 return DeviceConnectionType.supportedConnectionTypes(
60 WalletType.monero,
61 HardwareWalletType.trezor,
62 Platform.isIOS,
63 ).isNotEmpty;
64 }
65
66 return true;
67 }
68
69 bool _bleIsInitialized = false;
70
71 Future<void> _initBLE() async {
72 if (isBleEnabled && !_bleIsInitialized) {
73 trezorBLE = sdk.TrezorInterface.ble(
74 onPermissionRequest: (_) async {
75 if (Platform.isMacOS) {
76 return true;
77 }
78
79 final Map<Permission, PermissionStatus> statuses = await [
80 Permission.bluetoothScan,
81 Permission.bluetoothConnect,
82 Permission.bluetoothAdvertise,
83 ].request();
84
85 return statuses.values.where((status) => status.isDenied).isEmpty;
86 },
87 bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
88 );
89 _bleIsInitialized = true;
90 }
91 }
92
93 @override
94 HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
95
96 @override
97 @observable
98 bool isConnecting = false;
99
100 @override
101 @observable
102 bool isBleEnabled = false;
103
104 @override
105 bool get hasBluetooth => true;
106
107 @override
108 Future<void> updateBleState() async {
109 final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState();
110
111 final newState = bleState == sdk.AvailabilityState.poweredOn;
112
113 if (newState != isBleEnabled) {
114 isBleEnabled = newState;
115 }
116 }
117
118 @override
119 Stream<HardwareWalletDevice> scanForBleDevices() =>
120 trezorBLE.scan().map(TrezorHardwareWalletDevice.new);
121
122 @override
123 Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
124 trezorUSB.devices.then((devices) => devices.map(TrezorHardwareWalletDevice.new).toList());
125
126 @override
127 Future<void> stopScanning() async {
128 if (_bleIsInitialized) {
129 await trezorBLE.stopScanning();
130 }
131 if (!Platform.isIOS) {
132 await trezorUSB.stopScanning();
133 }
134 }
135
136 sdk.TrezorClient? _client;
137
138 Completer<String?>? _pinCompleter;
139
140 @observable
141 TrezorParingState paringState = TrezorParingState.initial;
142
143 void setParingPin(String pin) => _pinCompleter?.complete(pin);
144
145 @override
146 @action
147 Future<bool> connectDevice(
148 HardwareWalletDevice device,
149 WalletType type, [
150 bool isRetry = false,
151 ]) async {
152 if (device is! TrezorHardwareWalletDevice) {
153 return false;
154 }
155 if (isConnecting) {
156 return false;
157 }
158 isConnecting = true;
159 paringState = TrezorParingState.initial;
160
161 try {
162 final trezorInterface =
163 device.connectionType == HardwareWalletConnectionType.ble ? trezorBLE : trezorUSB;
164 final connection = await trezorInterface.connect(device.device);
165
166 if (!isRetry) {
167 unawaited(
168 showModalBottomSheet(
169 context: navigatorKey.currentContext!,
170 isScrollControlled: true,
171 isDismissible: false,
172 enableDrag: false,
173 useSafeArea: true,
174 builder: (_) => HardwareWalletProceedOnDeviceSheet(
175 hardwareWalletType: hardwareWalletType,
176 trezorConnectVM: this,
177 onRetry: () => connectDevice(device, type, true),
178 ),
179 ),
180 );
181 }
182
183 Future<String> onPinCode() async {
184 _pinCompleter = Completer<String?>();
185 paringState = TrezorParingState.enterPin;
186
187 final res = await _pinCompleter!.future;
188 paringState = TrezorParingState.verifyingPin;
189 if (res == null) {
190 throw Exception();
191 }
192 return res;
193 }
194
195 final deviceInfo = await _deviceName;
196
197 _state ??= await _getState();
198 _client = sdk.TrezorClient.getClientForConnection(
199 connection,
200 _state!,
201 "Cake Wallet",
202 deviceInfo,
203 onPinCode,
204 );
205
206 await _client!.createChannel();
207
208 if (!_state!.pairingCredentials.any((c) => c.autoconnect == true)) {
209 if (_client case final sdk.TrezorClientV2 clientV2) {
210 try {
211 final auto = await clientV2.getAutoPairingCredentials();
212 _state!.setPairingCredentials([auto]);
213 await _saveState();
214 } catch (_) {}
215 }
216 }
217
218 paringState = TrezorParingState.success;
219 return true;
220 } catch (e) {
221 await _client?.connection.disconnect();
222 _client = null;
223 _state = sdk.ThpState();
224 // rethrow;
225 paringState = TrezorParingState.fail(e.toString());
226 printV(e);
227 return false;
228 } finally {
229 isConnecting = false;
230 }
231 }
232
233 Future<String> get _deviceName async {
234 final deviceInfo = DeviceInfoPlugin();
235 if (Platform.isAndroid) {
236 return (await deviceInfo.androidInfo).model;
237 }
238 if (Platform.isIOS) {
239 return (await deviceInfo.iosInfo).name;
240 }
241 return "Computer";
242 }
243
244 @override
245 bool isConnected(WalletType type) => type == WalletType.monero
246 ? _client != null && _client?.connection.isDisconnected == false
247 : true;
248
249 @override
250 HardwareWalletService getHardwareWalletService(WalletType type) {
251 switch (type) {
252 case WalletType.monero:
253 return monero!.getTrezorHardwareWalletService(_client!);
254 case WalletType.bitcoin:
255 return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
256 case WalletType.litecoin:
257 return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
258 case WalletType.ethereum:
259 case WalletType.polygon:
260 return evm!.getTrezorHardwareWalletService(trezorConnect);
261 default:
262 throw UnimplementedError();
263 }
264 }
265
266 @override
267 Future<void> initWallet(WalletBase wallet) async {
268 switch (wallet.type) {
269 case WalletType.monero:
270 return monero!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
271 case WalletType.bitcoin:
272 case WalletType.litecoin:
273 return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
274 case WalletType.ethereum:
275 case WalletType.polygon:
276 return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
277 default:
278 throw Exception("Unexpected wallet type: ${wallet.type} for trezor");
279 }
280 }
281
282 final EncryptionFileUtils _encryptionFileUtils = encryptionFileUtilsFor(true);
283 static const String _secureStorageKey = "com.cakewallet.trezor/thp_state";
284
285 Future<String> get _thpJsonFile async => "${(await getAppDir()).path}/thp_state.json.enc";
286
287 Future<sdk.ThpState> _getState() async {
288 final password = await _secureStorage.read(key: _secureStorageKey);
289 if (password == null) {
290 return _saveState();
291 }
292 try {
293 final state = await _encryptionFileUtils.read(path: await _thpJsonFile, password: password);
294 return sdk.ThpState.fromJson(state);
295 } catch (_) {
296 return _saveState();
297 }
298 }
299
300 Future<sdk.ThpState> _saveState() async {
301 try {
302 final password = generateKey();
303 await _secureStorage.write(key: _secureStorageKey, value: password);
304
305 final file = File(await _thpJsonFile);
306 if (!file.existsSync()) {
307 file.createSync();
308 }
309
310 final state = _state ?? sdk.ThpState();
311 await _encryptionFileUtils.write(
312 path: file.path,
313 password: password,
314 data: jsonEncode(state.toMap()),
315 );
316 _state = state;
317 return state;
318 } catch (_) {
319 throw Exception("Unable to save Trezor State");
320 }
321 }
322
323 Future<bool> syncKeyImages(WalletBase wallet) async {
324 if (wallet.type == WalletType.monero) {
325 try {
326 await monero!.syncTrezor(wallet);
327 } catch (_) {
328 return false;
329 }
330 }
331 return true;
332 }
333 }
334
335 abstract class TrezorParingState {
336 static TrezorParingState initial = InitialTrezorParingState();
337 static TrezorParingState enterPin = EnterPinTrezorParingState();
338 static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
339 static TrezorParingState success = SuccessTrezorParingState();
340
341 static TrezorParingState fail(String message) => FailTrezorParingState(message);
342 }
343
344 class InitialTrezorParingState extends TrezorParingState {}
345
346 class EnterPinTrezorParingState extends TrezorParingState {}
347
348 class VerifyingPinTrezorParingState extends TrezorParingState {}
349
350 class SuccessTrezorParingState extends TrezorParingState {}
351
352 class FailTrezorParingState extends TrezorParingState {
353 FailTrezorParingState(this.message);
354
355 final String message;
356 }