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_wownero/api/exceptions/creation_transaction_exception.dart'; import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:cw_wownero/wownero_amount_format.dart'; import 'package:cw_wownero/wownero_wallet.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as wownero_output; import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:http/http.dart'; import 'package:mutex/mutex.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; import 'package:stackwallet/models/paymint/utxo_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/node_service.dart'; import 'package:stackwallet/services/price.dart'; import 'package:stackwallet/utilities/constants.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/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; const int MINIMUM_CONFIRMATIONS = 10; class WowneroWallet extends CoinServiceAPI { final String _walletId; final Coin _coin; final PriceAPI _priceAPI; final SecureStorageInterface _secureStorage; final Prefs _prefs; String _walletName; bool _shouldAutoSync = false; bool _isConnected = false; bool _hasCalledExit = false; bool refreshMutex = false; bool longMutex = false; WalletService? walletService; KeyService? keysStorage; WowneroWalletBase? walletBase; WalletCreationService? _walletCreationService; Timer? _autoSaveTimer; Future? _currentReceivingAddress; Future? _feeObject; Future? _transactionData; Mutex prepareSendMutex = Mutex(); Mutex estimateFeeMutex = Mutex(); WowneroWallet({ required String walletId, required String walletName, required Coin coin, required SecureStorageInterface secureStorage, PriceAPI? priceAPI, Prefs? prefs, }) : _walletId = walletId, _walletName = walletName, _coin = coin, _priceAPI = priceAPI ?? PriceAPI(Client()), _secureStorage = secureStorage, _prefs = prefs ?? Prefs.instance; @override bool get isFavorite { try { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { Logging.instance.log( "isFavorite fetch failed (returning false by default): $e\n$s", level: LogLevel.Error); return false; } } @override set isFavorite(bool markFavorite) { DB.instance.put( boxName: walletId, key: "isFavorite", value: markFavorite); } @override bool get shouldAutoSync => _shouldAutoSync; @override set shouldAutoSync(bool shouldAutoSync) { if (_shouldAutoSync != shouldAutoSync) { _shouldAutoSync = shouldAutoSync; // wow 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 // not really used for wownero Future> get allOwnAddresses async => []; @override Future get availableBalance async { int runningBalance = 0; for (final entry in walletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } return Format.satoshisToAmount(runningBalance, coin: coin); } @override // not used Future get balanceMinusMaxFee => throw UnimplementedError(); @override Coin get coin => _coin; @override Future confirmSend({required Map txData}) async { try { Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); final pendingWowneroTransaction = txData['pendingWowneroTransaction'] as PendingWowneroTransaction; try { await pendingWowneroTransaction.commit(); Logging.instance.log( "transaction ${pendingWowneroTransaction.id} has been sent", level: LogLevel.Info); return pendingWowneroTransaction.id; } catch (e, s) { Logging.instance.log("$walletName wownero 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 => _currentReceivingAddress ??= _getCurrentAddressForChain(0); @override Future estimateFeeFor(int satoshiAmount, 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; } var aprox; await estimateFeeMutex.protect(() async { { try { aprox = (await prepareSend( // This address is only used for getting an approximate fee, never for sending address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", satoshiAmount: satoshiAmount, args: {"feeRate": feeRateType}))['fee']; await Future.delayed(const Duration(milliseconds: 500)); } catch (e, s) { aprox = walletBase!.calculateEstimatedFee(priority, satoshiAmount); } } }); print("this is the aprox fee $aprox for $satoshiAmount"); final fee = (aprox as int); return fee; } @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 { var restoreHeight = walletBase?.walletInfo.restoreHeight; highestPercentCached = 0; await walletBase?.rescan(height: restoreHeight); } @override Future generateNewAddress() async { try { const String indexKey = "receivingIndex"; // First increment the receiving index await _incrementAddressIndexForChain(0); final newReceivingIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain(0, newReceivingIndex); // Add that new receiving address to the array of receiving addresses await _addToAddressesArrayForChain(newReceivingAddress, 0); // Set the new receiving address that the service _currentReceivingAddress = Future(() => 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( "Opening existing ${coin.prettyName} wallet $walletName...", level: LogLevel.Info); if ((DB.instance.get(boxName: walletId, key: "id")) == null) { //todo: check if print needed // debugPrint("Exception was thrown"); throw Exception( "Attempted to initialize an existing wallet using an unknown wallet ID!"); } walletService = wownero.createWowneroWalletService(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 (e, s) { throw Exception("Password not found $e, $s"); } walletBase = (await walletService?.openWallet(_walletId, password!)) as WowneroWalletBase; Logging.instance.log( "Opened existing ${coin.prettyName} wallet $walletName", level: LogLevel.Info, ); // Wallet already exists, triggers for a returning user String indexKey = "receivingIndex"; final curIndex = await DB.instance.get(boxName: walletId, key: indexKey) as int; // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain(0, curIndex); Logging.instance.log( "wownero address in init existing: $newReceivingAddress", level: LogLevel.Info); _currentReceivingAddress = Future(() => newReceivingAddress); } @override Future initializeNew({int seedWordsLength = 14}) async { await _prefs.init(); // this should never fail if ((await _secureStorage.read(key: '${_walletId}_mnemonic')) != null) { throw Exception( "Attempted to overwrite mnemonic on generate new wallet!"); } // TODO: Wallet Service may need to be switched to Wownero walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); keysStorage = KeyService(_secureStorage); WalletInfo walletInfo; WalletCredentials credentials; try { String name = _walletId; final dirPath = await _pathForWalletDir(name: name, type: WalletType.wownero); final path = await _pathForWallet(name: name, type: WalletType.wownero); credentials = wownero.createWowneroNewWalletCredentials( name: name, language: "English", seedWordsLength: seedWordsLength, ); walletInfo = WalletInfo.external( id: WalletBase.idFor(name, WalletType.wownero), name: name, type: WalletType.wownero, 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?.create(credentials); final bufferedCreateHeight = (seedWordsLength == 14) ? getSeedHeightSync(wallet?.seed.trim() as String) : wownero.getHeightByDate( date: DateTime.now().subtract(const Duration( days: 2))); // subtract a couple days to ensure we have a buffer for SWB await DB.instance.put( boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); walletInfo.restoreHeight = bufferedCreateHeight; await _secureStorage.write( key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); walletInfo.address = wallet?.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); walletBase?.close(); walletBase = wallet as WowneroWalletBase; } catch (e, s) { 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.wownero)); await walletBase?.startSync(); await DB.instance .put(boxName: walletId, key: "id", value: _walletId); // Set relevant indexes await DB.instance .put(boxName: walletId, key: "receivingIndex", value: 0); await DB.instance .put(boxName: walletId, key: "changeIndex", value: 0); await DB.instance.put( boxName: walletId, key: 'blocked_tx_hashes', value: ["0xdefault"], ); // A list of transaction hashes to represent frozen utxos in wallet // initialize address book entries await DB.instance.put( boxName: walletId, key: 'addressBookEntries', value: {}); await DB.instance .put(boxName: walletId, key: "isFavorite", value: false); // Generate and add addresses to relevant arrays final initialReceivingAddress = await _generateAddressForChain(0, 0); // final initialChangeAddress = await _generateAddressForChain(1, 0); await _addToAddressesArrayForChain(initialReceivingAddress, 0); // await _addToAddressesArrayForChain(initialChangeAddress, 1); await DB.instance.put( boxName: walletId, key: 'receivingAddresses', value: [initialReceivingAddress]); await DB.instance .put(boxName: walletId, key: "receivingIndex", value: 0); walletBase?.close(); _currentReceivingAddress = Future(() => initialReceivingAddress); Logging.instance .log("initializeNew for $walletName $walletId", level: LogLevel.Info); } @override bool get isConnected => _isConnected; @override bool get isRefreshing => refreshMutex; @override // not used in wow Future get maxFee => throw UnimplementedError(); @override Future> get mnemonic async { final mnemonicString = await _secureStorage.read(key: '${_walletId}_mnemonic'); if (mnemonicString == null) { return []; } final List data = mnemonicString.split(' '); return data; } @override // not used in wow Future get pendingBalance => throw UnimplementedError(); @override Future> prepareSend({ required String address, required int satoshiAmount, Map? args, }) async { 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; final satInDecimal = Format.satoshisToAmount(satoshiAmount, coin: coin); if (satInDecimal == balance) { isSendAll = true; } Logging.instance .log("$address $satoshiAmount $args", level: LogLevel.Info); String amountToSend = satInDecimal .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); Logging.instance .log("$satoshiAmount $amountToSend", level: LogLevel.Info); wownero_output.Output output = wownero_output.Output(walletBase!); output.address = address; output.sendAll = isSendAll; output.setCryptoAmount(amountToSend); List outputs = [output]; Object tmp = wownero.createWowneroTransactionCreationCredentials( 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); } PendingWowneroTransaction pendingWowneroTransaction = await (awaitPendingTransaction!) as PendingWowneroTransaction; int realfee = Format.decimalAmountToSatoshis( Decimal.parse(pendingWowneroTransaction.feeFormatted), coin); //todo: check if print needed // debugPrint("fee? $realfee"); Map txData = { "pendingWowneroTransaction": pendingWowneroTransaction, "fee": realfee, "addresss": address, "recipientAmt": satoshiAmount, }; 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, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height, }) async { final int seedLength = mnemonic.trim().split(" ").length; if (!(seedLength == 14 || seedLength == 25)) { throw Exception("Invalid wownero mnemonic length found: $seedLength"); } await _prefs.init(); longMutex = true; final start = DateTime.now(); try { // check to make sure we aren't overwriting a mnemonic // this should never fail if ((await _secureStorage.read(key: '${_walletId}_mnemonic')) != null) { longMutex = false; throw Exception("Attempted to overwrite mnemonic on restore!"); } await _secureStorage.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); // extract seed height from 14 word seed if (seedLength == 14) { height = getSeedHeightSync(mnemonic.trim()); } else { // 25 word seed. TODO validate if (height == 0) { height = wownero.getHeightByDate( date: DateTime.now().subtract(const Duration( days: 2))); // subtract a couple days to ensure we have a buffer for SWB\ } } await DB.instance .put(boxName: walletId, key: "restoreHeight", value: height); walletService = wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); keysStorage = KeyService(_secureStorage); WalletInfo walletInfo; WalletCredentials credentials; String name = _walletId; final dirPath = await _pathForWalletDir(name: name, type: WalletType.wownero); final path = await _pathForWallet(name: name, type: WalletType.wownero); credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( name: name, height: height, mnemonic: mnemonic.trim(), ); try { walletInfo = WalletInfo.external( id: WalletBase.idFor(name, WalletType.wownero), name: name, type: WalletType.wownero, 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 WowneroWalletBase; await DB.instance.put( boxName: walletId, key: 'receivingAddresses', value: [walletInfo.address!]); await DB.instance .put(boxName: walletId, key: "receivingIndex", value: 0); await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance .put(boxName: walletId, key: "changeIndex", value: 0); await DB.instance.put( boxName: walletId, key: 'blocked_tx_hashes', value: ["0xdefault"], ); // A list of transaction hashes to represent frozen utxos in wallet // initialize address book entries await DB.instance.put( boxName: walletId, key: 'addressBookEntries', value: {}); await DB.instance .put(boxName: walletId, key: "isFavorite", value: false); } catch (e, s) { //todo: come back to this 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.wownero)); 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, ), ); final newTxData = await _fetchTransactionData(); _transactionData = Future(() => newTxData); await _checkCurrentReceivingAddressesForTransactions(); String indexKey = "receivingIndex"; final curIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; // Use new index to derive a new receiving address try { final newReceivingAddress = await _generateAddressForChain(0, curIndex); _currentReceivingAddress = Future(() => newReceivingAddress); } catch (e, s) { Logging.instance.log( "Failed to call _generateAddressForChain(0, $curIndex): $e\n$s", level: LogLevel.Error); } if (walletBase?.syncStatus is SyncedSyncStatus) { refreshMutex = false; GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.synced, walletId, coin, ), ); } } @override Future send({ required String toAddress, required int amount, Map args = const {}, }) { // not used for xmr throw UnimplementedError(); } @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 WowneroWalletBase?; 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)); } await walletBase?.startSync(); await refresh(); _autoSaveTimer?.cancel(); _autoSaveTimer = Timer.periodic( const Duration(seconds: 93), (_) async => await walletBase?.save(), ); } else { await exit(); } }; Future _updateCachedBalance(int sats) async { await DB.instance.put( boxName: walletId, key: "cachedWowneroBalanceSats", value: sats, ); } int _getCachedBalance() => DB.instance.get( boxName: walletId, key: "cachedWowneroBalanceSats", ) as int? ?? 0; @override 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 Format.satoshisToAmount(bal, coin: coin); } 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 Format.satoshisToAmount(transactionBalance, coin: coin); } } catch (_) { return Format.satoshisToAmount(_getCachedBalance(), coin: coin); } } @override Future get transactionData => _transactionData ??= _fetchTransactionData(); @override // not used for xmr Future> get unspentOutputs => throw UnimplementedError(); @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)); // 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; /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] /// and /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! Future _getCurrentAddressForChain(int chain) async { // Here, we assume that chain == 1 if it isn't 0 String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; final internalChainArray = (DB.instance .get(boxName: walletId, key: arrayKey)) as List; return internalChainArray.last as String; } /// Increases the index for either the internal or external chain, depending on [chain]. /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! Future _incrementAddressIndexForChain(int chain) async { // Here we assume chain == 1 if it isn't 0 String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; final newIndex = (DB.instance.get(boxName: walletId, key: indexKey)) + 1; await DB.instance .put(boxName: walletId, key: indexKey, value: newIndex); } Future _generateAddressForChain(int chain, int index) async { // String address = walletBase!.getTransactionAddress(chain, index); return address; } /// Adds [address] to the relevant chain's address array, which is determined by [chain]. /// [address] - Expects a standard native segwit address /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! Future _addToAddressesArrayForChain(String address, int chain) async { String chainArray = ''; if (chain == 0) { chainArray = 'receivingAddresses'; } else { chainArray = 'changeAddresses'; } final addressArray = DB.instance.get(boxName: walletId, key: chainArray); if (addressArray == null) { Logging.instance.log( 'Attempting to add the following to $chainArray array for chain $chain:${[ address ]}', level: LogLevel.Info); await DB.instance .put(boxName: walletId, key: chainArray, value: [address]); } else { // Make a deep copy of the existing list final List newArray = []; addressArray .forEach((dynamic _address) => newArray.add(_address as String)); newArray.add(address); // Add the address passed into the method await DB.instance .put(boxName: walletId, key: chainArray, value: newArray); } } 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 _fetchTransactionData() 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); // 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); // } // } // } // } // sort thing stuff // change to get Wownero price final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; final List> midSortedArray = []; 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); String am = wowneroAmountToString(amount: tx.value.amount!); final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2); Map midSortedTx = {}; // // create final tx map midSortedTx["txid"] = tx.value.id; midSortedTx["confirmed_status"] = !tx.value.isPending && tx.value.confirmations != null && tx.value.confirmations! >= MINIMUM_CONFIRMATIONS; midSortedTx["confirmations"] = tx.value.confirmations ?? 0; midSortedTx["timestamp"] = (tx.value.date.millisecondsSinceEpoch ~/ 1000); midSortedTx["txType"] = tx.value.direction == TransactionDirection.incoming ? "Received" : "Sent"; midSortedTx["amount"] = tx.value.amount; midSortedTx["worthNow"] = worthNow; midSortedTx["worthAtBlockTimestamp"] = worthNow; midSortedTx["fees"] = tx.value.fee; // TODO: shouldn't wownero have an address I can grab if (tx.value.direction == TransactionDirection.incoming) { final addressInfo = tx.value.additionalInfo; midSortedTx["address"] = walletBase?.getTransactionAddress( addressInfo!['accountIndex'] as int, addressInfo['addressIndex'] as int, ); } else { midSortedTx["address"] = ""; } final int txHeight = tx.value.height ?? 0; midSortedTx["height"] = txHeight; // if (txHeight >= latestTxnBlockHeight) { // latestTxnBlockHeight = txHeight; // } midSortedTx["aliens"] = []; midSortedTx["inputSize"] = 0; midSortedTx["outputSize"] = 0; midSortedTx["inputs"] = []; midSortedTx["outputs"] = []; midSortedArray.add(midSortedTx); } } // sort by date ---- midSortedArray .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); Logging.instance.log(midSortedArray, level: LogLevel.Info); // buildDateTimeChunks final Map result = {"dateTimeChunks": []}; final dateArray = []; for (int i = 0; i < midSortedArray.length; i++) { final txObject = midSortedArray[i]; final date = extractDateFromTimestamp(txObject["timestamp"] as int); final txTimeArray = [txObject["timestamp"], date]; if (dateArray.contains(txTimeArray[1])) { result["dateTimeChunks"].forEach((dynamic chunk) { if (extractDateFromTimestamp(chunk["timestamp"] as int) == txTimeArray[1]) { if (chunk["transactions"] == null) { chunk["transactions"] = >[]; } chunk["transactions"].add(txObject); } }); } else { dateArray.add(txTimeArray[1]); final chunk = { "timestamp": txTimeArray[0], "transactions": [txObject], }; result["dateTimeChunks"].add(chunk); } } // final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; final Map transactionsMap = {}; transactionsMap .addAll(TransactionData.fromJson(result).getAllTransactions()); final txModel = TransactionData.fromMap(transactionsMap); // // await DB.instance.put( // boxName: walletId, // key: 'storedTxnDataHeight', // value: latestTxnBlockHeight); // await DB.instance.put( // boxName: walletId, key: 'latest_tx_model', value: txModel); // await DB.instance.put( // boxName: walletId, // key: 'cachedTxids', // value: cachedTxids.toList(growable: false)); return txModel; } 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() { // print("============================="); print("New Wownero Block! :: $walletName"); print("============================="); } void onNewTransaction() { // print("============================="); print("New Wownero Transaction! :: $walletName"); print("============================="); // call this here? GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( "New data found in $walletId $walletName in background!", 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; } 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 String indexKey = "receivingIndex"; final curIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; if (highestIndex >= curIndex) { // First increment the receiving index await _incrementAddressIndexForChain(0); final newReceivingIndex = DB.instance.get(boxName: walletId, key: indexKey) as int; // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain(0, newReceivingIndex); // Add that new receiving address to the array of receiving addresses await _addToAddressesArrayForChain(newReceivingAddress, 0); _currentReceivingAddress = Future(() => newReceivingAddress); } } 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, ); }