| 1 | import 'dart:io'; |
| 2 | import 'dart:convert'; |
| 3 | import 'package:cake_wallet/core/secure_storage.dart'; |
| 4 | import 'package:collection/collection.dart'; |
| 5 | import 'package:cw_core/utils/print_verbose.dart'; |
| 6 | import 'package:shared_preferences/shared_preferences.dart'; |
| 7 | import 'package:hive/hive.dart'; |
| 8 | import 'package:path_provider/path_provider.dart'; |
| 9 | import 'package:cake_wallet/core/key_service.dart'; |
| 10 | import 'package:cake_wallet/entities/contact.dart'; |
| 11 | import 'package:cw_core/crypto_currency.dart'; |
| 12 | import 'package:cake_wallet/entities/encrypt.dart'; |
| 13 | import 'package:cake_wallet/entities/fiat_currency.dart'; |
| 14 | import 'package:cake_wallet/entities/ios_legacy_helper.dart' as ios_legacy_helper; |
| 15 | import 'package:cake_wallet/entities/preferences_key.dart'; |
| 16 | import 'package:cake_wallet/entities/secret_store_key.dart'; |
| 17 | import 'package:cw_core/wallet_info.dart'; |
| 18 | import 'package:cw_core/wallet_type.dart'; |
| 19 | import 'package:cake_wallet/exchange/exchange_provider_description.dart'; |
| 20 | import 'package:cake_wallet/exchange/trade.dart'; |
| 21 | import 'package:cake_wallet/.secrets.g.dart' as secrets; |
| 22 | |
| 23 | const reservedNames = ["flutter_assets", "wallets", "db"]; |
| 24 | |
| 25 | Future<void> migrate_android_v1() async { |
| 26 | final appDocDir = await getApplicationDocumentsDirectory(); |
| 27 | |
| 28 | await android_migrate_hives(appDocDir: appDocDir); |
| 29 | await android_migrate_wallets(appDocDir: appDocDir); |
| 30 | } |
| 31 | |
| 32 | Future<void> ios_migrate_v1(Box<Contact> contactSource) async { |
| 33 | final prefs = await SharedPreferences.getInstance(); |
| 34 | |
| 35 | if (prefs.getBool('ios_migration_v1_completed') ?? false) { |
| 36 | return; |
| 37 | } |
| 38 | |
| 39 | await ios_migrate_user_defaults(); |
| 40 | await ios_migrate_pin(); |
| 41 | await ios_migrate_wallet_passwords(); |
| 42 | await ios_migrate_wallet_info(); |
| 43 | await ios_migrate_trades_list(); |
| 44 | await ios_migrate_address_book(contactSource); |
| 45 | |
| 46 | await prefs.setBool('ios_migration_v1_completed', true); |
| 47 | } |
| 48 | |
| 49 | Future<void> ios_migrate_user_defaults() async { |
| 50 | //get the new shared preferences instance |
| 51 | final prefs = await SharedPreferences.getInstance(); |
| 52 | |
| 53 | if (prefs.getBool('ios_migration_user_defaults_completed') ?? false) { |
| 54 | return; |
| 55 | } |
| 56 | |
| 57 | //translate the node uri |
| 58 | final nodeURI = await ios_legacy_helper.getString('node_uri'); |
| 59 | // await prefs.setString('current_node_id', nodeURI); |
| 60 | await prefs.setInt('current_node_id', 0); |
| 61 | |
| 62 | //should we provide default btc node key? |
| 63 | final activeCurrency = await ios_legacy_helper.getInt('currency'); |
| 64 | |
| 65 | if (activeCurrency != null) { |
| 66 | final convertedCurrency = convertFiatLegacy(activeCurrency); |
| 67 | |
| 68 | await prefs.setString('current_fiat_currency', convertedCurrency.serialize()); |
| 69 | } |
| 70 | |
| 71 | //translate fee priority |
| 72 | final activeFeeTier = await ios_legacy_helper.getInt('saved_fee_priority'); |
| 73 | |
| 74 | if (activeFeeTier != null) { |
| 75 | await prefs.setInt('current_fee_priority', activeFeeTier); |
| 76 | } |
| 77 | |
| 78 | //translate current balance mode |
| 79 | final currentBalanceMode = await ios_legacy_helper.getInt('display_balance_mode'); |
| 80 | if (currentBalanceMode != null) { |
| 81 | await prefs.setInt('current_balance_display_mode', currentBalanceMode); |
| 82 | } |
| 83 | |
| 84 | //translate should save recipient address |
| 85 | final shouldSave = await ios_legacy_helper.getBool('should_save_recipient_address'); |
| 86 | |
| 87 | if (shouldSave != null) { |
| 88 | await prefs.setBool('save_recipient_address', shouldSave); |
| 89 | } |
| 90 | |
| 91 | //translate biometric |
| 92 | final biometricOn = await ios_legacy_helper.getBool('biometric_authentication_on'); |
| 93 | |
| 94 | if (biometricOn != null) { |
| 95 | await prefs.setBool('allow_biometrical_authentication', biometricOn); |
| 96 | } |
| 97 | |
| 98 | //read the current theme as integer, write it back as a bool |
| 99 | final currentTheme = prefs.getInt('current-theme'); |
| 100 | bool isDark = false; |
| 101 | if (currentTheme == 1) { |
| 102 | isDark = true; |
| 103 | } |
| 104 | await prefs.setBool('dark_theme', isDark); |
| 105 | |
| 106 | //assign the pin length |
| 107 | final pinLength = await ios_legacy_helper.getInt('pin-length'); |
| 108 | |
| 109 | if (pinLength != null) { |
| 110 | await prefs.setInt(PreferencesKey.currentPinLength, pinLength); |
| 111 | } |
| 112 | |
| 113 | //default value for display list key? |
| 114 | final walletName = await ios_legacy_helper.getString('current_wallet_name'); |
| 115 | |
| 116 | if (walletName != null) { |
| 117 | await prefs.setString('current_wallet_name', walletName); |
| 118 | } |
| 119 | |
| 120 | await prefs.setInt('current_wallet_type', serializeToInt(WalletType.monero)); |
| 121 | |
| 122 | await prefs.setBool('ios_migration_user_defaults_completed', true); |
| 123 | } |
| 124 | |
| 125 | Future<void> ios_migrate_pin() async { |
| 126 | final prefs = await SharedPreferences.getInstance(); |
| 127 | |
| 128 | if (prefs.getBool('ios_migration_pin_completed') ?? false) { |
| 129 | return; |
| 130 | } |
| 131 | |
| 132 | final flutterSecureStorage = secureStorageShared; |
| 133 | final pinPassword = await flutterSecureStorage.readNoIOptions(key: 'pin_password'); |
| 134 | // No pin |
| 135 | if (pinPassword == null) { |
| 136 | await prefs.setBool('ios_migration_pin_completed', true); |
| 137 | return; |
| 138 | } |
| 139 | |
| 140 | final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); |
| 141 | final encodedPassword = encodedPinCode(pin: pinPassword); |
| 142 | await flutterSecureStorage.write(key: key, value: encodedPassword); |
| 143 | |
| 144 | await prefs.setBool('ios_migration_pin_completed', true); |
| 145 | } |
| 146 | |
| 147 | Future<void> ios_migrate_wallet_passwords() async { |
| 148 | final prefs = await SharedPreferences.getInstance(); |
| 149 | |
| 150 | if (prefs.getBool('ios_migration_wallet_passwords_completed') ?? false) { |
| 151 | return; |
| 152 | } |
| 153 | |
| 154 | final appDocDir = await getApplicationDocumentsDirectory(); |
| 155 | final flutterSecureStorage = secureStorageShared; |
| 156 | final keyService = KeyService(flutterSecureStorage); |
| 157 | final walletsDir = Directory('${appDocDir.path}/wallets'); |
| 158 | final moneroWalletsDir = Directory('${walletsDir.path}/monero'); |
| 159 | |
| 160 | if (!moneroWalletsDir.existsSync() || moneroWalletsDir.listSync().isEmpty) { |
| 161 | await prefs.setBool('ios_migration_wallet_passwords_completed', true); |
| 162 | return; |
| 163 | } |
| 164 | |
| 165 | moneroWalletsDir.listSync().forEach((item) async { |
| 166 | try { |
| 167 | if (item is Directory) { |
| 168 | final name = item.path.split('/').last; |
| 169 | final oldKey = 'wallet_monero_' + name + '_password'; |
| 170 | final password = await flutterSecureStorage.readNoIOptions(key: oldKey); |
| 171 | await keyService.saveWalletPassword(walletName: name, password: password!); |
| 172 | } |
| 173 | } catch (e) { |
| 174 | printV(e.toString()); |
| 175 | } |
| 176 | }); |
| 177 | |
| 178 | await prefs.setBool('ios_migration_wallet_passwords_completed', true); |
| 179 | } |
| 180 | |
| 181 | FiatCurrency convertFiatLegacy(int raw) { |
| 182 | final _map = { |
| 183 | 0: 'aud', |
| 184 | 1: 'bgn', |
| 185 | 2: 'brl', |
| 186 | 3: 'cad', |
| 187 | 4: 'chf', |
| 188 | 5: 'cny', |
| 189 | 6: 'czk', |
| 190 | 7: 'eur', |
| 191 | 8: 'dkk', |
| 192 | 9: 'gbp', |
| 193 | 10: 'hkd', |
| 194 | 11: 'hrk', |
| 195 | 12: 'huf', |
| 196 | 13: 'idr', |
| 197 | 14: 'ils', |
| 198 | 15: 'inr', |
| 199 | 16: 'isk', |
| 200 | 17: 'jpy', |
| 201 | 18: 'krw', |
| 202 | 19: 'mxn', |
| 203 | 20: 'myr', |
| 204 | 21: 'nok', |
| 205 | 22: 'nzd', |
| 206 | 23: 'php', |
| 207 | 24: 'pln', |
| 208 | 25: 'ron', |
| 209 | 26: 'rub', |
| 210 | 27: 'sek', |
| 211 | 28: 'sgd', |
| 212 | 29: 'thb', |
| 213 | 30: 'try', |
| 214 | 31: 'usd', |
| 215 | 32: 'zar', |
| 216 | 33: 'vef' |
| 217 | }; |
| 218 | final fiatAsString = _map[raw]!; |
| 219 | |
| 220 | return FiatCurrency.deserialize(raw: fiatAsString.toUpperCase()); |
| 221 | } |
| 222 | |
| 223 | Future<void> android_migrate_hives({required Directory appDocDir}) async { |
| 224 | final dbDir = Directory('${appDocDir.path}/db'); |
| 225 | final files = <File>[]; |
| 226 | |
| 227 | appDocDir.listSync().forEach((FileSystemEntity item) { |
| 228 | final ext = item.path.split('.').last; |
| 229 | |
| 230 | if (item is File && (ext == "hive" || ext == "lock")) { |
| 231 | files.add(item); |
| 232 | } |
| 233 | }); |
| 234 | |
| 235 | if (!dbDir.existsSync()) { |
| 236 | dbDir.createSync(); |
| 237 | } |
| 238 | |
| 239 | files.forEach((File hive) { |
| 240 | final name = hive.path.split('/').last; |
| 241 | hive.copySync('${dbDir.path}/$name'); |
| 242 | hive.deleteSync(); |
| 243 | }); |
| 244 | } |
| 245 | |
| 246 | Future<void> android_migrate_wallets({required Directory appDocDir}) async { |
| 247 | final walletsDir = Directory('${appDocDir.path}/wallets'); |
| 248 | final moneroWalletsDir = Directory('${walletsDir.path}/monero'); |
| 249 | final dirs = <Directory>[]; |
| 250 | |
| 251 | appDocDir.listSync().forEach((FileSystemEntity item) { |
| 252 | final name = item.path.split('/').last; |
| 253 | |
| 254 | if (item is Directory && !reservedNames.contains(name)) { |
| 255 | dirs.add(item); |
| 256 | } |
| 257 | }); |
| 258 | |
| 259 | if (!moneroWalletsDir.existsSync()) { |
| 260 | await moneroWalletsDir.create(recursive: true); |
| 261 | } |
| 262 | |
| 263 | dirs.forEach((Directory dir) { |
| 264 | final name = dir.path.split('/').last; |
| 265 | final newDir = Directory('${moneroWalletsDir.path}/$name'); |
| 266 | newDir.createSync(); |
| 267 | |
| 268 | dir.listSync().forEach((file) { |
| 269 | if (file is File) { |
| 270 | final fileName = file.path.split('/').last; |
| 271 | file.copySync('${newDir.path}/$fileName'); |
| 272 | file.deleteSync(); |
| 273 | } |
| 274 | }); |
| 275 | |
| 276 | dir.deleteSync(); |
| 277 | }); |
| 278 | } |
| 279 | |
| 280 | Future<void> ios_migrate_wallet_info() async { |
| 281 | final prefs = await SharedPreferences.getInstance(); |
| 282 | |
| 283 | if (prefs.getBool('ios_migration_wallet_info_completed') ?? false) { |
| 284 | return; |
| 285 | } |
| 286 | |
| 287 | try { |
| 288 | final appDocDir = await getApplicationDocumentsDirectory(); |
| 289 | final walletsDir = Directory('${appDocDir.path}/wallets'); |
| 290 | final moneroWalletsDir = Directory('${walletsDir.path}/monero'); |
| 291 | final walletsInfo = await WalletInfo.getAll(); |
| 292 | final infoRecords = moneroWalletsDir |
| 293 | .listSync() |
| 294 | .map((item) { |
| 295 | try { |
| 296 | if (item is Directory) { |
| 297 | final name = item.path.split('/').last; |
| 298 | final configFile = File('${item.path}/$name.json'); |
| 299 | |
| 300 | if (!configFile.existsSync()) { |
| 301 | return null; |
| 302 | } |
| 303 | |
| 304 | final config = json.decode(configFile.readAsStringSync()) as Map<String, dynamic>; |
| 305 | final isRecovery = config['isRecovery'] as bool? ?? false; |
| 306 | final dateAsDouble = config['date'] as double; |
| 307 | final timestamp = dateAsDouble.toInt() * 1000; |
| 308 | final date = DateTime.fromMillisecondsSinceEpoch(timestamp); |
| 309 | final id = walletTypeToString(WalletType.monero).toLowerCase() + '_' + name; |
| 310 | final exist = walletsInfo.firstWhereOrNull((el) => el.id == id) != null; |
| 311 | |
| 312 | if (exist) { |
| 313 | return null; |
| 314 | } |
| 315 | |
| 316 | final walletInfo = WalletInfo.external( |
| 317 | id: id, |
| 318 | type: WalletType.monero, |
| 319 | name: name, |
| 320 | isRecovery: isRecovery, |
| 321 | restoreHeight: 0, |
| 322 | date: date, |
| 323 | dirPath: item.path, |
| 324 | path: '${item.path}/$name', |
| 325 | address: ''); |
| 326 | |
| 327 | return walletInfo; |
| 328 | } |
| 329 | } catch (e) { |
| 330 | printV(e.toString()); |
| 331 | return null; |
| 332 | } |
| 333 | }) |
| 334 | .where((el) => el != null) |
| 335 | .whereType<WalletInfo>() |
| 336 | .toList(); |
| 337 | for (final info in infoRecords) { |
| 338 | await info.save(); |
| 339 | } |
| 340 | await prefs.setBool('ios_migration_wallet_info_completed', true); |
| 341 | } catch (e) { |
| 342 | printV(e.toString()); |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | Future<void> ios_migrate_trades_list() async { |
| 347 | final prefs = await SharedPreferences.getInstance(); |
| 348 | |
| 349 | if (prefs.getBool('ios_migration_trade_list_completed') ?? false) { |
| 350 | return; |
| 351 | } |
| 352 | |
| 353 | try { |
| 354 | final appDocDir = await getApplicationDocumentsDirectory(); |
| 355 | final url = '${appDocDir.path}/trades_list.json'; |
| 356 | final file = File(url); |
| 357 | |
| 358 | if (!file.existsSync()) { |
| 359 | await prefs.setBool('ios_migration_trade_list_completed', true); |
| 360 | return; |
| 361 | } |
| 362 | |
| 363 | final content = file.readAsBytesSync(); |
| 364 | final flutterSecureStorage = secureStorageShared; |
| 365 | final masterPassword = await flutterSecureStorage.readNoIOptions(key: 'master_password'); |
| 366 | final key = masterPassword!.replaceAll('-', ''); |
| 367 | final decoded = await ios_legacy_helper.decrypt(content, key: key, salt: secrets.salt); |
| 368 | final decodedJson = json.decode(decoded) as List<dynamic>; |
| 369 | |
| 370 | for (final dynamic el in decodedJson) { |
| 371 | final elAsMap = el as Map<String, dynamic>; |
| 372 | final providerAsString = elAsMap['provider'] as String; |
| 373 | final fromAsString = elAsMap['from'] as String; |
| 374 | final toAsString = elAsMap['to'] as String; |
| 375 | final dateAsDouble = elAsMap['date'] as double; |
| 376 | final tradeId = elAsMap['tradeID'] as String; |
| 377 | final to = CryptoCurrency.fromString(toAsString); |
| 378 | final from = CryptoCurrency.fromString(fromAsString); |
| 379 | final timestamp = dateAsDouble.toInt() * 1000; |
| 380 | final date = DateTime.fromMillisecondsSinceEpoch(timestamp); |
| 381 | ExchangeProviderDescription? provider; |
| 382 | |
| 383 | switch (providerAsString.toLowerCase()) { |
| 384 | case 'changenow': |
| 385 | provider = ExchangeProviderDescription.changeNow; |
| 386 | break; |
| 387 | case 'xmr.to': |
| 388 | provider = ExchangeProviderDescription.xmrto; |
| 389 | break; |
| 390 | case 'morph': |
| 391 | provider = ExchangeProviderDescription.morphToken; |
| 392 | break; |
| 393 | default: |
| 394 | break; |
| 395 | } |
| 396 | |
| 397 | if (provider == null) continue; |
| 398 | |
| 399 | await Trade( |
| 400 | id: tradeId, |
| 401 | provider: provider, |
| 402 | from: from, |
| 403 | to: to, |
| 404 | createdAt: date, |
| 405 | amount: '', |
| 406 | receiveAmount: '', |
| 407 | ).save(); |
| 408 | } |
| 409 | |
| 410 | await prefs.setBool('ios_migration_trade_list_completed', true); |
| 411 | } catch (e) { |
| 412 | printV(e.toString()); |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | Future<void> ios_migrate_address_book(Box<Contact> contactSource) async { |
| 417 | try { |
| 418 | final prefs = await SharedPreferences.getInstance(); |
| 419 | |
| 420 | if (prefs.getBool('ios_migration_address_book_completed') ?? false) { |
| 421 | return; |
| 422 | } |
| 423 | |
| 424 | final appDocDir = await getApplicationDocumentsDirectory(); |
| 425 | final addressBookJSON = File('${appDocDir.path}/address_book.json'); |
| 426 | |
| 427 | if (!addressBookJSON.existsSync()) { |
| 428 | await prefs.setBool('ios_migration_address_book_completed', true); |
| 429 | return; |
| 430 | } |
| 431 | |
| 432 | final List<dynamic> addresses = |
| 433 | json.decode(addressBookJSON.readAsStringSync()) as List<dynamic>; |
| 434 | final contacts = addresses.map((dynamic item) { |
| 435 | final _item = item as Map<String, dynamic>; |
| 436 | final type = _item["type"] as String; |
| 437 | final address = _item["address"] as String; |
| 438 | final name = _item["name"] as String; |
| 439 | |
| 440 | return Contact(address: address, name: name, type: CryptoCurrency.fromString(type)); |
| 441 | }); |
| 442 | |
| 443 | await contactSource.addAll(contacts); |
| 444 | await prefs.setBool('ios_migration_address_book_completed', true); |
| 445 | } catch (e) { |
| 446 | printV(e.toString()); |
| 447 | } |
| 448 | } |