| 1 | import 'dart:async'; |
| 2 | import 'dart:convert'; |
| 3 | import 'dart:ffi'; |
| 4 | import 'dart:isolate'; |
| 5 | import 'dart:math'; |
| 6 | |
| 7 | import 'package:cw_core/amount/money.dart'; |
| 8 | import 'package:cw_core/crypto_currency.dart'; |
| 9 | import 'package:cw_core/utils/print_verbose.dart'; |
| 10 | import 'package:cw_monero/api/account_list.dart'; |
| 11 | import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; |
| 12 | import 'package:cw_monero/api/get_all_unspent.dart'; |
| 13 | import 'package:monero/monero.dart' as monero; |
| 14 | import 'package:mutex/mutex.dart'; |
| 15 | import 'package:polyseed/polyseed.dart'; |
| 16 | |
| 17 | bool debugMonero = false; |
| 18 | |
| 19 | int getSyncingHeight() { |
| 20 | // final height = monero.MONERO_cw_WalletListener_height(getWlptr()); |
| 21 | if (currentWallet == null) return 0; |
| 22 | final h2 = currentWallet!.blockChainHeight(); |
| 23 | // printV("height: $height / $h2"); |
| 24 | return h2; |
| 25 | } |
| 26 | |
| 27 | bool isNeededToRefresh() { |
| 28 | final wl = getWlptr(); |
| 29 | if (wl == null) return false; |
| 30 | final ret = wl.isNeedToRefresh(); |
| 31 | wl.resetNeedToRefresh(); |
| 32 | return ret; |
| 33 | } |
| 34 | |
| 35 | bool isNewTransactionExist() { |
| 36 | final wlptr = getWlptr(); |
| 37 | if (wlptr == null) return false; |
| 38 | final ret = wlptr.isNewTransactionExist(); |
| 39 | wlptr.resetIsNewTransactionExist(); |
| 40 | return ret; |
| 41 | } |
| 42 | |
| 43 | String getFilename() => currentWallet!.filename(); |
| 44 | |
| 45 | String getSeed() { |
| 46 | // monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); |
| 47 | final cakepolyseed = currentWallet!.getCacheAttribute(key: "cakewallet.seed"); |
| 48 | final cakepassphrase = getPassphrase(); |
| 49 | |
| 50 | final weirdPolyseed = currentWallet!.getPolyseed(passphrase: cakepassphrase); |
| 51 | if (weirdPolyseed != "") return weirdPolyseed; |
| 52 | |
| 53 | if (cakepolyseed != "") { |
| 54 | if (cakepassphrase != "") { |
| 55 | try { |
| 56 | final lang = PolyseedLang.getByPhrase(cakepolyseed); |
| 57 | final coin = PolyseedCoin.POLYSEED_MONERO; |
| 58 | final ps = Polyseed.decode(cakepolyseed, lang, coin); |
| 59 | if (ps.isEncrypted || cakepassphrase == "") return ps.encode(lang, coin); |
| 60 | ps.crypt(cakepassphrase); |
| 61 | return ps.encode(lang, coin); |
| 62 | } catch (e) { |
| 63 | printV(e); |
| 64 | } |
| 65 | } |
| 66 | return cakepolyseed; |
| 67 | } |
| 68 | |
| 69 | final bip39 = currentWallet!.getCacheAttribute(key: "cakewallet.seed.bip39"); |
| 70 | |
| 71 | if (bip39.isNotEmpty) return bip39; |
| 72 | |
| 73 | final legacy = getSeedLegacy(null); |
| 74 | return legacy; |
| 75 | } |
| 76 | |
| 77 | String? getSeedLanguage(String? language) { |
| 78 | switch (language) { |
| 79 | case "Chinese (Traditional)": |
| 80 | language = "Chinese (simplified)"; |
| 81 | break; |
| 82 | case "Chinese (Simplified)": |
| 83 | language = "Chinese (simplified)"; |
| 84 | break; |
| 85 | case "Korean": |
| 86 | language = "English"; |
| 87 | break; |
| 88 | case "Czech": |
| 89 | language = "English"; |
| 90 | break; |
| 91 | case "Japanese": |
| 92 | language = "English"; |
| 93 | break; |
| 94 | } |
| 95 | return language; |
| 96 | } |
| 97 | |
| 98 | String getSeedLegacy(String? language) { |
| 99 | final cakepassphrase = getPassphrase(); |
| 100 | language = getSeedLanguage(language); |
| 101 | var legacy = currentWallet!.seed(seedOffset: cakepassphrase); |
| 102 | if (currentWallet!.status() != 0) { |
| 103 | if (currentWallet!.errorString().contains("seed_language")) { |
| 104 | currentWallet!.setSeedLanguage(language: "English"); |
| 105 | legacy = currentWallet!.seed(seedOffset: cakepassphrase); |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | if (language != null) { |
| 110 | currentWallet!.setSeedLanguage(language: language); |
| 111 | final status = currentWallet!.status(); |
| 112 | if (status != 0) { |
| 113 | final err = currentWallet!.errorString(); |
| 114 | if (legacy.isNotEmpty) { |
| 115 | return "$err\n\n$legacy"; |
| 116 | } |
| 117 | return err; |
| 118 | } |
| 119 | legacy = currentWallet!.seed(seedOffset: cakepassphrase); |
| 120 | } |
| 121 | |
| 122 | if (currentWallet!.status() != 0) { |
| 123 | final err = currentWallet!.errorString(); |
| 124 | if (legacy.isNotEmpty) { |
| 125 | return "$err\n\n$legacy"; |
| 126 | } |
| 127 | return err; |
| 128 | } |
| 129 | return legacy; |
| 130 | } |
| 131 | |
| 132 | String getPassphrase() => currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? ""; |
| 133 | |
| 134 | Map<int, Map<int, Map<int, String>>> addressCache = {}; |
| 135 | |
| 136 | String getAddress({int accountIndex = 0, int addressIndex = 0}) { |
| 137 | // this is a workaround for when we switch the wallet pointer, |
| 138 | // it should never reach UI but should be good enough to prevent gray screen |
| 139 | // or other errors because of forced null check. |
| 140 | if (currentWallet == null) return "<wallet not ready ($accountIndex:$addressIndex)>"; |
| 141 | |
| 142 | // printV("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}"); |
| 143 | // this could be a while loop, but I'm in favor of making it if to not cause freezes |
| 144 | if (currentWallet!.numSubaddresses(accountIndex: accountIndex) - 1 < addressIndex) { |
| 145 | if (currentWallet!.numSubaddressAccounts() < accountIndex) { |
| 146 | currentWallet!.addSubaddressAccount(); |
| 147 | } else { |
| 148 | currentWallet!.addSubaddress(accountIndex: accountIndex); |
| 149 | } |
| 150 | } |
| 151 | addressCache[currentWallet!.ffiAddress()] ??= {}; |
| 152 | addressCache[currentWallet!.ffiAddress()]![accountIndex] ??= {}; |
| 153 | addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex] ??= |
| 154 | currentWallet!.address(accountIndex: accountIndex, addressIndex: addressIndex); |
| 155 | return addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex]!; |
| 156 | } |
| 157 | |
| 158 | Money getFullBalance({int accountIndex = 0}) => |
| 159 | Money.fromInt(currentWallet?.balance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr); |
| 160 | |
| 161 | Money getUnlockedBalance({int accountIndex = 0}) => Money.fromInt( |
| 162 | currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr); |
| 163 | |
| 164 | int getCurrentHeight() => currentWallet?.blockChainHeight() ?? 0; |
| 165 | |
| 166 | int cachedNodeHeight = 0; |
| 167 | bool isHeightRefreshing = false; |
| 168 | int getNodeHeightSync() { |
| 169 | if (isHeightRefreshing == false) { |
| 170 | if (cachedNodeHeight != 0 && getWlptr()?.height() == 1) { |
| 171 | return cachedNodeHeight; |
| 172 | } |
| 173 | (() async { |
| 174 | try { |
| 175 | isHeightRefreshing = true; |
| 176 | final wptrAddress = currentWallet!.ffiAddress(); |
| 177 | cachedNodeHeight = await Isolate.run(() async { |
| 178 | return monero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress)); |
| 179 | }); |
| 180 | } catch (_) { |
| 181 | } finally { |
| 182 | isHeightRefreshing = false; |
| 183 | } |
| 184 | })(); |
| 185 | } |
| 186 | return cachedNodeHeight; |
| 187 | } |
| 188 | |
| 189 | Future<bool> isConnected() async { |
| 190 | final wptrAddress = currentWallet!.ffiAddress(); |
| 191 | return await Isolate.run(() => monero.Wallet_connected(Pointer.fromAddress(wptrAddress))) == 1; |
| 192 | } |
| 193 | |
| 194 | Future<bool> setupNodeSync( |
| 195 | {required String address, |
| 196 | String? login, |
| 197 | String? password, |
| 198 | bool useSSL = false, |
| 199 | bool isLightWallet = false, |
| 200 | String? socksProxyAddress}) async { |
| 201 | printV(''' |
| 202 | { |
| 203 | wptr!, |
| 204 | daemonAddress: $address, |
| 205 | useSsl: $useSSL, |
| 206 | proxyAddress: $socksProxyAddress ?? '', |
| 207 | daemonUsername: $login ?? '', |
| 208 | daemonPassword: $password ?? '' |
| 209 | } |
| 210 | '''); |
| 211 | final addr = currentWallet!.ffiAddress(); |
| 212 | printV("init: start"); |
| 213 | await Isolate.run(() { |
| 214 | monero.Wallet_init(Pointer.fromAddress(addr), |
| 215 | daemonAddress: address, |
| 216 | useSsl: useSSL, |
| 217 | proxyAddress: socksProxyAddress ?? '', |
| 218 | daemonUsername: login ?? '', |
| 219 | daemonPassword: password ?? ''); |
| 220 | }); |
| 221 | printV("init: end"); |
| 222 | |
| 223 | final status = currentWallet!.status(); |
| 224 | |
| 225 | if (status != 0) { |
| 226 | final error = currentWallet!.errorString(); |
| 227 | if (error != "no tx keys found for this txid") { |
| 228 | printV("error: $error"); |
| 229 | throw SetupWalletException(message: error); |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | if (true) { |
| 234 | currentWallet!.init3( |
| 235 | argv0: '', |
| 236 | defaultLogBaseName: 'moneroc', |
| 237 | console: true, |
| 238 | logPath: '', |
| 239 | ); |
| 240 | } |
| 241 | |
| 242 | return status == 0; |
| 243 | } |
| 244 | |
| 245 | void startRefreshSync() { |
| 246 | currentWallet!.startRefresh(); |
| 247 | } |
| 248 | |
| 249 | void setRefreshFromBlockHeight({required int height}) { |
| 250 | currentWallet!.setRefreshFromBlockHeight(refresh_from_block_height: height); |
| 251 | } |
| 252 | |
| 253 | void setRecoveringFromSeed({required bool isRecovery}) { |
| 254 | currentWallet!.setRecoveringFromSeed(recoveringFromSeed: isRecovery); |
| 255 | currentWallet!.store(); |
| 256 | } |
| 257 | |
| 258 | final storeMutex = Mutex(); |
| 259 | |
| 260 | void refresh() { |
| 261 | currentWallet!.refresh(); |
| 262 | } |
| 263 | |
| 264 | int lastStorePointer = 0; |
| 265 | int lastStoreHeight = 0; |
| 266 | void storeSync({bool force = false}) async { |
| 267 | final addr = currentWallet!.ffiAddress(); |
| 268 | final synchronized = await Isolate.run(() { |
| 269 | return monero.Wallet_synchronized(Pointer.fromAddress(addr)); |
| 270 | }); |
| 271 | if (lastStorePointer == addr && |
| 272 | lastStoreHeight + 75000 > currentWallet!.blockChainHeight() && |
| 273 | !synchronized && |
| 274 | !force) { |
| 275 | return; |
| 276 | } |
| 277 | lastStorePointer = currentWallet!.ffiAddress(); |
| 278 | lastStoreHeight = currentWallet!.blockChainHeight(); |
| 279 | await storeMutex.acquire(); |
| 280 | await Isolate.run(() { |
| 281 | monero.Wallet_store(Pointer.fromAddress(addr)); |
| 282 | }); |
| 283 | storeMutex.release(); |
| 284 | } |
| 285 | |
| 286 | void setPasswordSync(String password) { |
| 287 | currentWallet!.setPassword(password: password); |
| 288 | |
| 289 | final status = currentWallet!.status(); |
| 290 | if (status != 0) { |
| 291 | throw Exception(currentWallet!.errorString()); |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | void closeCurrentWallet() { |
| 296 | currentWallet!.stop(); |
| 297 | } |
| 298 | |
| 299 | String getSecretViewKey() => currentWallet?.secretViewKey() ?? ""; |
| 300 | |
| 301 | String getPublicViewKey() => currentWallet?.publicViewKey() ?? ""; |
| 302 | |
| 303 | String getSecretSpendKey() => currentWallet?.secretSpendKey() ?? ""; |
| 304 | |
| 305 | String getPublicSpendKey() => currentWallet?.publicSpendKey() ?? ""; |
| 306 | |
| 307 | class SyncListener { |
| 308 | SyncListener(this.onNewBlock, this.onNewTransaction) |
| 309 | : _cachedBlockchainHeight = 0, |
| 310 | _lastKnownBlockHeight = 0, |
| 311 | _initialSyncHeight = 0 { |
| 312 | _start(); |
| 313 | } |
| 314 | |
| 315 | void Function(int, int, double) onNewBlock; |
| 316 | void Function() onNewTransaction; |
| 317 | |
| 318 | Timer? _updateSyncInfoTimer; |
| 319 | int _cachedBlockchainHeight; |
| 320 | int _lastKnownBlockHeight; |
| 321 | int _initialSyncHeight; |
| 322 | |
| 323 | Future<int> getNodeHeightOrUpdate(int baseHeight) async { |
| 324 | if (_cachedBlockchainHeight < baseHeight || _cachedBlockchainHeight == 0) { |
| 325 | _cachedBlockchainHeight = await getNodeHeight(); |
| 326 | } |
| 327 | |
| 328 | return _cachedBlockchainHeight; |
| 329 | } |
| 330 | |
| 331 | void _start() { |
| 332 | _cachedBlockchainHeight = 0; |
| 333 | _lastKnownBlockHeight = 0; |
| 334 | _initialSyncHeight = 0; |
| 335 | _updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async { |
| 336 | if (isNewTransactionExist()) { |
| 337 | onNewTransaction(); |
| 338 | } |
| 339 | |
| 340 | var syncHeight = getSyncingHeight(); |
| 341 | |
| 342 | if (syncHeight <= 0) { |
| 343 | syncHeight = getCurrentHeight(); |
| 344 | } |
| 345 | |
| 346 | if (_initialSyncHeight <= 0) { |
| 347 | _initialSyncHeight = syncHeight; |
| 348 | } |
| 349 | |
| 350 | // in case when node didn't report new height yet |
| 351 | // it is a workaround for moving height request to another isolate |
| 352 | final nodeHeight = await getNodeHeightOrUpdate(syncHeight); |
| 353 | if (nodeHeight == 0) { |
| 354 | return; |
| 355 | } |
| 356 | final bchHeight = max(nodeHeight, syncHeight); |
| 357 | // printV("syncHeight: $syncHeight, _lastKnownBlockHeight: $_lastKnownBlockHeight, bchHeight: $bchHeight"); |
| 358 | if (_lastKnownBlockHeight == syncHeight) { |
| 359 | return; |
| 360 | } |
| 361 | |
| 362 | _lastKnownBlockHeight = syncHeight; |
| 363 | final track = bchHeight - _initialSyncHeight; |
| 364 | final diff = track - (bchHeight - syncHeight); |
| 365 | final ptc = diff <= 0 ? 0.0 : diff / track; |
| 366 | final left = bchHeight - syncHeight; |
| 367 | |
| 368 | if ((syncHeight < 0 || left < 0)) { |
| 369 | printV("not calling onNewBlock: syncHeight: $syncHeight, left: $left"); |
| 370 | return; |
| 371 | } |
| 372 | |
| 373 | // 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents; |
| 374 | onNewBlock.call(syncHeight, left, ptc); |
| 375 | }); |
| 376 | } |
| 377 | |
| 378 | void stop() => _updateSyncInfoTimer?.cancel(); |
| 379 | } |
| 380 | |
| 381 | SyncListener setListeners( |
| 382 | void Function(int, int, double) onNewBlock, void Function() onNewTransaction) { |
| 383 | final listener = SyncListener(onNewBlock, onNewTransaction); |
| 384 | // setListenerNative(); |
| 385 | return listener; |
| 386 | } |
| 387 | |
| 388 | void onStartup() {} |
| 389 | |
| 390 | void _storeSync(Object _) => storeSync(); |
| 391 | |
| 392 | Future<bool> _setupNodeSync(Map<String, Object?> args) async { |
| 393 | final address = args['address'] as String; |
| 394 | final login = (args['login'] ?? '') as String; |
| 395 | final password = (args['password'] ?? '') as String; |
| 396 | final useSSL = args['useSSL'] as bool; |
| 397 | final isLightWallet = args['isLightWallet'] as bool; |
| 398 | final socksProxyAddress = (args['socksProxyAddress'] ?? '') as String; |
| 399 | |
| 400 | return setupNodeSync( |
| 401 | address: address, |
| 402 | login: login, |
| 403 | password: password, |
| 404 | useSSL: useSSL, |
| 405 | isLightWallet: isLightWallet, |
| 406 | socksProxyAddress: socksProxyAddress); |
| 407 | } |
| 408 | |
| 409 | void startRefresh() => startRefreshSync(); |
| 410 | |
| 411 | Future<void> store() async => _storeSync(0); |
| 412 | |
| 413 | Future<int> getNodeHeight() async => getNodeHeightSync(); |
| 414 | |
| 415 | void rescanBlockchainAsync() => currentWallet!.rescanBlockchainAsync(); |
| 416 | |
| 417 | String getSubaddressLabel(int accountIndex, int addressIndex) { |
| 418 | return currentWallet!.getSubaddressLabel(accountIndex: accountIndex, addressIndex: addressIndex); |
| 419 | } |
| 420 | |
| 421 | Future setTrustedDaemon(bool trusted) async => currentWallet!.setTrustedDaemon(arg: trusted); |
| 422 | |
| 423 | Future<bool> trustedDaemon() async => currentWallet!.trustedDaemon(); |
| 424 | |
| 425 | String signMessage(String message, {String address = ""}) => |
| 426 | currentWallet!.signMessage(message: message, address: address); |
| 427 | |
| 428 | bool verifyMessage(String message, String address, String signature) => |
| 429 | currentWallet!.verifySignedMessage(message: message, address: address, signature: signature); |
| 430 | |
| 431 | Map<String, List<int>> debugCallLength() => monero.debugCallLength; |
| 432 | |
| 433 | Map<String, dynamic> getWalletCacheDebug() { |
| 434 | try { |
| 435 | final jsonString = |
| 436 | monero.MONERO_Wallet_serializeCacheToJson(Pointer.fromAddress(currentWallet!.ffiAddress())); |
| 437 | final blob = json.decode(jsonString); |
| 438 | blob['cake:unspent'] = getAllUnspent(); |
| 439 | return blob; |
| 440 | } catch (e) { |
| 441 | return { |
| 442 | "error": e.toString(), |
| 443 | }; |
| 444 | } |
| 445 | } |