| 1 | import 'dart:ffi'; |
| 2 | import 'dart:io'; |
| 3 | import 'dart:isolate'; |
| 4 | |
| 5 | import 'package:cw_core/utils/print_verbose.dart'; |
| 6 | import 'package:cw_monero/api/account_list.dart'; |
| 7 | import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart'; |
| 8 | import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; |
| 9 | import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; |
| 10 | import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; |
| 11 | import 'package:cw_monero/api/transaction_history.dart'; |
| 12 | import 'package:cw_monero/api/wallet.dart'; |
| 13 | import 'package:cw_monero/ledger.dart'; |
| 14 | import 'package:flutter/foundation.dart'; |
| 15 | import 'package:monero/src/monero.dart'; |
| 16 | import 'package:monero/src/wallet2.dart'; |
| 17 | import 'package:monero/monero.dart' as monero; |
| 18 | |
| 19 | class MoneroCException implements Exception { |
| 20 | final String message; |
| 21 | |
| 22 | MoneroCException(this.message); |
| 23 | |
| 24 | @override |
| 25 | String toString() => message; |
| 26 | } |
| 27 | |
| 28 | void checkIfMoneroCIsFine() { |
| 29 | final checksum = MoneroWalletChecksum(); |
| 30 | final cppCsCpp = checksum.checksum_wallet2_api_c_cpp(); |
| 31 | final cppCsH = checksum.checksum_wallet2_api_c_h(); |
| 32 | final cppCsExp = checksum.checksum_wallet2_api_c_exp(); |
| 33 | |
| 34 | final dartCsCpp = monero.wallet2_api_c_cpp_sha256; |
| 35 | final dartCsH = monero.wallet2_api_c_h_sha256; |
| 36 | final dartCsExp = monero.wallet2_api_c_exp_sha256; |
| 37 | |
| 38 | if (cppCsCpp != dartCsCpp) { |
| 39 | throw MoneroCException( |
| 40 | "monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'"); |
| 41 | } |
| 42 | |
| 43 | if (cppCsH != dartCsH) { |
| 44 | throw MoneroCException( |
| 45 | "monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'"); |
| 46 | } |
| 47 | |
| 48 | if (cppCsExp != dartCsExp && (Platform.isIOS || Platform.isMacOS)) { |
| 49 | throw MoneroCException( |
| 50 | "monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'"); |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | Wallet2WalletManager? _wmPtr; |
| 55 | Wallet2WalletManager wmPtr = (() { |
| 56 | try { |
| 57 | // Problems with the wallet? Crashes? Lags? this will print all calls to xmr |
| 58 | // codebase, so it will be easier to debug what happens. At least easier |
| 59 | // than plugging gdb in. Especially on windows/android. |
| 60 | monero.printStarts = false; |
| 61 | if (kDebugMode && debugMonero) { |
| 62 | MoneroWalletManagerFactory().setLogLevel(4); |
| 63 | } |
| 64 | _wmPtr ??= MoneroWalletManagerFactory().getWalletManager(); |
| 65 | if (kDebugMode && debugMonero) { |
| 66 | MoneroWalletManagerFactory().setLogLevel(4); |
| 67 | } |
| 68 | printV("ptr: $_wmPtr"); |
| 69 | } catch (e) { |
| 70 | printV(e); |
| 71 | rethrow; |
| 72 | } |
| 73 | return _wmPtr!; |
| 74 | })(); |
| 75 | |
| 76 | Wallet2Wallet createWalletPointer() { |
| 77 | final newWptr = wmPtr.createWallet(path: "", password: "", language: "", networkType: 0); |
| 78 | return newWptr; |
| 79 | } |
| 80 | |
| 81 | void createWallet( |
| 82 | {required String path, |
| 83 | required String password, |
| 84 | required String language, |
| 85 | required String passphrase, |
| 86 | int nettype = 0}) { |
| 87 | txhistory = null; |
| 88 | language = getSeedLanguage(language)!; |
| 89 | final newW = |
| 90 | wmPtr.createWallet(path: path, password: password, language: language, networkType: 0); |
| 91 | |
| 92 | int status = newW.status(); |
| 93 | if (status != 0) { |
| 94 | throw WalletCreationException(message: newW.errorString()); |
| 95 | } |
| 96 | newW.store(path: path); |
| 97 | setupBackgroundSync(password, newW); |
| 98 | newW.store(path: path); |
| 99 | |
| 100 | currentWallet = newW; |
| 101 | currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase); |
| 102 | currentWallet!.store(path: path); |
| 103 | openedWalletsByPath[path] = currentWallet!; |
| 104 | _lastOpenedWallet = path; |
| 105 | } |
| 106 | |
| 107 | bool isWalletExist({required String path}) { |
| 108 | return wmPtr.walletExists(path); |
| 109 | } |
| 110 | |
| 111 | void restoreWalletFromSeedSync( |
| 112 | {required String path, |
| 113 | required String password, |
| 114 | required String passphrase, |
| 115 | required String seed, |
| 116 | int nettype = 0, |
| 117 | int restoreHeight = 0}) { |
| 118 | txhistory = null; |
| 119 | final newW = wmPtr.recoveryWallet( |
| 120 | path: path, |
| 121 | password: password, |
| 122 | mnemonic: seed, |
| 123 | restoreHeight: restoreHeight, |
| 124 | seedOffset: passphrase, |
| 125 | networkType: 0, |
| 126 | ); |
| 127 | |
| 128 | final status = newW.status(); |
| 129 | |
| 130 | if (status != 0) { |
| 131 | final error = newW.errorString(); |
| 132 | if (error.contains('word list failed verification')) { |
| 133 | throw WalletRestoreFromSeedException( |
| 134 | message: |
| 135 | "Seed verification failed, please make sure you entered the correct seed with the correct words order", |
| 136 | ); |
| 137 | } |
| 138 | throw WalletRestoreFromSeedException(message: error); |
| 139 | } |
| 140 | currentWallet = newW; |
| 141 | |
| 142 | setRefreshFromBlockHeight(height: restoreHeight); |
| 143 | setupBackgroundSync(password, newW); |
| 144 | |
| 145 | currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase); |
| 146 | |
| 147 | openedWalletsByPath[path] = currentWallet!; |
| 148 | |
| 149 | currentWallet!.store(path: path); |
| 150 | _lastOpenedWallet = path; |
| 151 | } |
| 152 | |
| 153 | void storePassphrase({required String path, required String passphrase}) { |
| 154 | currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase); |
| 155 | currentWallet!.store(path: path); |
| 156 | } |
| 157 | |
| 158 | void restoreWalletFromKeys( |
| 159 | {required String path, |
| 160 | required String password, |
| 161 | required String language, |
| 162 | required String address, |
| 163 | required String viewKey, |
| 164 | required String spendKey, |
| 165 | int nettype = 0, |
| 166 | int restoreHeight = 0}) { |
| 167 | txhistory = null; |
| 168 | var newW = (spendKey != "") |
| 169 | ? wmPtr.createDeterministicWalletFromSpendKey( |
| 170 | path: path, |
| 171 | password: password, |
| 172 | language: language, |
| 173 | spendKeyString: spendKey, |
| 174 | newWallet: true, |
| 175 | // TODO(mrcyjanek): safe to remove |
| 176 | restoreHeight: restoreHeight) |
| 177 | : wmPtr.createWalletFromKeys( |
| 178 | path: path, |
| 179 | password: password, |
| 180 | restoreHeight: restoreHeight, |
| 181 | addressString: address, |
| 182 | viewKeyString: viewKey, |
| 183 | spendKeyString: spendKey, |
| 184 | nettype: 0, |
| 185 | ); |
| 186 | |
| 187 | int status = newW.status(); |
| 188 | if (status != 0) { |
| 189 | throw WalletRestoreFromKeysException(message: newW.errorString()); |
| 190 | } |
| 191 | newW.store(path: path); |
| 192 | |
| 193 | // CW-712 - Try to restore deterministic wallet first, if the view key doesn't |
| 194 | // match the view key provided |
| 195 | if (spendKey != "") { |
| 196 | final viewKeyRestored = newW.secretViewKey(); |
| 197 | if (viewKey != viewKeyRestored && viewKey != "") { |
| 198 | wmPtr.closeWallet(newW, false); |
| 199 | File(path).deleteSync(); |
| 200 | File(path + ".keys").deleteSync(); |
| 201 | newW = wmPtr.createWalletFromKeys( |
| 202 | path: path, |
| 203 | password: password, |
| 204 | restoreHeight: restoreHeight, |
| 205 | addressString: address, |
| 206 | viewKeyString: viewKey, |
| 207 | spendKeyString: spendKey, |
| 208 | nettype: 0, |
| 209 | ); |
| 210 | int status = newW.status(); |
| 211 | if (status != 0) { |
| 212 | throw WalletRestoreFromKeysException(message: newW.errorString()); |
| 213 | } |
| 214 | newW.store(path: path); |
| 215 | |
| 216 | setupBackgroundSync(password, newW); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | currentWallet = newW; |
| 221 | |
| 222 | openedWalletsByPath[path] = currentWallet!; |
| 223 | _lastOpenedWallet = path; |
| 224 | } |
| 225 | |
| 226 | // English only, because normalization. |
| 227 | void restoreWalletFromPolyseedWithOffset( |
| 228 | {required String path, |
| 229 | required String password, |
| 230 | required String seed, |
| 231 | required String seedOffset, |
| 232 | required String language, |
| 233 | int nettype = 0}) { |
| 234 | txhistory = null; |
| 235 | final newW = wmPtr.createWalletFromPolyseed( |
| 236 | path: path, |
| 237 | password: password, |
| 238 | networkType: nettype, |
| 239 | mnemonic: seed, |
| 240 | seedOffset: seedOffset, |
| 241 | newWallet: true, // safe to remove |
| 242 | restoreHeight: 0, |
| 243 | kdfRounds: 1, |
| 244 | ); |
| 245 | |
| 246 | int status = newW.status(); |
| 247 | |
| 248 | if (status != 0) { |
| 249 | final err = newW.errorString(); |
| 250 | printV("err: $err"); |
| 251 | throw WalletRestoreFromKeysException(message: err); |
| 252 | } |
| 253 | |
| 254 | currentWallet = newW; |
| 255 | |
| 256 | currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed); |
| 257 | currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: seedOffset); |
| 258 | currentWallet!.store(path: path); |
| 259 | |
| 260 | setupBackgroundSync(password, currentWallet!); |
| 261 | storeSync(); |
| 262 | |
| 263 | openedWalletsByPath[path] = currentWallet!; |
| 264 | } |
| 265 | |
| 266 | void restoreWalletFromSpendKeySync( |
| 267 | {required String path, |
| 268 | required String password, |
| 269 | required String seed, |
| 270 | required String language, |
| 271 | required String spendKey, |
| 272 | int nettype = 0, |
| 273 | int restoreHeight = 0}) { |
| 274 | // txhistory = null; |
| 275 | // wptr = monero.WalletManager_createWalletFromKeys( |
| 276 | // wmPtr, |
| 277 | // path: path, |
| 278 | // password: password, |
| 279 | // restoreHeight: restoreHeight, |
| 280 | // addressString: '', |
| 281 | // spendKeyString: spendKey, |
| 282 | // viewKeyString: '', |
| 283 | // nettype: 0, |
| 284 | // ); |
| 285 | |
| 286 | txhistory = null; |
| 287 | final newW = wmPtr.createDeterministicWalletFromSpendKey( |
| 288 | path: path, |
| 289 | password: password, |
| 290 | language: language, |
| 291 | spendKeyString: spendKey, |
| 292 | newWallet: true, // TODO(mrcyjanek): safe to remove |
| 293 | restoreHeight: restoreHeight, |
| 294 | ); |
| 295 | |
| 296 | int status = newW.status(); |
| 297 | |
| 298 | if (status != 0) { |
| 299 | final err = newW.errorString(); |
| 300 | printV("err: $err"); |
| 301 | throw WalletRestoreFromKeysException(message: err); |
| 302 | } |
| 303 | |
| 304 | currentWallet = newW; |
| 305 | |
| 306 | currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed); |
| 307 | |
| 308 | storeSync(); |
| 309 | |
| 310 | setupBackgroundSync(password, currentWallet!); |
| 311 | |
| 312 | openedWalletsByPath[path] = currentWallet!; |
| 313 | _lastOpenedWallet = path; |
| 314 | } |
| 315 | |
| 316 | String _lastOpenedWallet = ""; |
| 317 | |
| 318 | Future<void> restoreWalletFromHardwareWallet( |
| 319 | {required String path, |
| 320 | required String password, |
| 321 | required String deviceName, |
| 322 | int nettype = 0, |
| 323 | int restoreHeight = 0}) async { |
| 324 | txhistory = null; |
| 325 | final wmPtr = MoneroWalletManagerFactory().getWalletManager().ffiAddress(); |
| 326 | final newWptrAddr = await Isolate.run(() { |
| 327 | return monero.WalletManager_createWalletFromDevice(Pointer.fromAddress(wmPtr), |
| 328 | path: path, password: password, restoreHeight: restoreHeight, deviceName: deviceName) |
| 329 | .address; |
| 330 | }); |
| 331 | final newW = MoneroWallet(Pointer.fromAddress(newWptrAddr)); |
| 332 | |
| 333 | final status = newW.status(); |
| 334 | |
| 335 | if (status != 0) { |
| 336 | final error = newW.errorString(); |
| 337 | throw WalletRestoreFromSeedException(message: error); |
| 338 | } |
| 339 | |
| 340 | currentWallet = newW; |
| 341 | currentWallet!.store(path: path); |
| 342 | _lastOpenedWallet = path; |
| 343 | openedWalletsByPath[path] = currentWallet!; |
| 344 | } |
| 345 | |
| 346 | Map<String, Wallet2Wallet> openedWalletsByPath = {}; |
| 347 | |
| 348 | Future<void> loadWallet({required String path, required String password, int nettype = 0}) async { |
| 349 | if (openedWalletsByPath[path] != null) { |
| 350 | txhistory = null; |
| 351 | currentWallet = openedWalletsByPath[path]!; |
| 352 | return; |
| 353 | } |
| 354 | if (currentWallet == null || path != _lastOpenedWallet) { |
| 355 | if (currentWallet != null) { |
| 356 | final addr = currentWallet!.ffiAddress(); |
| 357 | Isolate.run(() { |
| 358 | monero.Wallet_store(Pointer.fromAddress(addr)); |
| 359 | }); |
| 360 | } |
| 361 | txhistory = null; |
| 362 | |
| 363 | /// Get the device type |
| 364 | /// 0: Software Wallet |
| 365 | /// 1: Ledger |
| 366 | /// 2: Trezor |
| 367 | var deviceType = 0; |
| 368 | |
| 369 | if (Platform.isAndroid || Platform.isIOS) { |
| 370 | deviceType = wmPtr.queryWalletDevice( |
| 371 | keysFileName: "$path.keys", |
| 372 | password: password, |
| 373 | kdfRounds: 1, |
| 374 | ); |
| 375 | final status = wmPtr.errorString(); |
| 376 | if (status != "") { |
| 377 | printV("loadWallet:" + status); |
| 378 | // This is most likely closeWallet call leaking error. This is fine. |
| 379 | if (status.contains("failed to save file")) { |
| 380 | printV("loadWallet: error leaked: $status"); |
| 381 | deviceType = 0; |
| 382 | } else { |
| 383 | throw WalletOpeningException(message: status); |
| 384 | } |
| 385 | } |
| 386 | } else { |
| 387 | deviceType = 0; |
| 388 | } |
| 389 | |
| 390 | if (deviceType == 1) { |
| 391 | if (gLedger == null) { |
| 392 | throw Exception("Tried to open a ledger wallet with no ledger connected"); |
| 393 | } |
| 394 | enableLedgerExchange(gLedger!); |
| 395 | } |
| 396 | |
| 397 | final addr = wmPtr.ffiAddress(); |
| 398 | final newWptrAddr = await Isolate.run(() { |
| 399 | return monero.WalletManager_openWallet(Pointer.fromAddress(addr), |
| 400 | path: path, password: password) |
| 401 | .address; |
| 402 | }); |
| 403 | |
| 404 | final newW = MoneroWallet(Pointer.fromAddress(newWptrAddr)); |
| 405 | |
| 406 | int status = newW.status(); |
| 407 | if (status != 0) { |
| 408 | final err = newW.errorString(); |
| 409 | printV("loadWallet:" + err); |
| 410 | throw WalletOpeningException(message: err); |
| 411 | } |
| 412 | if (deviceType == 0) { |
| 413 | setupBackgroundSync(password, newW); |
| 414 | } |
| 415 | |
| 416 | currentWallet = newW; |
| 417 | _lastOpenedWallet = path; |
| 418 | openedWalletsByPath[path] = currentWallet!; |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | void setupBackgroundSync(String password, Wallet2Wallet wallet) { |
| 423 | if (isViewOnlyBySpendKey(wallet)) { |
| 424 | return; |
| 425 | } |
| 426 | wallet.setupBackgroundSync( |
| 427 | backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); |
| 428 | if (wallet.status() != 0) { |
| 429 | // We simply ignore the error. |
| 430 | printV("setupBackgroundSync: ${wallet.errorString()}"); |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | Future<void> openWallet({required String path, required String password, int nettype = 0}) async => |
| 435 | loadWallet(path: path, password: password, nettype: nettype); |
| 436 | |
| 437 | bool isViewOnlyBySpendKey(Wallet2Wallet? wallet) => |
| 438 | int.tryParse((wallet ?? currentWallet!).secretSpendKey()) == 0; |