import 'dart:async'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; import 'package:collection/collection.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/bip39_seed.dart'; import 'package:cw_monero/ledger.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:monero/src/monero.dart' as m; import 'package:monero/monero.dart' as monero; import 'package:polyseed/polyseed.dart'; enum MoneroSeedType { polyseed, legacy, bip39 } class MoneroNewWalletCredentials extends WalletCredentials { MoneroNewWalletCredentials( {required String name, required this.language, required this.seedType, String? password, this.passphrase, this.mnemonic}) : super(name: name, password: password); final String language; final MoneroSeedType seedType; final String? passphrase; final String? mnemonic; } class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials { MoneroRestoreWalletFromHardwareCredentials( {required String name, required this.ledgerConnection, int height = 0, String? password}) : super(name: name, password: password, height: height); LedgerConnection ledgerConnection; } class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials { MoneroRestoreWalletFromSeedCredentials( {required String name, required this.mnemonic, required this.passphrase, int height = 0, String? password}) : super(name: name, password: password, height: height); final String mnemonic; final String passphrase; } class MoneroWalletLoadingException implements Exception { @override String toString() => 'Failure to load the wallet.'; } class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials { MoneroRestoreWalletFromKeysCredentials( {required String name, required String password, required this.language, required this.address, required this.viewKey, required this.spendKey, int height = 0}) : super(name: name, password: password, height: height); final String language; final String address; final String viewKey; final String spendKey; } enum OpenWalletTry { initial, cacheRestored, cacheRemoved, } class MoneroWalletService extends WalletService< MoneroNewWalletCredentials, MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials, MoneroRestoreWalletFromHardwareCredentials> { MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; final Box unspentCoinsInfoSource; static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); @override WalletType getType() => WalletType.monero; @override Future create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); if (credentials.seedType == MoneroSeedType.bip39) { return _restoreFromBip39( path: path, password: credentials.password!, mnemonic: credentials.mnemonic ?? getBip39Seed(), passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, ); } if (credentials.seedType == MoneroSeedType.polyseed) { final polyseed = Polyseed.create(); final lang = PolyseedLang.getByEnglishName(credentials.language); if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!); final heightOverride = getMoneroHeigthByDate( date: DateTime.now().subtract(Duration(days: 2))); return _restoreFromPolyseed(path, credentials.password!, polyseed, credentials.walletInfo!, lang, overrideHeight: heightOverride, passphrase: credentials.passphrase); } monero_wallet_manager.createWallet( path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase ?? ""); final wallet = MoneroWallet( walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); await wallet.init(); return wallet; } catch (e) { // TODO: Implement Exception for wallet list service. printV('MoneroWalletsManager Error: ${e.toString()}'); rethrow; } } @override Future isWalletExit(String name) async { try { final path = await pathForWallet(name: name, type: getType()); return monero_wallet_manager.isWalletExist(path: path); } catch (e) { // TODO: Implement Exception for wallet list service. printV('MoneroWalletsManager Error: $e'); rethrow; } } @override Future openWallet(String name, String password, {OpenWalletTry openWalletTry = OpenWalletTry.initial}) async { try { final path = await pathForWallet(name: name, type: getType()); if (walletFilesExist(path)) await repairOldAndroidWallet(name); await monero_wallet_manager .openWallet(path: path, password: password); final walletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); final wallet = MoneroWallet( walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password); if (wallet.isHardwareWallet) { wallet.setLedgerConnection(gLedger!); gLedger = null; } await wallet.init(); return wallet; } catch (e) { // TODO: Implement Exception for wallet list service. switch (openWalletTry) { case OpenWalletTry.initial: await restoreOrResetWalletFiles(name); return await openWallet(name, password, openWalletTry: OpenWalletTry.cacheRestored); case OpenWalletTry.cacheRestored: await removeCache(name); return await openWallet(name, password, openWalletTry: OpenWalletTry.cacheRemoved); case OpenWalletTry.cacheRemoved: rethrow; } } } @override Future remove(String wallet) async { final path = await pathForWalletDir(name: wallet, type: getType()); if (openedWalletsByPath["$path/$wallet"] != null) { // NOTE: this is realistically only required on windows. printV("closing wallet"); final w = openedWalletsByPath["$path/$wallet"]!; final wmaddr = wmPtr.ffiAddress(); final waddr = w.ffiAddress(); openedWalletsByPath.remove("$path/$wallet"); await closeWalletAwaitIfShould(wmaddr, waddr); printV("wallet closed"); } final file = Directory(path); final isExist = file.existsSync(); if (isExist) { await file.delete(recursive: true); } final walletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); await walletInfoSource.delete(walletInfo.key); } @override Future rename(String currentName, String password, String newName) async { final currentWalletInfo = walletInfoSource.values.firstWhere( (info) => info.id == WalletBase.idFor(currentName, getType())); final currentWallet = MoneroWallet( walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password, ); await currentWallet.renameWalletFiles(newName); final newWalletInfo = currentWalletInfo; newWalletInfo.id = WalletBase.idFor(newName, getType()); newWalletInfo.name = newName; await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } @override Future restoreFromKeys(MoneroRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); monero_wallet_manager.restoreWalletFromKeys( path: path, password: credentials.password!, language: credentials.language, restoreHeight: credentials.height!, address: credentials.address, viewKey: credentials.viewKey, spendKey: credentials.spendKey); final wallet = MoneroWallet( walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); await wallet.init(); return wallet; } catch (e) { // TODO: Implement Exception for wallet list service. printV('MoneroWalletsManager Error: $e'); rethrow; } } @override Future restoreFromHardwareWallet( MoneroRestoreWalletFromHardwareCredentials credentials) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); final password = credentials.password; final height = credentials.height; enableLedgerExchange(credentials.ledgerConnection); await monero_wallet_manager.restoreWalletFromHardwareWallet( path: path, password: password!, restoreHeight: height!, deviceName: 'Ledger'); final wallet = MoneroWallet( walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); await wallet.init(); return wallet; } catch (e) { // TODO: Implement Exception for wallet list service. printV('MoneroWalletsManager Error: $e'); rethrow; } } @override Future restoreFromSeed( MoneroRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { // Restore from Polyseed try { if (Polyseed.isValidSeed(credentials.mnemonic)) { return restoreFromPolyseed(credentials); } } catch (e) { printV("Polyseed restore failed: $e"); rethrow; } try { if (isBip39Seed(credentials.mnemonic)) { final path = await pathForWallet(name: credentials.name, type: getType()); return _restoreFromBip39( path: path, password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, overrideHeight: credentials.height!, passphrase: credentials.passphrase, ); } } catch (e) { printV("Bip39 restore failed: $e"); rethrow; } try { final path = await pathForWallet(name: credentials.name, type: getType()); monero_wallet_manager.restoreWalletFromSeedSync( path: path, password: credentials.password!, passphrase: credentials.passphrase, seed: credentials.mnemonic, restoreHeight: credentials.height!); final wallet = MoneroWallet( walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); await wallet.init(); return wallet; } catch (e) { // TODO: Implement Exception for wallet list service. printV('MoneroWalletsManager Error: $e'); rethrow; } } Future _restoreFromBip39({ required String path, required String password, required String mnemonic, required WalletInfo walletInfo, String? passphrase, int? overrideHeight, }) async { walletInfo.derivationInfo = DerivationInfo( derivationType: DerivationType.bip39, derivationPath: "m/44'/128'/0'/0/0", ); final legacyMnemonic = getLegacySeedFromBip39(mnemonic, passphrase: passphrase ?? ""); final height = overrideHeight ?? getMoneroHeigthByDate(date: DateTime.now()); walletInfo.isRecovery = true; walletInfo.restoreHeight = height; monero_wallet_manager.restoreWalletFromSeedSync( path: path, password: password, passphrase: '', seed: legacyMnemonic, restoreHeight: height, ); currentWallet!.setCacheAttribute( key: "cakewallet.seed.bip39", value: mnemonic); currentWallet!.setCacheAttribute( key: "cakewallet.passphrase", value: passphrase ?? ''); currentWallet!.store(); final wallet = MoneroWallet( walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password, ); await wallet.init(); return wallet; } Future restoreFromPolyseed( MoneroRestoreWalletFromSeedCredentials credentials) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); final polyseedCoin = PolyseedCoin.POLYSEED_MONERO; final lang = PolyseedLang.getByPhrase(credentials.mnemonic); final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin); return _restoreFromPolyseed( path, credentials.password!, polyseed, credentials.walletInfo!, lang, passphrase: credentials.passphrase); } catch (e) { // TODO: Implement Exception for wallet list service. printV('MoneroWalletsManager Error: $e'); rethrow; } } Future _restoreFromPolyseed(String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight, String? passphrase}) async { if (polyseed.isEncrypted == false && (passphrase ?? '') != "") { // Fallback to the different passphrase offset method, when a passphrase // was provided but the polyseed is not encrypted. monero_wallet_manager.restoreWalletFromPolyseedWithOffset( path: path, password: password, seed: polyseed.encode(lang, coin), seedOffset: passphrase ?? '', language: "English"); final wallet = MoneroWallet( walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password, ); await wallet.init(); return wallet; } if (polyseed.isEncrypted) polyseed.crypt(passphrase ?? ''); final height = overrideHeight ?? getMoneroHeigthByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); final spendKey = polyseed.generateKey(coin, 32).toHexString(); final seed = polyseed.encode(lang, coin); walletInfo.isRecovery = true; walletInfo.restoreHeight = height; monero_wallet_manager.restoreWalletFromSpendKeySync( path: path, password: password, seed: seed, language: lang.nameEnglish, restoreHeight: height, spendKey: spendKey); currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed); currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase??''); final wallet = MoneroWallet( walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password, ); await wallet.init(); return wallet; } Future repairOldAndroidWallet(String name) async { try { if (!Platform.isAndroid) return; final oldAndroidWalletDirPath = await outdatedAndroidPathForWalletDir(name: name); final dir = Directory(oldAndroidWalletDirPath); if (!dir.existsSync()) return; final newWalletDirPath = await pathForWalletDir(name: name, type: getType()); dir.listSync().forEach((f) { final file = File(f.path); final name = f.path.split('/').last; final newPath = newWalletDirPath + '/$name'; final newFile = File(newPath); if (!newFile.existsSync()) { newFile.createSync(); } newFile.writeAsBytesSync(file.readAsBytesSync()); }); } catch (e) { printV(e.toString()); } } @override Future getSeeds(String name, String password, WalletType type) async { try { final path = await pathForWallet(name: name, type: getType()); if (walletFilesExist(path)) await repairOldAndroidWallet(name); await monero_wallet_manager .openWallet(path: path, password: password); final walletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); final wallet = MoneroWallet( walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password, ); return wallet.seed; } catch (_) { // if the file couldn't be opened or read return ''; } } @override bool requireHardwareWalletConnection(String name) { return walletInfoSource.values .firstWhereOrNull( (info) => info.id == WalletBase.idFor(name, getType())) ?.isHardwareWallet ?? false; } } Future closeWalletAwaitIfShould(int wmaddr, int waddr) async { if (Platform.isWindows) { await Isolate.run(() { monero.WalletManager_closeWallet( Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); monero.WalletManager_errorString(Pointer.fromAddress(wmaddr)); }); } else { unawaited(Isolate.run(() { monero.WalletManager_closeWallet( Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); monero.WalletManager_errorString(Pointer.fromAddress(wmaddr)); })); } }