import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.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/exceptions/creation_transaction_exception.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/monero/monero.dart' as xmr_dart; import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output; import 'package:isar/isar.dart'; import 'package:mutex/mutex.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/cryptonote_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import 'package:tuple/tuple.dart'; class MoneroWallet extends CryptonoteWallet with MultiAddressInterface { MoneroWallet(CryptoCurrencyNetwork network) : super(Monero(network)); @override FilterOperation? get changeAddressFilterOperation => null; @override FilterOperation? get receivingAddressFilterOperation => null; final prepareSendMutex = Mutex(); final estimateFeeMutex = Mutex(); bool _hasCalledExit = false; WalletService? cwWalletService; KeyService? cwKeysStorage; MoneroWalletBase? cwWalletBase; WalletCreationService? cwWalletCreationService; Timer? _autoSaveTimer; bool _txRefreshLock = false; int _lastCheckedHeight = -1; int _txCount = 0; int _currentKnownChainHeight = 0; double _highestPercentCached = 0; @override Future estimateFeeFor(Amount amount, int feeRate) async { MoneroTransactionPriority priority; FeeRateType feeRateType = FeeRateType.slow; switch (feeRate) { case 1: priority = MoneroTransactionPriority.regular; feeRateType = FeeRateType.average; break; case 2: priority = MoneroTransactionPriority.medium; feeRateType = FeeRateType.average; break; case 3: priority = MoneroTransactionPriority.fast; feeRateType = FeeRateType.fast; break; case 4: priority = MoneroTransactionPriority.fastest; feeRateType = FeeRateType.fast; break; case 0: default: priority = MoneroTransactionPriority.slow; feeRateType = FeeRateType.slow; break; } dynamic approximateFee; await estimateFeeMutex.protect(() async { { try { final data = await prepareSend( txData: TxData( recipients: [ // This address is only used for getting an approximate fee, never for sending ( address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", amount: amount, isChange: false, ), ], feeRateType: feeRateType, ), ); approximateFee = data.fee!; // unsure why this delay? await Future.delayed(const Duration(milliseconds: 500)); } catch (e) { approximateFee = cwWalletBase!.calculateEstimatedFee( priority, amount.raw.toInt(), ); } } }); if (approximateFee is Amount) { return approximateFee as Amount; } else { return Amount( rawValue: BigInt.from(approximateFee as int), fractionDigits: cryptoCurrency.fractionDigits, ); } } @override Future get fees async => FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 15, numberOfBlocksSlow: 20, fast: MoneroTransactionPriority.fast.raw!, medium: MoneroTransactionPriority.regular.raw!, slow: MoneroTransactionPriority.slow.raw!, ); @override Future pingCheck() async { return await cwWalletBase?.isConnected() ?? false; } @override Future updateBalance() async { final total = await _totalBalance; final available = await _availableBalance; final balance = Balance( total: total, spendable: available, blockedTotal: Amount( rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits, ), pendingSpendable: total - available, ); await info.updateBalance(newBalance: balance, isar: mainDB.isar); } @override Future updateChainHeight() async { await info.updateCachedChainHeight( newHeight: _currentKnownChainHeight, isar: mainDB.isar, ); } @override Future updateNode() async { final node = getCurrentNode(); final host = Uri.parse(node.host).host; await cwWalletBase?.connectToNode( node: Node( uri: "$host:${node.port}", type: WalletType.monero, trusted: node.trusted ?? false, ), ); // TODO: is this sync call needed? Do we need to notify ui here? // await cwWalletBase?.startSync(); // if (shouldRefresh) { // await refresh(); // } } @override Future updateTransactions() async { await cwWalletBase!.updateTransactions(); final transactions = cwWalletBase?.transactionHistory!.transactions; // final cachedTransactions = // DB.instance.get(boxName: walletId, key: 'latest_tx_model') // as TransactionData?; // int latestTxnBlockHeight = // DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") // as int? ?? // 0; // // final txidsList = DB.instance // .get(boxName: walletId, key: "cachedTxids") as List? ?? // []; // // final Set cachedTxids = Set.from(txidsList); // TODO: filter to skip cached + confirmed txn processing in next step // final unconfirmedCachedTransactions = // cachedTransactions?.getAllTransactions() ?? {}; // unconfirmedCachedTransactions // .removeWhere((key, value) => value.confirmedStatus); // // if (cachedTransactions != null) { // for (final tx in allTxHashes.toList(growable: false)) { // final txHeight = tx["height"] as int; // if (txHeight > 0 && // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { // allTxHashes.remove(tx); // } // } // } // } final List> txnsData = []; if (transactions != null) { for (var tx in transactions.entries) { Address? address; TransactionType type; if (tx.value.direction == TransactionDirection.incoming) { final addressInfo = tx.value.additionalInfo; final addressString = cwWalletBase?.getTransactionAddress( addressInfo!['accountIndex'] as int, addressInfo['addressIndex'] as int, ); if (addressString != null) { address = await mainDB .getAddresses(walletId) .filter() .valueEqualTo(addressString) .findFirst(); } type = TransactionType.incoming; } else { // txn.address = ""; type = TransactionType.outgoing; } final txn = Transaction( walletId: walletId, txid: tx.value.id, timestamp: (tx.value.date.millisecondsSinceEpoch ~/ 1000), type: type, subType: TransactionSubType.none, amount: tx.value.amount ?? 0, amountString: Amount( rawValue: BigInt.from(tx.value.amount ?? 0), fractionDigits: cryptoCurrency.fractionDigits, ).toJsonString(), fee: tx.value.fee ?? 0, height: tx.value.height, isCancelled: false, isLelantus: false, slateId: null, otherData: null, nonce: null, inputs: [], outputs: [], numberOfMessages: null, ); txnsData.add(Tuple2(txn, address)); } } await mainDB.addNewTransactionData(txnsData, walletId); } @override Future init() async { cwWalletService = xmr_dart.monero .createMoneroWalletService(DB.instance.moneroWalletInfoBox); cwKeysStorage = KeyService(secureStorageInterface); if (await cwWalletService!.isWalletExit(walletId)) { String? password; try { password = await cwKeysStorage!.getWalletPassword(walletName: walletId); } catch (e, s) { throw Exception("Password not found $e, $s"); } cwWalletBase = (await cwWalletService!.openWallet(walletId, password)) as MoneroWalletBase; unawaited(_start()); } else { WalletInfo walletInfo; WalletCredentials credentials; try { String name = walletId; final dirPath = await _pathForWalletDir(name: name, type: WalletType.monero); final path = await _pathForWallet(name: name, type: WalletType.monero); credentials = xmr_dart.monero.createMoneroNewWalletCredentials( name: name, language: "English", ); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, WalletType.monero), name: name, type: WalletType.monero, isRecovery: false, restoreHeight: credentials.height ?? 0, date: DateTime.now(), path: path, dirPath: dirPath, // TODO: find out what to put for address address: '', ); credentials.walletInfo = walletInfo; final _walletCreationService = WalletCreationService( secureStorage: secureStorageInterface, walletService: cwWalletService, keyService: cwKeysStorage, ); _walletCreationService.type = WalletType.monero; // To restore from a seed final wallet = await _walletCreationService.create(credentials); // subtract a couple days to ensure we have a buffer for SWB final bufferedCreateHeight = xmr_dart.monero.getHeigthByDate( date: DateTime.now().subtract(const Duration(days: 2))); await info.updateRestoreHeight( newRestoreHeight: bufferedCreateHeight, isar: mainDB.isar, ); // special case for xmr/wow. Normally mnemonic + passphrase is saved // before wallet.init() is called await secureStorageInterface.write( key: Wallet.mnemonicKey(walletId: walletId), value: wallet.seed.trim(), ); await secureStorageInterface.write( key: Wallet.mnemonicPassphraseKey(walletId: walletId), value: "", ); walletInfo.restoreHeight = bufferedCreateHeight; walletInfo.address = wallet.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); cwWalletBase?.close(); cwWalletBase = wallet as MoneroWalletBase; unawaited(_start()); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); cwWalletBase?.close(); } await updateNode(); await cwWalletBase?.startSync(); // cwWalletBase?.close(); } return super.init(); } Future _start() async { cwWalletBase?.onNewBlock = onNewBlock; cwWalletBase?.onNewTransaction = onNewTransaction; cwWalletBase?.syncStatusChanged = syncStatusChanged; if (cwWalletBase != null && !(await cwWalletBase!.isConnected())) { final node = getCurrentNode(); final host = Uri.parse(node.host).host; await cwWalletBase?.connectToNode( node: Node( uri: "$host:${node.port}", type: WalletType.monero, trusted: node.trusted ?? false, ), ); } await cwWalletBase?.startSync(); unawaited(refresh()); _autoSaveTimer?.cancel(); _autoSaveTimer = Timer.periodic( const Duration(seconds: 193), (_) async => await cwWalletBase?.save(), ); } @override Future exit() async { if (!_hasCalledExit) { _hasCalledExit = true; cwWalletBase?.onNewBlock = null; cwWalletBase?.onNewTransaction = null; cwWalletBase?.syncStatusChanged = null; _autoSaveTimer?.cancel(); await cwWalletBase?.save(prioritySave: true); cwWalletBase?.close(); } } @override Future generateNewReceivingAddress() async { try { final currentReceiving = await getCurrentReceivingAddress(); final newReceivingIndex = currentReceiving == null ? 0 : currentReceiving.derivationIndex + 1; final newReceivingAddress = _addressFor(index: newReceivingIndex); // Add that new receiving address await mainDB.putAddress(newReceivingAddress); await info.updateReceivingAddress( newAddress: newReceivingAddress.value, isar: mainDB.isar, ); } catch (e, s) { Logging.instance.log( "Exception in generateNewAddress(): $e\n$s", level: LogLevel.Error, ); } } @override Future checkReceivingAddressForTransactions() async { try { int highestIndex = -1; for (var element in cwWalletBase!.transactionHistory!.transactions!.entries) { if (element.value.direction == TransactionDirection.incoming) { int curAddressIndex = element.value.additionalInfo!['addressIndex'] as int; if (curAddressIndex > highestIndex) { highestIndex = curAddressIndex; } } } // Check the new receiving index final currentReceiving = await getCurrentReceivingAddress(); final curIndex = currentReceiving?.derivationIndex ?? -1; if (highestIndex >= curIndex) { // First increment the receiving index final newReceivingIndex = curIndex + 1; // Use new index to derive a new receiving address final newReceivingAddress = _addressFor(index: newReceivingIndex); final existing = await mainDB .getAddresses(walletId) .filter() .valueEqualTo(newReceivingAddress.value) .findFirst(); if (existing == null) { // Add that new change address await mainDB.putAddress(newReceivingAddress); } else { // we need to update the address await mainDB.updateAddress(existing, newReceivingAddress); } // keep checking until address with no tx history is set as current await checkReceivingAddressForTransactions(); } } on SocketException catch (se, s) { Logging.instance.log( "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", level: LogLevel.Error); return; } catch (e, s) { Logging.instance.log( "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", level: LogLevel.Error); rethrow; } } @override Future refresh() async { // Awaiting this lock could be dangerous. // Since refresh is periodic (generally) if (refreshMutex.isLocked) { return; } // this acquire should be almost instant due to above check. // Slight possibility of race but should be irrelevant await refreshMutex.acquire(); GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.syncing, walletId, info.coin, ), ); await updateTransactions(); await updateBalance(); await checkReceivingAddressForTransactions(); if (cwWalletBase?.syncStatus is SyncedSyncStatus) { refreshMutex.release(); GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.synced, walletId, info.coin, ), ); } } @override Future recover({required bool isRescan}) async { if (isRescan) { await refreshMutex.protect(() async { // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); var restoreHeight = cwWalletBase?.walletInfo.restoreHeight; _highestPercentCached = 0; await cwWalletBase?.rescan(height: restoreHeight); }); await refresh(); return; } await refreshMutex.protect(() async { final mnemonic = await getMnemonic(); final seedLength = mnemonic.trim().split(" ").length; if (seedLength != 25) { throw Exception("Invalid monero mnemonic length found: $seedLength"); } try { int height = info.restoreHeight; // 25 word seed. TODO validate if (height == 0) { height = xmr_dart.monero.getHeigthByDate( date: DateTime.now().subtract( const Duration( // subtract a couple days to ensure we have a buffer for SWB days: 2, ), ), ); } // TODO: info.updateRestoreHeight // await DB.instance // .put(boxName: walletId, key: "restoreHeight", value: height); cwWalletService = xmr_dart.monero .createMoneroWalletService(DB.instance.moneroWalletInfoBox); cwKeysStorage = KeyService(secureStorageInterface); WalletInfo walletInfo; WalletCredentials credentials; String name = walletId; final dirPath = await _pathForWalletDir(name: name, type: WalletType.monero); final path = await _pathForWallet(name: name, type: WalletType.monero); credentials = xmr_dart.monero.createMoneroRestoreWalletFromSeedCredentials( name: name, height: height, mnemonic: mnemonic.trim(), ); try { walletInfo = WalletInfo.external( id: WalletBase.idFor(name, WalletType.monero), name: name, type: WalletType.monero, isRecovery: false, restoreHeight: credentials.height ?? 0, date: DateTime.now(), path: path, dirPath: dirPath, // TODO: find out what to put for address address: ''); credentials.walletInfo = walletInfo; cwWalletCreationService = WalletCreationService( secureStorage: secureStorageInterface, walletService: cwWalletService, keyService: cwKeysStorage, ); cwWalletCreationService!.changeWalletType(); // To restore from a seed final wallet = await cwWalletCreationService!.restoreFromSeed(credentials); walletInfo.address = wallet.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); cwWalletBase?.close(); cwWalletBase = wallet as MoneroWalletBase; } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Fatal); } await updateNode(); await cwWalletBase?.rescan(height: credentials.height); cwWalletBase?.close(); } catch (e, s) { Logging.instance.log( "Exception rethrown from recoverFromMnemonic(): $e\n$s", level: LogLevel.Error); rethrow; } }); } @override Future prepareSend({required TxData txData}) async { try { final feeRate = txData.feeRateType; if (feeRate is FeeRateType) { MoneroTransactionPriority feePriority; switch (feeRate) { case FeeRateType.fast: feePriority = MoneroTransactionPriority.fast; break; case FeeRateType.average: feePriority = MoneroTransactionPriority.regular; break; case FeeRateType.slow: feePriority = MoneroTransactionPriority.slow; break; default: throw ArgumentError("Invalid use of custom fee"); } Future? awaitPendingTransaction; try { // check for send all bool isSendAll = false; final balance = await _availableBalance; if (txData.amount! == balance && txData.recipients!.first.amount == balance) { isSendAll = true; } List outputs = []; for (final recipient in txData.recipients!) { final output = monero_output.Output(cwWalletBase!); output.address = recipient.address; output.sendAll = isSendAll; String amountToSend = recipient.amount.decimal.toString(); output.setCryptoAmount(amountToSend); } final tmp = xmr_dart.monero.createMoneroTransactionCreationCredentials( outputs: outputs, priority: feePriority, ); await prepareSendMutex.protect(() async { awaitPendingTransaction = cwWalletBase!.createTransaction(tmp); }); } catch (e, s) { Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", level: LogLevel.Warning); } PendingMoneroTransaction pendingMoneroTransaction = await (awaitPendingTransaction!) as PendingMoneroTransaction; final realFee = Amount.fromDecimal( Decimal.parse(pendingMoneroTransaction.feeFormatted), fractionDigits: cryptoCurrency.fractionDigits, ); return txData.copyWith( fee: realFee, pendingMoneroTransaction: pendingMoneroTransaction, ); } else { throw ArgumentError("Invalid fee rate argument provided!"); } } catch (e, s) { Logging.instance.log("Exception rethrown from prepare send(): $e\n$s", level: LogLevel.Info); if (e.toString().contains("Incorrect unlocked balance")) { throw Exception("Insufficient balance!"); } else if (e is CreationTransactionException) { throw Exception("Insufficient funds to pay for transaction fee!"); } else { throw Exception("Transaction failed with error code $e"); } } } @override Future confirmSend({required TxData txData}) async { try { try { await txData.pendingMoneroTransaction!.commit(); Logging.instance.log( "transaction ${txData.pendingMoneroTransaction!.id} has been sent", level: LogLevel.Info); return txData.copyWith(txid: txData.pendingMoneroTransaction!.id); } catch (e, s) { Logging.instance.log("${info.name} monero confirmSend: $e\n$s", level: LogLevel.Error); rethrow; } } catch (e, s) { Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", level: LogLevel.Info); rethrow; } } // ====== private ============================================================ void onNewBlock({required int height, required int blocksLeft}) { _currentKnownChainHeight = height; updateChainHeight(); _refreshTxDataHelper(); } void onNewTransaction() { // call this here? GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( "New data found in $walletId ${info.name} in background!", walletId, ), ); } void syncStatusChanged() async { final syncStatus = cwWalletBase?.syncStatus; if (syncStatus != null) { if (syncStatus.progress() == 1 && refreshMutex.isLocked) { refreshMutex.release(); } WalletSyncStatus? status; xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(true); if (syncStatus is SyncingSyncStatus) { final int blocksLeft = syncStatus.blocksLeft; // ensure at least 1 to prevent math errors final int height = max(1, syncStatus.height); final nodeHeight = height + blocksLeft; _currentKnownChainHeight = nodeHeight; final percent = height / nodeHeight; final highest = max(_highestPercentCached, percent); // update cached if (_highestPercentCached < percent) { _highestPercentCached = percent; } GlobalEventBus.instance.fire( RefreshPercentChangedEvent( highest, walletId, ), ); GlobalEventBus.instance.fire( BlocksRemainingEvent( blocksLeft, walletId, ), ); } else if (syncStatus is SyncedSyncStatus) { status = WalletSyncStatus.synced; } else if (syncStatus is NotConnectedSyncStatus) { status = WalletSyncStatus.unableToSync; xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); } else if (syncStatus is StartingSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( RefreshPercentChangedEvent( _highestPercentCached, walletId, ), ); } else if (syncStatus is FailedSyncStatus) { status = WalletSyncStatus.unableToSync; xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); } else if (syncStatus is ConnectingSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( RefreshPercentChangedEvent( _highestPercentCached, walletId, ), ); } else if (syncStatus is ConnectedSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( RefreshPercentChangedEvent( _highestPercentCached, walletId, ), ); } else if (syncStatus is LostConnectionSyncStatus) { status = WalletSyncStatus.unableToSync; xmrAndWowSyncSpecificFunctionThatShouldBeGottenRidOfInTheFuture(false); } if (status != null) { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( status, walletId, info.coin, ), ); } } } Address _addressFor({required int index, int account = 0}) { String address = cwWalletBase!.getTransactionAddress(account, index); final newReceivingAddress = Address( walletId: walletId, derivationIndex: index, derivationPath: null, value: address, publicKey: [], type: AddressType.cryptonote, subType: AddressSubType.receiving, ); return newReceivingAddress; } Future get _availableBalance async { try { int runningBalance = 0; for (final entry in cwWalletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } return Amount( rawValue: BigInt.from(runningBalance), fractionDigits: cryptoCurrency.fractionDigits, ); } catch (_) { return info.cachedBalance.spendable; } } Future get _totalBalance async { try { final balanceEntries = cwWalletBase?.balance?.entries; if (balanceEntries != null) { int bal = 0; for (var element in balanceEntries) { bal = bal + element.value.fullBalance; } return Amount( rawValue: BigInt.from(bal), fractionDigits: cryptoCurrency.fractionDigits, ); } else { final transactions = cwWalletBase!.transactionHistory!.transactions; int transactionBalance = 0; for (var tx in transactions!.entries) { if (tx.value.direction == TransactionDirection.incoming) { transactionBalance += tx.value.amount!; } else { transactionBalance += -tx.value.amount! - tx.value.fee!; } } return Amount( rawValue: BigInt.from(transactionBalance), fractionDigits: cryptoCurrency.fractionDigits, ); } } catch (_) { return info.cachedBalance.total; } } Future _refreshTxDataHelper() async { if (_txRefreshLock) return; _txRefreshLock = true; final syncStatus = cwWalletBase?.syncStatus; if (syncStatus != null && syncStatus is SyncingSyncStatus) { final int blocksLeft = syncStatus.blocksLeft; final tenKChange = blocksLeft ~/ 10000; // only refresh transactions periodically during a sync if (_lastCheckedHeight == -1 || tenKChange < _lastCheckedHeight) { _lastCheckedHeight = tenKChange; await _refreshTxData(); } } else { await _refreshTxData(); } _txRefreshLock = false; } Future _refreshTxData() async { await updateTransactions(); final count = await mainDB.getTransactions(walletId).count(); if (count > _txCount) { _txCount = count; await updateBalance(); GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( "New transaction data found in $walletId ${info.name}!", walletId, ), ); } } Future _pathForWalletDir({ required String name, required WalletType type, }) async { Directory root = await StackFileSystem.applicationRootDirectory(); final prefix = walletTypeToString(type).toLowerCase(); final walletsDir = Directory('${root.path}/wallets'); final walletDire = Directory('${walletsDir.path}/$prefix/$name'); if (!walletDire.existsSync()) { walletDire.createSync(recursive: true); } return walletDire.path; } Future _pathForWallet({ required String name, required WalletType type, }) async => await _pathForWalletDir(name: name, type: type) .then((path) => '$path/$name'); @override Future checkChangeAddressForTransactions() async { // do nothing } @override Future generateNewChangeAddress() async { // do nothing } // TODO: [prio=med/low] is this required? // bool _isActive = false; // @override // void Function(bool)? get onIsActiveWalletChanged => (isActive) async { // if (_isActive == isActive) { // return; // } // _isActive = isActive; // // if (isActive) { // _hasCalledExit = false; // String? password; // try { // password = // await keysStorage?.getWalletPassword(walletName: _walletId); // } catch (e, s) { // throw Exception("Password not found $e, $s"); // } // walletBase = (await walletService?.openWallet(_walletId, password!)) // as MoneroWalletBase?; // // walletBase!.onNewBlock = onNewBlock; // walletBase!.onNewTransaction = onNewTransaction; // walletBase!.syncStatusChanged = syncStatusChanged; // // if (!(await walletBase!.isConnected())) { // final node = await _getCurrentNode(); // final host = Uri.parse(node.host).host; // await walletBase?.connectToNode( // node: Node( // uri: "$host:${node.port}", // type: WalletType.Monero, // trusted: node.trusted ?? false, // ), // ); // } // await walletBase?.startSync(); // await refresh(); // _autoSaveTimer?.cancel(); // _autoSaveTimer = Timer.periodic( // const Duration(seconds: 193), // (_) async => await walletBase?.save(), // ); // } else { // await exit(); // } // }; }