dev
dart 602 lines 19.3 KB
Raw
1 import 'dart:async';
2 import 'dart:ffi';
3 import 'dart:io';
4 import 'dart:isolate';
5
6 import 'package:cw_core/get_height_by_date.dart';
7 import 'package:cw_core/hardware/hardware_wallet_service.dart';
8 import 'package:cw_core/monero_wallet_utils.dart';
9 import 'package:cw_core/pathForWallet.dart';
10 import 'package:cw_core/unspent_coins_info.dart';
11 import 'package:cw_core/utils/print_verbose.dart';
12 import 'package:cw_core/wallet_base.dart';
13 import 'package:cw_core/wallet_credentials.dart';
14 import 'package:cw_core/wallet_info.dart';
15 import 'package:cw_core/wallet_service.dart';
16 import 'package:cw_core/wallet_type.dart';
17 import 'package:cw_monero/api/account_list.dart';
18 import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
19 import 'package:cw_monero/api/wallet_manager.dart';
20 import 'package:cw_monero/bip39_seed.dart';
21 import 'package:cw_monero/ledger.dart';
22 import 'package:cw_monero/monero_wallet.dart';
23 import 'package:cw_monero/trezor.dart';
24 import 'package:hive/hive.dart';
25 import 'package:monero/monero.dart' as monero;
26 import 'package:polyseed/polyseed.dart';
27
28 enum MoneroSeedType { polyseed, legacy, bip39 }
29
30 class MoneroNewWalletCredentials extends WalletCredentials {
31 MoneroNewWalletCredentials(
32 {required String name,
33 required this.language,
34 required this.seedType,
35 String? password,
36 this.passphrase,
37 this.mnemonic})
38 : super(name: name, password: password);
39
40 final String language;
41 final MoneroSeedType seedType;
42 final String? passphrase;
43 final String? mnemonic;
44 }
45
46 class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
47 MoneroRestoreWalletFromHardwareCredentials({
48 required String name,
49 required this.hardwareWalletService,
50 int height = 0,
51 String? password,
52 String? passphrase,
53 }) : super(name: name, password: password, height: height, passphrase: passphrase);
54
55 final HardwareWalletService hardwareWalletService;
56 }
57
58 class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
59 MoneroRestoreWalletFromSeedCredentials(
60 {required String name,
61 required this.mnemonic,
62 required this.passphrase,
63 int height = 0,
64 String? password})
65 : super(name: name, password: password, height: height);
66
67 final String mnemonic;
68 final String passphrase;
69 }
70
71 class MoneroWalletLoadingException implements Exception {
72 @override
73 String toString() => 'Failure to load the wallet.';
74 }
75
76 class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
77 MoneroRestoreWalletFromKeysCredentials(
78 {required String name,
79 required String password,
80 required this.language,
81 required this.address,
82 required this.viewKey,
83 required this.spendKey,
84 super.hardwareWalletType,
85 int height = 0})
86 : super(name: name, password: password, height: height);
87
88 final String language;
89 final String address;
90 final String viewKey;
91 final String spendKey;
92 }
93
94 enum OpenWalletTry {
95 initial,
96 cacheRestored,
97 cacheRemoved,
98 }
99
100 class MoneroWalletService extends WalletService<
101 MoneroNewWalletCredentials,
102 MoneroRestoreWalletFromSeedCredentials,
103 MoneroRestoreWalletFromKeysCredentials,
104 MoneroRestoreWalletFromHardwareCredentials> {
105 MoneroWalletService(this.unspentCoinsInfoSource);
106
107 final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
108
109 static bool walletFilesExist(String path) =>
110 !File(path).existsSync() && !File('$path.keys').existsSync();
111
112 @override
113 WalletType getType() => WalletType.monero;
114
115 @override
116 Future<MoneroWallet> create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async {
117 try {
118 final path = await pathForWallet(name: credentials.name, type: getType());
119
120 if (credentials.seedType == MoneroSeedType.bip39) {
121 return _restoreFromBip39(
122 path: path,
123 password: credentials.password!,
124 mnemonic: credentials.mnemonic ?? getBip39Seed(),
125 passphrase: credentials.passphrase,
126 walletInfo: credentials.walletInfo!,
127 );
128 }
129
130 if (credentials.seedType == MoneroSeedType.polyseed) {
131 final polyseed = Polyseed.create();
132 final lang = PolyseedLang.getByEnglishName(credentials.language);
133
134 if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!);
135
136 final heightOverride =
137 getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2)));
138
139 return _restoreFromPolyseed(
140 path, credentials.password!, polyseed, credentials.walletInfo!, lang,
141 overrideHeight: heightOverride, passphrase: credentials.passphrase);
142 }
143
144 monero_wallet_manager.createWallet(
145 path: path,
146 password: credentials.password!,
147 language: credentials.language,
148 passphrase: credentials.passphrase ?? "");
149 final wallet = MoneroWallet(
150 walletInfo: credentials.walletInfo!,
151 derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
152 unspentCoinsInfo: unspentCoinsInfoSource,
153 password: credentials.password!);
154 await wallet.init();
155
156 return wallet;
157 } catch (e) {
158 // TODO: Implement Exception for wallet list service.
159 printV('MoneroWalletsManager Error: ${e.toString()}');
160 rethrow;
161 }
162 }
163
164 @override
165 Future<bool> isWalletExit(String name) async {
166 try {
167 final path = await pathForWallet(name: name, type: getType());
168 return monero_wallet_manager.isWalletExist(path: path);
169 } catch (e) {
170 // TODO: Implement Exception for wallet list service.
171 printV('MoneroWalletsManager Error: $e');
172 rethrow;
173 }
174 }
175
176 @override
177 Future<MoneroWallet> openWallet(String name, String password,
178 {OpenWalletTry openWalletTry = OpenWalletTry.initial}) async {
179 try {
180 final path = await pathForWallet(name: name, type: getType());
181
182 if (walletFilesExist(path)) await repairOldAndroidWallet(name);
183
184 await monero_wallet_manager.openWallet(path: path, password: password);
185 final walletInfo = await WalletInfo.get(name, getType());
186 if (walletInfo == null) {
187 throw Exception('Wallet not found');
188 }
189 final wallet = MoneroWallet(
190 walletInfo: walletInfo,
191 derivationInfo: await walletInfo.getDerivationInfo(),
192 unspentCoinsInfo: unspentCoinsInfoSource,
193 password: password);
194
195 if (wallet.hardwareWalletType == HardwareWalletType.ledger) {
196 wallet.setLedgerConnection(gLedger!);
197 gLedger = null;
198 }
199
200 await wallet.init();
201
202 return wallet;
203 } catch (e) {
204 // TODO: Implement Exception for wallet list service.
205
206 switch (openWalletTry) {
207 case OpenWalletTry.initial:
208 await restoreOrResetWalletFiles(name);
209 return await openWallet(name, password, openWalletTry: OpenWalletTry.cacheRestored);
210 case OpenWalletTry.cacheRestored:
211 await removeCache(name);
212 return await openWallet(name, password, openWalletTry: OpenWalletTry.cacheRemoved);
213 case OpenWalletTry.cacheRemoved:
214 rethrow;
215 }
216 }
217 }
218
219 @override
220 Future<void> remove(String wallet) async {
221 final path = await pathForWalletDir(name: wallet, type: getType());
222 if (openedWalletsByPath["$path/$wallet"] != null) {
223 // NOTE: this is realistically only required on windows.
224 printV("closing wallet");
225 final w = openedWalletsByPath["$path/$wallet"]!;
226 final wmaddr = wmPtr.ffiAddress();
227 final waddr = w.ffiAddress();
228 openedWalletsByPath.remove("$path/$wallet");
229 await closeWalletAwaitIfShould(wmaddr, waddr);
230 printV("wallet closed");
231 }
232
233 final file = Directory(path);
234 final isExist = file.existsSync();
235
236 if (isExist) {
237 await file.delete(recursive: true);
238 }
239
240 final walletInfo = await WalletInfo.get(wallet, getType());
241 if (walletInfo == null) {
242 throw Exception('Wallet not found');
243 }
244 await WalletInfo.delete(walletInfo);
245 }
246
247 @override
248 Future<void> rename(String currentName, String password, String newName) async {
249 final currentWalletInfo = await WalletInfo.get(currentName, getType());
250 if (currentWalletInfo == null) {
251 throw Exception('Wallet not found');
252 }
253 final currentWallet = MoneroWallet(
254 walletInfo: currentWalletInfo,
255 derivationInfo: await currentWalletInfo.getDerivationInfo(),
256 unspentCoinsInfo: unspentCoinsInfoSource,
257 password: password,
258 );
259
260 await currentWallet.renameWalletFiles(newName);
261
262 final newWalletInfo = currentWalletInfo;
263 newWalletInfo.id = WalletBase.idFor(newName, getType());
264 newWalletInfo.name = newName;
265
266 await newWalletInfo.save();
267 }
268
269 @override
270 Future<MoneroWallet> restoreFromKeys(MoneroRestoreWalletFromKeysCredentials credentials,
271 {bool? isTestnet}) async {
272 try {
273 final path = await pathForWallet(name: credentials.name, type: getType());
274 monero_wallet_manager.restoreWalletFromKeys(
275 path: path,
276 password: credentials.password!,
277 language: credentials.language,
278 restoreHeight: credentials.height!,
279 address: credentials.address,
280 viewKey: credentials.viewKey,
281 spendKey: credentials.spendKey);
282 final wallet = MoneroWallet(
283 walletInfo: credentials.walletInfo!,
284 derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
285 unspentCoinsInfo: unspentCoinsInfoSource,
286 password: credentials.password!);
287 await wallet.init();
288
289 return wallet;
290 } catch (e) {
291 // TODO: Implement Exception for wallet list service.
292 printV('MoneroWalletsManager Error: $e');
293 rethrow;
294 }
295 }
296
297 @override
298 Future<MoneroWallet> restoreFromHardwareWallet(
299 MoneroRestoreWalletFromHardwareCredentials credentials) async {
300 try {
301 final path = await pathForWallet(name: credentials.name, type: getType());
302 final password = credentials.password;
303
304 if (credentials.hardwareWalletService case MoneroLedgerService service) {
305 enableLedgerExchange(service.connection);
306
307 await monero_wallet_manager.restoreWalletFromHardwareWallet(
308 path: path,
309 password: password!,
310 restoreHeight: credentials.height!,
311 deviceName: 'Ledger',
312 );
313 } else if (credentials.hardwareWalletService case MoneroTrezorService service) {
314 final trezor = Trezor(service);
315
316 if (credentials.passphrase != null) {
317 await trezor.newPassphraseSession(credentials.passphrase);
318 }
319
320 final watchCredentials = await trezor.getWatchCredentials();
321
322 monero_wallet_manager.restoreWalletFromKeys(
323 path: path,
324 password: credentials.password!,
325 language: "English",
326 restoreHeight: credentials.height!,
327 address: watchCredentials.address,
328 viewKey: watchCredentials.watchKey,
329 spendKey: "",
330 );
331
332 if (credentials.passphrase != null) {
333 monero_wallet_manager.storePassphrase(path: path, passphrase: credentials.passphrase!);
334 }
335 }
336
337 final wallet = MoneroWallet(
338 walletInfo: credentials.walletInfo!,
339 derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
340 unspentCoinsInfo: unspentCoinsInfoSource,
341 password: credentials.password!);
342 await wallet.init();
343
344 return wallet;
345 } catch (e) {
346 // TODO: Implement Exception for wallet list service.
347 printV('MoneroWalletsManager Error: $e');
348 rethrow;
349 }
350 }
351
352 @override
353 Future<MoneroWallet> restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials,
354 {bool? isTestnet}) async {
355 // Restore from Polyseed
356 try {
357 if (Polyseed.isValidSeed(credentials.mnemonic)) {
358 return restoreFromPolyseed(credentials);
359 }
360 } catch (e) {
361 printV("Polyseed restore failed: $e");
362 rethrow;
363 }
364
365 try {
366 if (isBip39Seed(credentials.mnemonic)) {
367 final path = await pathForWallet(name: credentials.name, type: getType());
368
369 return _restoreFromBip39(
370 path: path,
371 password: credentials.password!,
372 mnemonic: credentials.mnemonic,
373 walletInfo: credentials.walletInfo!,
374 overrideHeight: credentials.height!,
375 passphrase: credentials.passphrase,
376 );
377 }
378 } catch (e) {
379 printV("Bip39 restore failed: $e");
380 rethrow;
381 }
382
383 try {
384 final path = await pathForWallet(name: credentials.name, type: getType());
385
386 monero_wallet_manager.restoreWalletFromSeedSync(
387 path: path,
388 password: credentials.password!,
389 passphrase: credentials.passphrase,
390 seed: credentials.mnemonic,
391 restoreHeight: credentials.height!);
392 final wallet = MoneroWallet(
393 walletInfo: credentials.walletInfo!,
394 derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
395 unspentCoinsInfo: unspentCoinsInfoSource,
396 password: credentials.password!);
397 await wallet.init();
398
399 return wallet;
400 } catch (e) {
401 // TODO: Implement Exception for wallet list service.
402 printV('MoneroWalletsManager Error: $e');
403 rethrow;
404 }
405 }
406
407 Future<MoneroWallet> _restoreFromBip39({
408 required String path,
409 required String password,
410 required String mnemonic,
411 required WalletInfo walletInfo,
412 String? passphrase,
413 int? overrideHeight,
414 }) async {
415 final derivationInfo = await walletInfo.getDerivationInfo();
416 derivationInfo.derivationType = DerivationType.bip39;
417 derivationInfo.derivationPath = "m/44'/128'/0'/0/0";
418 await derivationInfo.save();
419
420 final legacyMnemonic = getLegacySeedFromBip39(mnemonic, passphrase: passphrase ?? "");
421 final height = overrideHeight ?? getMoneroHeigthByDate(date: DateTime.now());
422
423 walletInfo.isRecovery = true;
424 walletInfo.restoreHeight = height;
425
426 monero_wallet_manager.restoreWalletFromSeedSync(
427 path: path,
428 password: password,
429 passphrase: '',
430 seed: legacyMnemonic,
431 restoreHeight: height,
432 );
433
434 currentWallet!.setCacheAttribute(key: "cakewallet.seed.bip39", value: mnemonic);
435 currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase ?? '');
436
437 currentWallet!.store();
438
439 final wallet = MoneroWallet(
440 walletInfo: walletInfo,
441 derivationInfo: derivationInfo,
442 unspentCoinsInfo: unspentCoinsInfoSource,
443 password: password,
444 );
445 await wallet.init();
446
447 return wallet;
448 }
449
450 Future<MoneroWallet> restoreFromPolyseed(
451 MoneroRestoreWalletFromSeedCredentials credentials) async {
452 try {
453 final path = await pathForWallet(name: credentials.name, type: getType());
454 final polyseedCoin = PolyseedCoin.POLYSEED_MONERO;
455 final lang = PolyseedLang.getByPhrase(credentials.mnemonic);
456 final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin);
457
458 return _restoreFromPolyseed(
459 path, credentials.password!, polyseed, credentials.walletInfo!, lang,
460 passphrase: credentials.passphrase);
461 } catch (e) {
462 // TODO: Implement Exception for wallet list service.
463 printV('MoneroWalletsManager Error: $e');
464 rethrow;
465 }
466 }
467
468 Future<MoneroWallet> _restoreFromPolyseed(
469 String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
470 {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO,
471 int? overrideHeight,
472 String? passphrase}) async {
473 if (polyseed.isEncrypted == false && (passphrase ?? '') != "") {
474 // Fallback to the different passphrase offset method, when a passphrase
475 // was provided but the polyseed is not encrypted.
476 monero_wallet_manager.restoreWalletFromPolyseedWithOffset(
477 path: path,
478 password: password,
479 seed: polyseed.encode(lang, coin),
480 seedOffset: passphrase ?? '',
481 language: "English");
482
483 final wallet = MoneroWallet(
484 walletInfo: walletInfo,
485 derivationInfo: await walletInfo.getDerivationInfo(),
486 unspentCoinsInfo: unspentCoinsInfoSource,
487 password: password,
488 );
489 await wallet.init();
490
491 return wallet;
492 }
493
494 if (polyseed.isEncrypted) polyseed.crypt(passphrase ?? '');
495
496 final height = overrideHeight ??
497 getMoneroHeigthByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000));
498 final spendKey = polyseed.generateKey(coin, 32).toHexString();
499 final seed = polyseed.encode(lang, coin);
500
501 walletInfo.isRecovery = true;
502 walletInfo.restoreHeight = height;
503
504 monero_wallet_manager.restoreWalletFromSpendKeySync(
505 path: path,
506 password: password,
507 seed: seed,
508 language: lang.nameEnglish,
509 restoreHeight: height,
510 spendKey: spendKey);
511
512 currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed);
513 currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase ?? '');
514
515 final wallet = MoneroWallet(
516 walletInfo: walletInfo,
517 derivationInfo: await walletInfo.getDerivationInfo(),
518 unspentCoinsInfo: unspentCoinsInfoSource,
519 password: password,
520 );
521 await wallet.init();
522
523 return wallet;
524 }
525
526 Future<void> repairOldAndroidWallet(String name) async {
527 try {
528 if (!Platform.isAndroid) return;
529
530 final oldAndroidWalletDirPath = await outdatedAndroidPathForWalletDir(name: name);
531 final dir = Directory(oldAndroidWalletDirPath);
532
533 if (!dir.existsSync()) return;
534
535 final newWalletDirPath = await pathForWalletDir(name: name, type: getType());
536
537 dir.listSync().forEach((f) {
538 final file = File(f.path);
539 final name = f.path.split('/').last;
540 final newPath = newWalletDirPath + '/$name';
541 final newFile = File(newPath);
542
543 if (!newFile.existsSync()) {
544 newFile.createSync();
545 }
546 newFile.writeAsBytesSync(file.readAsBytesSync());
547 });
548 } catch (e) {
549 printV(e.toString());
550 }
551 }
552
553 @override
554 Future<String> getSeeds(String name, String password, WalletType type) async {
555 try {
556 final path = await pathForWallet(name: name, type: getType());
557
558 if (walletFilesExist(path)) await repairOldAndroidWallet(name);
559
560 await monero_wallet_manager.openWallet(path: path, password: password);
561 final walletInfo = await WalletInfo.get(name, getType());
562 if (walletInfo == null) {
563 throw Exception('Wallet not found');
564 }
565 final wallet = MoneroWallet(
566 walletInfo: walletInfo,
567 derivationInfo: await walletInfo.getDerivationInfo(),
568 unspentCoinsInfo: unspentCoinsInfoSource,
569 password: password,
570 );
571 return wallet.seed;
572 } catch (_) {
573 // if the file couldn't be opened or read
574 return '';
575 }
576 }
577
578 @override
579 Future<bool> requireHardwareWalletConnection(String name) async {
580 final walletInfo = await WalletInfo.get(name, getType());
581 if (walletInfo == null) {
582 return false;
583 }
584 return walletInfo.hardwareWalletType == HardwareWalletType.ledger;
585 }
586 }
587
588 Future<void> closeWalletAwaitIfShould(int wmaddr, int waddr) async {
589 if (Platform.isWindows) {
590 await Isolate.run(() {
591 monero.WalletManager_closeWallet(
592 Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
593 monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
594 });
595 } else {
596 unawaited(Isolate.run(() {
597 monero.WalletManager_closeWallet(
598 Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
599 monero.WalletManager_errorString(Pointer.fromAddress(wmaddr));
600 }));
601 }
602 }