| 1 | import 'dart:ffi'; |
| 2 | import 'dart:io'; |
| 3 | import 'package:cw_core/exceptions.dart' show WalletDeprecationException; |
| 4 | import 'package:cw_core/monero_wallet_utils.dart'; |
| 5 | import 'package:cw_core/pathForWallet.dart'; |
| 6 | import 'package:cw_core/unspent_coins_info.dart'; |
| 7 | import 'package:cw_core/utils/print_verbose.dart'; |
| 8 | import 'package:cw_core/wallet_base.dart'; |
| 9 | import 'package:cw_core/wallet_credentials.dart'; |
| 10 | import 'package:cw_core/wallet_info.dart'; |
| 11 | import 'package:cw_core/wallet_service.dart'; |
| 12 | import 'package:cw_core/wallet_type.dart'; |
| 13 | import 'package:cw_core/get_height_by_date.dart'; |
| 14 | import 'package:cw_wownero/api/account_list.dart'; |
| 15 | import 'package:cw_wownero/api/exceptions/wallet_opening_exception.dart'; |
| 16 | import 'package:cw_wownero/api/wallet_manager.dart' as wownero_wallet_manager; |
| 17 | import 'package:cw_wownero/api/wallet_manager.dart'; |
| 18 | import 'package:cw_wownero/wownero_wallet.dart'; |
| 19 | import 'package:flutter/widgets.dart'; |
| 20 | import 'package:hive/hive.dart'; |
| 21 | import 'package:polyseed/polyseed.dart'; |
| 22 | import 'package:monero/wownero.dart' as wownero; |
| 23 | |
| 24 | class WowneroNewWalletCredentials extends WalletCredentials { |
| 25 | WowneroNewWalletCredentials( |
| 26 | {required String name, |
| 27 | required this.language, |
| 28 | required this.isPolyseed, |
| 29 | this.passphrase, |
| 30 | String? password}) |
| 31 | : super(name: name, password: password); |
| 32 | |
| 33 | final String language; |
| 34 | final bool isPolyseed; |
| 35 | final String? passphrase; |
| 36 | } |
| 37 | |
| 38 | class WowneroRestoreWalletFromSeedCredentials extends WalletCredentials { |
| 39 | WowneroRestoreWalletFromSeedCredentials( |
| 40 | {required String name, |
| 41 | required this.mnemonic, |
| 42 | required this.passphrase, |
| 43 | int height = 0, |
| 44 | String? password}) |
| 45 | : super(name: name, password: password, height: height); |
| 46 | |
| 47 | final String mnemonic; |
| 48 | final String passphrase; |
| 49 | } |
| 50 | |
| 51 | class WowneroWalletLoadingException implements Exception { |
| 52 | @override |
| 53 | String toString() => 'Failure to load the wallet.'; |
| 54 | } |
| 55 | |
| 56 | class WowneroRestoreWalletFromKeysCredentials extends WalletCredentials { |
| 57 | WowneroRestoreWalletFromKeysCredentials( |
| 58 | {required String name, |
| 59 | required String password, |
| 60 | required this.language, |
| 61 | required this.address, |
| 62 | required this.viewKey, |
| 63 | required this.spendKey, |
| 64 | int height = 0}) |
| 65 | : super(name: name, password: password, height: height); |
| 66 | |
| 67 | final String language; |
| 68 | final String address; |
| 69 | final String viewKey; |
| 70 | final String spendKey; |
| 71 | } |
| 72 | |
| 73 | class WowneroWalletService extends WalletService< |
| 74 | WowneroNewWalletCredentials, |
| 75 | WowneroRestoreWalletFromSeedCredentials, |
| 76 | WowneroRestoreWalletFromKeysCredentials, |
| 77 | WowneroNewWalletCredentials> { |
| 78 | WowneroWalletService(this.unspentCoinsInfoSource); |
| 79 | |
| 80 | final Box<UnspentCoinsInfo> unspentCoinsInfoSource; |
| 81 | |
| 82 | static bool walletFilesExist(String path) => |
| 83 | !File(path).existsSync() && !File('$path.keys').existsSync(); |
| 84 | |
| 85 | @override |
| 86 | WalletType getType() => WalletType.wownero; |
| 87 | |
| 88 | @override |
| 89 | Future<WowneroWallet> create(WowneroNewWalletCredentials credentials, {bool? isTestnet}) async { |
| 90 | try { |
| 91 | final path = await pathForWallet(name: credentials.name, type: getType()); |
| 92 | |
| 93 | if (credentials.isPolyseed) { |
| 94 | final polyseed = Polyseed.create(); |
| 95 | final lang = PolyseedLang.getByEnglishName(credentials.language); |
| 96 | |
| 97 | if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!); |
| 98 | |
| 99 | final heightOverride = |
| 100 | getWowneroHeightByDate(date: DateTime.now().subtract(Duration(days: 2))); |
| 101 | |
| 102 | return _restoreFromPolyseed( |
| 103 | path, credentials.password!, polyseed, credentials.walletInfo!, lang, |
| 104 | overrideHeight: heightOverride, passphrase: credentials.passphrase); |
| 105 | } |
| 106 | |
| 107 | await wownero_wallet_manager.createWallet( |
| 108 | path: path, |
| 109 | password: credentials.password!, |
| 110 | language: credentials.language, |
| 111 | passphrase: credentials.passphrase ?? ''); |
| 112 | final wallet = WowneroWallet( |
| 113 | walletInfo: credentials.walletInfo!, |
| 114 | derivationInfo: await credentials.walletInfo!.getDerivationInfo(), |
| 115 | unspentCoinsInfo: unspentCoinsInfoSource, |
| 116 | password: credentials.password!); |
| 117 | await wallet.init(); |
| 118 | |
| 119 | return wallet; |
| 120 | } catch (e) { |
| 121 | // TODO: Implement Exception for wallet list service. |
| 122 | printV('WowneroWalletsManager Error: ${e.toString()}'); |
| 123 | rethrow; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | @override |
| 128 | Future<bool> isWalletExit(String name) async { |
| 129 | try { |
| 130 | final path = await pathForWallet(name: name, type: getType()); |
| 131 | return wownero_wallet_manager.isWalletExist(path: path); |
| 132 | } catch (e) { |
| 133 | // TODO: Implement Exception for wallet list service. |
| 134 | printV('WowneroWalletsManager Error: $e'); |
| 135 | rethrow; |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | @override |
| 140 | Future<WowneroWallet> openWallet(String name, String password) async { |
| 141 | WowneroWallet? wallet; |
| 142 | try { |
| 143 | final path = await pathForWallet(name: name, type: getType()); |
| 144 | |
| 145 | if (walletFilesExist(path)) { |
| 146 | await repairOldAndroidWallet(name); |
| 147 | } |
| 148 | |
| 149 | await wownero_wallet_manager.openWalletAsync({'path': path, 'password': password}); |
| 150 | final walletInfo = await WalletInfo.get(name, getType()); |
| 151 | if (walletInfo == null) { |
| 152 | throw Exception('Wallet not found'); |
| 153 | } |
| 154 | |
| 155 | wallet = WowneroWallet( |
| 156 | walletInfo: walletInfo, |
| 157 | derivationInfo: await walletInfo.getDerivationInfo(), |
| 158 | unspentCoinsInfo: unspentCoinsInfoSource, |
| 159 | password: password); |
| 160 | throw WalletDeprecationException(seed: wallet.seed, curr: wallet.currency); |
| 161 | |
| 162 | final isValid = wallet.walletAddresses.validate(); |
| 163 | |
| 164 | if (!isValid) { |
| 165 | await restoreOrResetWalletFiles(name); |
| 166 | wallet.close(shouldCleanup: false); |
| 167 | return openWallet(name, password); |
| 168 | } |
| 169 | |
| 170 | await wallet.init(); |
| 171 | return wallet; |
| 172 | } catch (e, s) { |
| 173 | rethrow; |
| 174 | // TODO: Implement Exception for wallet list service. |
| 175 | |
| 176 | final bool isBadAlloc = e.toString().contains('bad_alloc') || |
| 177 | (e is WalletOpeningException && |
| 178 | (e.message == 'std::bad_alloc' || e.message.contains('bad_alloc'))); |
| 179 | |
| 180 | final bool doesNotCorrespond = e.toString().contains('does not correspond') || |
| 181 | (e is WalletOpeningException && e.message.contains('does not correspond')); |
| 182 | |
| 183 | final bool isMissingCacheFilesIOS = e.toString().contains('basic_string') || |
| 184 | (e is WalletOpeningException && e.message.contains('basic_string')); |
| 185 | |
| 186 | final bool isMissingCacheFilesAndroid = e.toString().contains('input_stream') || |
| 187 | e.toString().contains('input stream error') || |
| 188 | (e is WalletOpeningException && |
| 189 | (e.message.contains('input_stream') || e.message.contains('input stream error'))); |
| 190 | |
| 191 | final bool invalidSignature = e.toString().contains('invalid signature') || |
| 192 | (e is WalletOpeningException && e.message.contains('invalid signature')); |
| 193 | |
| 194 | if (!isBadAlloc && |
| 195 | !doesNotCorrespond && |
| 196 | !isMissingCacheFilesIOS && |
| 197 | !isMissingCacheFilesAndroid && |
| 198 | !invalidSignature && |
| 199 | wallet != null && |
| 200 | wallet.onError != null) { |
| 201 | wallet.onError!(FlutterErrorDetails(exception: e, stack: s)); |
| 202 | } |
| 203 | |
| 204 | await restoreOrResetWalletFiles(name); |
| 205 | return openWallet(name, password); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | @override |
| 210 | Future<void> remove(String wallet) async { |
| 211 | final path = await pathForWalletDir(name: wallet, type: getType()); |
| 212 | if (openedWalletsByPath["$path/$wallet"] != null) { |
| 213 | // NOTE: this is realistically only required on windows. |
| 214 | printV("closing wallet"); |
| 215 | final wmaddr = wmPtr.address; |
| 216 | final waddr = openedWalletsByPath["$path/$wallet"]!.address; |
| 217 | // await Isolate.run(() { |
| 218 | wownero.WalletManager_closeWallet( |
| 219 | Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), false); |
| 220 | // }); |
| 221 | openedWalletsByPath.remove("$path/$wallet"); |
| 222 | printV("wallet closed"); |
| 223 | } |
| 224 | |
| 225 | final file = Directory(path); |
| 226 | final isExist = file.existsSync(); |
| 227 | |
| 228 | if (isExist) { |
| 229 | await file.delete(recursive: true); |
| 230 | } |
| 231 | |
| 232 | final walletInfo = await WalletInfo.get(wallet, getType()); |
| 233 | if (walletInfo == null) { |
| 234 | throw Exception('Wallet not found'); |
| 235 | } |
| 236 | await WalletInfo.delete(walletInfo); |
| 237 | } |
| 238 | |
| 239 | @override |
| 240 | Future<void> rename(String currentName, String password, String newName) async { |
| 241 | final currentWalletInfo = await WalletInfo.get(currentName, getType()); |
| 242 | if (currentWalletInfo == null) { |
| 243 | throw Exception('Wallet not found'); |
| 244 | } |
| 245 | final currentWallet = WowneroWallet( |
| 246 | walletInfo: currentWalletInfo, |
| 247 | derivationInfo: await currentWalletInfo.getDerivationInfo(), |
| 248 | unspentCoinsInfo: unspentCoinsInfoSource, |
| 249 | password: password); |
| 250 | |
| 251 | await currentWallet.renameWalletFiles(newName); |
| 252 | |
| 253 | final newWalletInfo = currentWalletInfo; |
| 254 | newWalletInfo.id = WalletBase.idFor(newName, getType()); |
| 255 | newWalletInfo.name = newName; |
| 256 | |
| 257 | await newWalletInfo.save(); |
| 258 | } |
| 259 | |
| 260 | @override |
| 261 | Future<WowneroWallet> restoreFromKeys(WowneroRestoreWalletFromKeysCredentials credentials, |
| 262 | {bool? isTestnet}) async { |
| 263 | try { |
| 264 | final path = await pathForWallet(name: credentials.name, type: getType()); |
| 265 | await wownero_wallet_manager.restoreFromKeys( |
| 266 | path: path, |
| 267 | password: credentials.password!, |
| 268 | language: credentials.language, |
| 269 | restoreHeight: credentials.height!, |
| 270 | address: credentials.address, |
| 271 | viewKey: credentials.viewKey, |
| 272 | spendKey: credentials.spendKey); |
| 273 | final wallet = WowneroWallet( |
| 274 | walletInfo: credentials.walletInfo!, |
| 275 | derivationInfo: await credentials.walletInfo!.getDerivationInfo(), |
| 276 | unspentCoinsInfo: unspentCoinsInfoSource, |
| 277 | password: credentials.password!); |
| 278 | await wallet.init(); |
| 279 | |
| 280 | return wallet; |
| 281 | } catch (e) { |
| 282 | // TODO: Implement Exception for wallet list service. |
| 283 | printV('WowneroWalletsManager Error: $e'); |
| 284 | rethrow; |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | @override |
| 289 | Future<WowneroWallet> restoreFromHardwareWallet(WowneroNewWalletCredentials credentials) { |
| 290 | throw UnimplementedError( |
| 291 | "Restoring a Wownero wallet from a hardware wallet is not yet supported!"); |
| 292 | } |
| 293 | |
| 294 | @override |
| 295 | Future<WowneroWallet> restoreFromSeed(WowneroRestoreWalletFromSeedCredentials credentials, |
| 296 | {bool? isTestnet}) async { |
| 297 | // Restore from Polyseed |
| 298 | if (Polyseed.isValidSeed(credentials.mnemonic)) { |
| 299 | return restoreFromPolyseed(credentials); |
| 300 | } |
| 301 | |
| 302 | try { |
| 303 | final path = await pathForWallet(name: credentials.name, type: getType()); |
| 304 | await wownero_wallet_manager.restoreFromSeed( |
| 305 | path: path, |
| 306 | password: credentials.password!, |
| 307 | passphrase: credentials.passphrase, |
| 308 | seed: credentials.mnemonic, |
| 309 | restoreHeight: credentials.height!); |
| 310 | final wallet = WowneroWallet( |
| 311 | walletInfo: credentials.walletInfo!, |
| 312 | derivationInfo: await credentials.walletInfo!.getDerivationInfo(), |
| 313 | unspentCoinsInfo: unspentCoinsInfoSource, |
| 314 | password: credentials.password!); |
| 315 | await wallet.init(); |
| 316 | |
| 317 | return wallet; |
| 318 | } catch (e) { |
| 319 | // TODO: Implement Exception for wallet list service. |
| 320 | printV('WowneroWalletsManager Error: $e'); |
| 321 | rethrow; |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | Future<WowneroWallet> restoreFromPolyseed( |
| 326 | WowneroRestoreWalletFromSeedCredentials credentials) async { |
| 327 | try { |
| 328 | final path = await pathForWallet(name: credentials.name, type: getType()); |
| 329 | final polyseedCoin = PolyseedCoin.POLYSEED_WOWNERO; |
| 330 | final lang = PolyseedLang.getByPhrase(credentials.mnemonic); |
| 331 | final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin); |
| 332 | |
| 333 | return _restoreFromPolyseed( |
| 334 | path, credentials.password!, polyseed, credentials.walletInfo!, lang, |
| 335 | passphrase: credentials.passphrase); |
| 336 | } catch (e) { |
| 337 | // TODO: Implement Exception for wallet list service. |
| 338 | printV('WowneroWalletsManager Error: $e'); |
| 339 | rethrow; |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | Future<WowneroWallet> _restoreFromPolyseed( |
| 344 | String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, |
| 345 | {PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, |
| 346 | int? overrideHeight, |
| 347 | String? passphrase}) async { |
| 348 | if (polyseed.isEncrypted == false && (passphrase ?? '') != "") { |
| 349 | // Fallback to the different passphrase offset method, when a passphrase |
| 350 | // was provided but the polyseed is not encrypted. |
| 351 | wownero_wallet_manager.restoreWalletFromPolyseedWithOffset( |
| 352 | path: path, |
| 353 | password: password, |
| 354 | seed: polyseed.encode(lang, coin), |
| 355 | seedOffset: passphrase ?? '', |
| 356 | language: "English"); |
| 357 | |
| 358 | final wallet = WowneroWallet( |
| 359 | walletInfo: walletInfo, |
| 360 | derivationInfo: await walletInfo.getDerivationInfo(), |
| 361 | unspentCoinsInfo: unspentCoinsInfoSource, |
| 362 | password: password, |
| 363 | ); |
| 364 | await wallet.init(); |
| 365 | |
| 366 | return wallet; |
| 367 | } |
| 368 | |
| 369 | if (polyseed.isEncrypted) polyseed.crypt(passphrase ?? ''); |
| 370 | |
| 371 | final height = overrideHeight ?? |
| 372 | getWowneroHeightByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); |
| 373 | final spendKey = polyseed.generateKey(coin, 32).toHexString(); |
| 374 | final seed = polyseed.encode(lang, coin); |
| 375 | |
| 376 | walletInfo.isRecovery = true; |
| 377 | walletInfo.restoreHeight = height; |
| 378 | |
| 379 | await wownero_wallet_manager.restoreFromSpendKey( |
| 380 | path: path, |
| 381 | password: password, |
| 382 | seed: seed, |
| 383 | language: lang.nameEnglish, |
| 384 | restoreHeight: height, |
| 385 | spendKey: spendKey); |
| 386 | |
| 387 | wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); |
| 388 | wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase ?? ''); |
| 389 | |
| 390 | final wallet = WowneroWallet( |
| 391 | walletInfo: walletInfo, |
| 392 | derivationInfo: await walletInfo.getDerivationInfo(), |
| 393 | unspentCoinsInfo: unspentCoinsInfoSource, |
| 394 | password: password); |
| 395 | await wallet.init(); |
| 396 | |
| 397 | return wallet; |
| 398 | } |
| 399 | |
| 400 | Future<void> repairOldAndroidWallet(String name) async { |
| 401 | try { |
| 402 | if (!Platform.isAndroid) { |
| 403 | return; |
| 404 | } |
| 405 | |
| 406 | final oldAndroidWalletDirPath = await outdatedAndroidPathForWalletDir(name: name); |
| 407 | final dir = Directory(oldAndroidWalletDirPath); |
| 408 | |
| 409 | if (!dir.existsSync()) { |
| 410 | return; |
| 411 | } |
| 412 | |
| 413 | final newWalletDirPath = await pathForWalletDir(name: name, type: getType()); |
| 414 | |
| 415 | dir.listSync().forEach((f) { |
| 416 | final file = File(f.path); |
| 417 | final name = f.path.split('/').last; |
| 418 | final newPath = newWalletDirPath + '/$name'; |
| 419 | final newFile = File(newPath); |
| 420 | |
| 421 | if (!newFile.existsSync()) { |
| 422 | newFile.createSync(); |
| 423 | } |
| 424 | newFile.writeAsBytesSync(file.readAsBytesSync()); |
| 425 | }); |
| 426 | } catch (e) { |
| 427 | printV(e.toString()); |
| 428 | } |
| 429 | } |
| 430 | } |