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/cupertino.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'; 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/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/coin_service.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/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:tuple/tuple.dart'; const int MINIMUM_CONFIRMATIONS = 10; class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { MoneroWallet({ required String walletId, required String walletName, required Coin coin, required SecureStorageInterface secureStorage, Prefs? prefs, MainDB? mockableOverride, }) { _walletId = walletId; _walletName = walletName; _coin = coin; _secureStorage = secureStorage; _prefs = prefs ?? Prefs.instance; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); } late final String _walletId; late final Coin _coin; late final SecureStorageInterface _secureStorage; late final Prefs _prefs; late String _walletName; bool _shouldAutoSync = false; bool _isConnected = false; bool _hasCalledExit = false; bool refreshMutex = false; bool longMutex = false; WalletService? walletService; KeyService? keysStorage; MoneroWalletBase? walletBase; WalletCreationService? _walletCreationService; Timer? _autoSaveTimer; Future get _currentReceivingAddress => db.getAddresses(walletId).sortByDerivationIndexDesc().findFirst(); Future? _feeObject; Mutex prepareSendMutex = Mutex(); Mutex estimateFeeMutex = Mutex(); @override set isFavorite(bool markFavorite) { _isFavorite = markFavorite; updateCachedIsFavorite(markFavorite); } @override bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); bool? _isFavorite; @override bool get shouldAutoSync => _shouldAutoSync; @override set shouldAutoSync(bool shouldAutoSync) { if (_shouldAutoSync != shouldAutoSync) { _shouldAutoSync = shouldAutoSync; // xmr wallets cannot be open at the same time // leave following commented out for now // if (!shouldAutoSync) { // timer?.cancel(); // moneroAutosaveTimer?.cancel(); // timer = null; // moneroAutosaveTimer = null; // stopNetworkAlivePinging(); // } else { // startNetworkAlivePinging(); // // Walletbase needs to be open for this to work // refresh(); // } } } @override String get walletName => _walletName; // setter for updating on rename @override set walletName(String newName) => _walletName = newName; @override Coin get coin => _coin; @override Future confirmSend({required Map txData}) async { try { Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); final pendingMoneroTransaction = txData['pendingMoneroTransaction'] as PendingMoneroTransaction; try { await pendingMoneroTransaction.commit(); Logging.instance.log( "transaction ${pendingMoneroTransaction.id} has been sent", level: LogLevel.Info); return pendingMoneroTransaction.id; } catch (e, s) { Logging.instance.log("$walletName 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; } } @override Future get currentReceivingAddress async => (await _currentReceivingAddress)?.value ?? (await _generateAddressForChain(0, 0)).value; @override Future estimateFeeFor(Amount amount, int feeRate) async { MoneroTransactionPriority priority; switch (feeRate) { case 1: priority = MoneroTransactionPriority.regular; break; case 2: priority = MoneroTransactionPriority.medium; break; case 3: priority = MoneroTransactionPriority.fast; break; case 4: priority = MoneroTransactionPriority.fastest; break; case 0: default: priority = MoneroTransactionPriority.slow; break; } final fee = walletBase!.calculateEstimatedFee(priority, amount.raw.toInt()); return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals); } @override Future exit() async { if (!_hasCalledExit) { walletBase?.onNewBlock = null; walletBase?.onNewTransaction = null; walletBase?.syncStatusChanged = null; _hasCalledExit = true; _autoSaveTimer?.cancel(); await walletBase?.save(prioritySave: true); walletBase?.close(); } } @override Future get fees => _feeObject ??= _getFees(); @override Future fullRescan( int maxUnusedAddressGap, int maxNumberOfIndexesToCheck, ) async { // clear blockchain info await db.deleteWalletBlockchainData(walletId); var restoreHeight = walletBase?.walletInfo.restoreHeight; highestPercentCached = 0; await walletBase?.rescan(height: restoreHeight); await refresh(); } @override Future generateNewAddress() async { try { final currentReceiving = await _currentReceivingAddress; final newReceivingIndex = currentReceiving!.derivationIndex + 1; // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain( 0, newReceivingIndex, ); // Add that new receiving address await db.putAddress(newReceivingAddress); return true; } catch (e, s) { Logging.instance.log( "Exception rethrown from generateNewAddress(): $e\n$s", level: LogLevel.Error); return false; } } @override bool get hasCalledExit => _hasCalledExit; @override Future initializeExisting() async { Logging.instance.log( "initializeExisting() ${coin.prettyName} wallet $walletName...", level: LogLevel.Info, ); if (getCachedId() == null) { throw Exception( "Attempted to initialize an existing wallet using an unknown wallet ID!"); } walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); keysStorage = KeyService(_secureStorage); await _prefs.init(); // final data = // DB.instance.get(boxName: walletId, key: "latest_tx_model") // as TransactionData?; // if (data != null) { // _transactionData = Future(() => data); // } String password; try { password = await keysStorage!.getWalletPassword(walletName: _walletId); } catch (_) { throw Exception("Monero password not found for $walletName"); } walletBase = (await walletService!.openWallet(_walletId, password)) as MoneroWalletBase; // await _checkCurrentReceivingAddressesForTransactions(); Logging.instance.log( "Opened existing ${coin.prettyName} wallet $walletName", level: LogLevel.Info, ); } @override Future initializeNew() async { await _prefs.init(); // this should never fail if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { throw Exception( "Attempted to overwrite mnemonic on generate new wallet!"); } walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); keysStorage = KeyService(_secureStorage); 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 = monero.createMoneroNewWalletCredentials( name: name, language: "English", ); // subtract a couple days to ensure we have a buffer for SWB final bufferedCreateHeight = monero.getHeigthByDate( date: DateTime.now().subtract(const Duration(days: 2))); await DB.instance.put( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, WalletType.monero), name: name, type: WalletType.monero, isRecovery: false, restoreHeight: bufferedCreateHeight, date: DateTime.now(), path: path, dirPath: dirPath, // TODO: find out what to put for address address: ''); credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( secureStorage: _secureStorage, walletService: walletService, keyService: keysStorage, ); _walletCreationService?.changeWalletType(); // To restore from a seed final wallet = await _walletCreationService?.create(credentials); await _secureStorage.write( key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); await _secureStorage.write( key: '${_walletId}_mnemonicPassphrase', value: "", ); walletInfo.address = wallet?.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); walletBase?.close(); walletBase = wallet as MoneroWalletBase; // walletBase!.onNewBlock = onNewBlock; // walletBase!.onNewTransaction = onNewTransaction; // walletBase!.syncStatusChanged = syncStatusChanged; } catch (e, s) { //todo: come back to this debugPrint("some nice searchable string thing"); debugPrint(e.toString()); debugPrint(s.toString()); walletBase?.close(); } 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 Future.wait([ updateCachedId(walletId), updateCachedIsFavorite(false), ]); // Generate and add addresses to relevant arrays final initialReceivingAddress = await _generateAddressForChain(0, 0); // final initialChangeAddress = await _generateAddressForChain(1, 0); await db.putAddress(initialReceivingAddress); walletBase?.close(); Logging.instance .log("initializeNew for $walletName $walletId", level: LogLevel.Info); } @override bool get isConnected => _isConnected; @override bool get isRefreshing => refreshMutex; @override // not used in xmr Future get maxFee => throw UnimplementedError(); @override Future> get mnemonic async { final _mnemonicString = await mnemonicString; if (_mnemonicString == null) { return []; } final List data = _mnemonicString.split(' '); return data; } @override Future get mnemonicString => _secureStorage.read(key: '${_walletId}_mnemonic'); @override Future get mnemonicPassphrase => _secureStorage.read( key: '${_walletId}_mnemonicPassphrase', ); @override Future> prepareSend({ required String address, required Amount amount, Map? args, }) async { String toAddress = address; try { final feeRate = args?["feeRate"]; 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; } Future? awaitPendingTransaction; try { // check for send all bool isSendAll = false; final balance = await _availableBalance; if (amount == balance) { isSendAll = true; } Logging.instance .log("$toAddress $amount $args", level: LogLevel.Info); String amountToSend = amount.decimal.toString(); Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); monero_output.Output output = monero_output.Output(walletBase!); output.address = toAddress; output.sendAll = isSendAll; output.setCryptoAmount(amountToSend); List outputs = [output]; Object tmp = monero.createMoneroTransactionCreationCredentials( outputs: outputs, priority: feePriority); await prepareSendMutex.protect(() async { awaitPendingTransaction = walletBase!.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 int realFee = Amount.fromDecimal( Decimal.parse(pendingMoneroTransaction.feeFormatted), fractionDigits: coin.decimals, ).raw.toInt(); Map txData = { "pendingMoneroTransaction": pendingMoneroTransaction, "fee": realFee, "addresss": toAddress, "recipientAmt": amount, }; Logging.instance.log("prepare send: $txData", level: LogLevel.Info); return txData; } 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 recoverFromMnemonic({ required String mnemonic, String? mnemonicPassphrase, // not used at the moment required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height, }) async { await _prefs.init(); longMutex = true; final start = DateTime.now(); try { // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag"); // if (!integrationTestFlag) { // final features = await electrumXClient.getServerFeatures(); // Logging.instance.log("features: $features"); // if (_networkType == BasicNetworkType.main) { // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { // throw Exception("genesis hash does not match main net!"); // } // } else if (_networkType == BasicNetworkType.test) { // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { // throw Exception("genesis hash does not match test net!"); // } // } // } // check to make sure we aren't overwriting a mnemonic // this should never fail if ((await mnemonicString) != null || (await this.mnemonicPassphrase) != null) { longMutex = false; throw Exception("Attempted to overwrite mnemonic on restore!"); } await _secureStorage.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); await _secureStorage.write( key: '${_walletId}_mnemonicPassphrase', value: mnemonicPassphrase ?? "", ); await DB.instance .put(boxName: walletId, key: "restoreHeight", value: height); walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); keysStorage = KeyService(_secureStorage); 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 = 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; _walletCreationService = WalletCreationService( secureStorage: _secureStorage, walletService: walletService, keyService: keysStorage, ); _walletCreationService!.changeWalletType(); // To restore from a seed final wallet = await _walletCreationService!.restoreFromSeed(credentials); walletInfo.address = wallet.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); walletBase?.close(); walletBase = wallet as MoneroWalletBase; // walletBase!.onNewBlock = onNewBlock; // walletBase!.onNewTransaction = onNewTransaction; // walletBase!.syncStatusChanged = syncStatusChanged; await Future.wait([ updateCachedId(walletId), updateCachedIsFavorite(false), ]); } catch (e, s) { debugPrint(e.toString()); debugPrint(s.toString()); } 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!.rescan(height: credentials.height); walletBase!.close(); } catch (e, s) { Logging.instance.log( "Exception rethrown from recoverFromMnemonic(): $e\n$s", level: LogLevel.Error); longMutex = false; rethrow; } longMutex = false; final end = DateTime.now(); Logging.instance.log( "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis", level: LogLevel.Info); } @override Future refresh() async { if (refreshMutex) { Logging.instance.log("$walletId $walletName refreshMutex denied", level: LogLevel.Info); return; } else { refreshMutex = true; } GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.syncing, walletId, coin, ), ); await _refreshTransactions(); await _updateBalance(); await _checkCurrentReceivingAddressesForTransactions(); if (walletBase?.syncStatus is SyncedSyncStatus) { refreshMutex = false; GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.synced, walletId, coin, ), ); } } @override Future testNetworkConnection() async { return await walletBase?.isConnected() ?? false; } 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(); // _autoSaveTimer?.cancel(); // await walletBase?.save(prioritySave: true); // walletBase?.close(); } }; Future _updateCachedBalance(int sats) async { await DB.instance.put( boxName: walletId, key: "cachedMoneroBalanceSats", value: sats, ); } int _getCachedBalance() => DB.instance.get( boxName: walletId, key: "cachedMoneroBalanceSats", ) as int? ?? 0; Future _updateBalance() async { final total = await _totalBalance; final available = await _availableBalance; _balance = Balance( coin: coin, total: total, spendable: available, blockedTotal: Amount( rawValue: BigInt.zero, fractionDigits: coin.decimals, ), pendingSpendable: total - available, ); await updateCachedBalance(_balance!); } Future get _availableBalance async { try { int runningBalance = 0; for (final entry in walletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } return Amount( rawValue: BigInt.from(runningBalance), fractionDigits: coin.decimals, ); } catch (_) { return Amount( rawValue: BigInt.zero, fractionDigits: coin.decimals, ); } } Future get _totalBalance async { try { final balanceEntries = walletBase?.balance?.entries; if (balanceEntries != null) { int bal = 0; for (var element in balanceEntries) { bal = bal + element.value.fullBalance; } await _updateCachedBalance(bal); return Amount( rawValue: BigInt.from(bal), fractionDigits: coin.decimals, ); } else { final transactions = walletBase!.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!; } } await _updateCachedBalance(transactionBalance); return Amount( rawValue: BigInt.from(transactionBalance), fractionDigits: coin.decimals, ); } } catch (_) { return Amount( rawValue: BigInt.from(_getCachedBalance()), fractionDigits: coin.decimals, ); } } @override Future updateNode(bool shouldRefresh) async { 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, ), ); // TODO: is this sync call needed? Do we need to notify ui here? await walletBase?.startSync(); if (shouldRefresh) { await refresh(); } } @override Future updateSentCachedTxData(Map txData) async { // not used for xmr return; } @override bool validateAddress(String address) => walletBase!.validateAddress(address); @override String get walletId => _walletId; Future _generateAddressForChain( int chain, int index, ) async { // String address = walletBase!.getTransactionAddress(chain, index); return isar_models.Address( walletId: walletId, derivationIndex: index, derivationPath: null, value: address, publicKey: [], type: isar_models.AddressType.cryptonote, subType: chain == 0 ? isar_models.AddressSubType.receiving : isar_models.AddressSubType.change, ); } Future _getFees() async { // TODO: not use random hard coded values here return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 15, numberOfBlocksSlow: 20, fast: MoneroTransactionPriority.fast.raw!, medium: MoneroTransactionPriority.regular.raw!, slow: MoneroTransactionPriority.slow.raw!, ); } Future _refreshTransactions() async { await walletBase!.updateTransactions(); final transactions = walletBase?.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); final List> txnsData = []; if (transactions != null) { for (var tx in transactions.entries) { // cachedTxids.add(tx.value.id); // Logging.instance.log( // "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} " // "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} " // "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}" // " ${tx.value.keyIndex}", // level: LogLevel.Info); isar_models.Address? address; isar_models.TransactionType type; if (tx.value.direction == TransactionDirection.incoming) { final addressInfo = tx.value.additionalInfo; final addressString = walletBase?.getTransactionAddress( addressInfo!['accountIndex'] as int, addressInfo['addressIndex'] as int, ); if (addressString != null) { address = await db .getAddresses(walletId) .filter() .valueEqualTo(addressString) .findFirst(); } type = isar_models.TransactionType.incoming; } else { // txn.address = ""; type = isar_models.TransactionType.outgoing; } final txn = isar_models.Transaction( walletId: walletId, txid: tx.value.id, timestamp: (tx.value.date.millisecondsSinceEpoch ~/ 1000), type: type, subType: isar_models.TransactionSubType.none, amount: tx.value.amount ?? 0, amountString: Amount( rawValue: BigInt.from(tx.value.amount ?? 0), fractionDigits: coin.decimals, ).toJsonString(), fee: tx.value.fee ?? 0, height: tx.value.height, isCancelled: false, isLelantus: false, slateId: null, otherData: null, nonce: null, inputs: [], outputs: [], ); txnsData.add(Tuple2(txn, address)); } } await db.addNewTransactionData(txnsData, walletId); // quick hack to notify manager to call notifyListeners if // transactions changed if (txnsData.isNotEmpty) { GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( "Transactions updated/added for: $walletId $walletName ", 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'); Future _getCurrentNode() async { return NodeService(secureStorageInterface: _secureStorage) .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } void onNewBlock({required int height, required int blocksLeft}) { // print("============================="); print("New Block! :: $walletName"); print("============================="); updateCachedChainHeight(height); _refreshTxDataHelper(); } void onNewTransaction() { // print("============================="); print("New Transaction! :: $walletName"); print("============================="); // call this here? GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( "New data found in $walletId $walletName in background!", walletId, ), ); } bool _txRefreshLock = false; int _lastCheckedHeight = -1; int _txCount = 0; Future _refreshTxDataHelper() async { if (_txRefreshLock) return; _txRefreshLock = true; final syncStatus = walletBase?.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 _refreshTransactions(); final count = await db.getTransactions(walletId).count(); if (count > _txCount) { _txCount = count; await _updateBalance(); GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( "New transaction data found in $walletId $walletName!", walletId, ), ); } } void syncStatusChanged() async { final syncStatus = walletBase?.syncStatus; if (syncStatus != null) { if (syncStatus.progress() == 1) { refreshMutex = false; } WalletSyncStatus? status; _isConnected = 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; final percent = height / nodeHeight; final highest = max(highestPercentCached, percent); // update cached if (highestPercentCached < percent) { highestPercentCached = percent; } await updateCachedChainHeight(height); 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; _isConnected = false; } else if (syncStatus is StartingSyncStatus) { status = WalletSyncStatus.syncing; GlobalEventBus.instance.fire( RefreshPercentChangedEvent( highestPercentCached, walletId, ), ); } else if (syncStatus is FailedSyncStatus) { status = WalletSyncStatus.unableToSync; _isConnected = 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; _isConnected = false; } if (status != null) { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( status, walletId, coin, ), ); } } } Future _checkCurrentReceivingAddressesForTransactions() async { try { await _checkReceivingAddressForTransactions(); } catch (e, s) { Logging.instance.log( "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", level: LogLevel.Error); rethrow; } } Future _checkReceivingAddressForTransactions() async { try { int highestIndex = -1; for (var element in walletBase!.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 _currentReceivingAddress; 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 = await _generateAddressForChain(0, newReceivingIndex); final existing = await db .getAddresses(walletId) .filter() .valueEqualTo(newReceivingAddress.value) .findFirst(); if (existing == null) { // Add that new change address await db.putAddress(newReceivingAddress); } else { // we need to update the address await db.updateAddress(existing, newReceivingAddress); // since we updated an existing address there is a chance it has // some tx history. To prevent address reuse we will call check again // recursively 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; } } double get highestPercentCached => DB.instance.get(boxName: walletId, key: "highestPercentCached") as double? ?? 0; set highestPercentCached(double value) => DB.instance.put( boxName: walletId, key: "highestPercentCached", value: value, ); @override int get storedChainHeight => getCachedChainHeight(); @override Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; @override Future> get transactions => db.getTransactions(walletId).sortByTimestampDesc().findAll(); @override // TODO: implement utxos Future> get utxos => throw UnimplementedError(); }