From 9aa593146cb0e52052ecaf4d848d7e2aa218a65d Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 30 Dec 2022 16:15:03 -0600 Subject: [PATCH] wownero sync refactor --- lib/services/coins/coin_service.dart | 2 +- lib/services/coins/monero/monero_wallet.dart | 4 +- .../coins/wownero/wownero_wallet.dart | 2555 ++++++++--------- pubspec.lock | 45 +- 4 files changed, 1149 insertions(+), 1457 deletions(-) diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index ceb9e7663..a690157ee 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -197,7 +197,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, - secureStore: secureStorageInterface, + secureStorage: secureStorageInterface, // tracker: tracker, ); diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 2f72e0dc2..caf41c34d 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -1096,14 +1096,14 @@ class MoneroWallet extends CoinServiceAPI { void onNewBlock() { // print("============================="); - print("New Block!"); + print("New Block! :: $walletName"); print("============================="); } void onNewTransaction() { // print("============================="); - print("New Transaction!"); + print("New Transaction! :: $walletName"); print("============================="); // call this here? diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 9e152d9e6..470df05e3 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; @@ -16,7 +17,6 @@ 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:dart_numerics/dart_numerics.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libmonero/core/key_service.dart'; @@ -33,7 +33,6 @@ 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/node_connection_status_changed_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'; @@ -52,1347 +51,46 @@ import 'package:stackwallet/utilities/stack_file_system.dart'; const int MINIMUM_CONFIRMATIONS = 10; -//https://github.com/wownero-project/wownero/blob/8361d60aef6e17908658128284899e3a11d808d4/src/cryptonote_config.h#L162 -const String GENESIS_HASH_MAINNET = - "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; -const String GENESIS_HASH_TESTNET = - "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; - class WowneroWallet extends CoinServiceAPI { - static const integrationTestFlag = - bool.fromEnvironment("IS_INTEGRATION_TEST"); - final _prefs = Prefs.instance; - - Timer? timer; - Timer? wowneroAutosaveTimer; - late Coin _coin; - - late SecureStorageInterface _secureStore; - - late PriceAPI _priceAPI; - - Future getCurrentNode() async { - return NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - WowneroWallet( - {required String walletId, - required String walletName, - required Coin coin, - PriceAPI? priceAPI, - required SecureStorageInterface secureStore}) { - _walletId = walletId; - _walletName = walletName; - _coin = coin; - - _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = secureStore; - } + final String _walletId; + final Coin _coin; + final PriceAPI _priceAPI; + final SecureStorageInterface _secureStorage; + final Prefs _prefs; + String _walletName; bool _shouldAutoSync = false; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - wowneroAutosaveTimer?.cancel(); - timer = null; - wowneroAutosaveTimer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - // Walletbase needs to be open for this to work - refresh(); - } - } - } - - @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.wownero)); - - // TODO: is this sync call needed? Do we need to notify ui here? - await walletBase?.startSync(); - - if (shouldRefresh) { - await refresh(); - } - } - - Future> _getMnemonicList() async { - final mnemonicString = - await _secureStore.read(key: '${_walletId}_mnemonic'); - if (mnemonicString == null) { - return []; - } - final List data = mnemonicString.split(' '); - return data; - } - - @override - Future> get mnemonic => _getMnemonicList(); - - Future get currentNodeHeight async { - try { - if (walletBase!.syncStatus! is SyncedSyncStatus && - walletBase!.syncStatus!.progress() == 1.0) { - return await walletBase!.getNodeHeight(); - } - } catch (e, s) {} - int _height = -1; - try { - _height = (walletBase!.syncStatus as SyncingSyncStatus).height; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - - int blocksRemaining = -1; - - try { - blocksRemaining = - (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - int currentHeight = _height + blocksRemaining; - if (_height == -1 || blocksRemaining == -1) { - currentHeight = int64MaxValue; - } - final cachedHeight = DB.instance - .get(boxName: walletId, key: "storedNodeHeight") as int? ?? - 0; - - if (currentHeight > cachedHeight && currentHeight != int64MaxValue) { - await DB.instance.put( - boxName: walletId, key: "storedNodeHeight", value: currentHeight); - return currentHeight; - } else { - return cachedHeight; - } - } - - Future get currentSyncingHeight async { - //TODO return the tip of the wownero blockchain - try { - if (walletBase!.syncStatus! is SyncedSyncStatus && - walletBase!.syncStatus!.progress() == 1.0) { - // Logging.instance - // .log("currentSyncingHeight lol", level: LogLevel.Warning); - return getSyncingHeight(); - } - } catch (e, s) {} - int syncingHeight = -1; - try { - syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - final cachedHeight = - DB.instance.get(boxName: walletId, key: "storedSyncingHeight") - as int? ?? - 0; - - if (syncingHeight > cachedHeight) { - await DB.instance.put( - boxName: walletId, key: "storedSyncingHeight", value: syncingHeight); - return syncingHeight; - } else { - return cachedHeight; - } - } - - Future updateStoredChainHeight({required int newHeight}) async { - await DB.instance.put( - boxName: walletId, key: "storedChainHeight", value: newHeight); - } - - int get storedChainHeight { - return DB.instance.get(boxName: walletId, key: "storedChainHeight") - as int? ?? - 0; - } - - /// 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 _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); - - // Set the new receiving address that the service - - _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; - } - } - - @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - - Timer? syncPercentTimer; - - Mutex syncHeightMutex = Mutex(); - Future stopSyncPercentTimer() async { - syncPercentTimer?.cancel(); - syncPercentTimer = null; - } - - Future startSyncPercentTimer() async { - if (syncPercentTimer != null) { - return; - } - syncPercentTimer?.cancel(); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(highestPercentCached, walletId)); - syncPercentTimer = Timer.periodic(const Duration(seconds: 30), (_) async { - if (syncHeightMutex.isLocked) { - return; - } - await syncHeightMutex.protect(() async { - // int restoreheight = walletBase!.walletInfo.restoreHeight ?? 0; - int _height = await currentSyncingHeight; - int _currentHeight = await currentNodeHeight; - double progress = 0; - try { - progress = walletBase!.syncStatus!.progress(); - } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); - } - - final int blocksRemaining = _currentHeight - _height; - - GlobalEventBus.instance - .fire(BlocksRemainingEvent(blocksRemaining, walletId)); - - if (progress == 1 && _currentHeight > 0 && _height > 0) { - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - return; - } - - // for some reason this can be 0 which screws up the percent calculation - // int64MaxValue is NOT the best value to use here - if (_currentHeight < 1) { - _currentHeight = int64MaxValue; - } - - if (_height < 1) { - _height = 1; - } - - double restorePercent = progress; - double highestPercent = highestPercentCached; - - Logging.instance.log( - "currentSyncingHeight: $_height, nodeHeight: $_currentHeight, restorePercent: $restorePercent, highestPercentCached: $highestPercentCached", - level: LogLevel.Info); - - if (restorePercent > 0 && restorePercent <= 1) { - // if (restorePercent > highestPercent) { - highestPercent = restorePercent; - highestPercentCached = restorePercent; - // } - } - - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(highestPercent, walletId)); - }); - }); - } - - 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, - ); - - /// Refreshes display data for the wallet - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log("$walletId $walletName refreshMutex denied", - level: LogLevel.Info); - return; - } else { - refreshMutex = true; - } - - if (walletBase == null) { - throw Exception("Tried to call refresh() in wownero without walletBase!"); - } - - try { - await startSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - final int _currentSyncingHeight = await currentSyncingHeight; - final int storedHeight = storedChainHeight; - int _currentNodeHeight = await currentNodeHeight; - - double progress = 0; - try { - progress = (walletBase!.syncStatus!).progress(); - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - await _fetchTransactionData(); - - bool stillSyncing = false; - Logging.instance.log( - "storedHeight: $storedHeight, _currentSyncingHeight: $_currentSyncingHeight, _currentNodeHeight: $_currentNodeHeight, progress: $progress, issynced: ${await walletBase!.isConnected()}", - level: LogLevel.Info); - - if (progress < 1.0) { - stillSyncing = true; - } - - if (_currentSyncingHeight > storedHeight) { - // 0 is returned from wownero as I assume an error????? - if (_currentSyncingHeight > 0) { - // 0 failed to fetch current height??? - await updateStoredChainHeight(newHeight: _currentSyncingHeight); - } - } - - 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); - } - final newTxData = await _fetchTransactionData(); - _transactionData = Future(() => newTxData); - - if (isActive || shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 60), (timer) async { - //todo: check if print needed - // debugPrint("run timer"); - //TODO: check for new data and refresh if needed. if wownero even needs this - // chain height check currently broken - // if ((await chainHeight) != (await storedChainHeight)) { - // if (await refreshIfThereIsNewData()) { - await refresh(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - // } - // } - }); - wowneroAutosaveTimer ??= - Timer.periodic(const Duration(seconds: 93), (timer) async { - //todo: check if print needed - // debugPrint("run wownero timer"); - if (isActive) { - await walletBase?.save(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - } - }); - } - - if (stillSyncing) { - debugPrint("still syncing"); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - refreshMutex = false; - return; - } - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - refreshMutex = false; - } catch (error, strace) { - refreshMutex = false; - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - coin, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - Logging.instance.log( - "Caught exception in refreshWalletData(): $error\n$strace", - level: LogLevel.Error); - } - } - - @override - // TODO: implement allOwnAddresses - Future> get allOwnAddresses { - return Future(() => []); - } - - @override - Future get balanceMinusMaxFee async => - (await availableBalance) - - Format.satoshisToAmount(await maxFee, coin: Coin.wownero); - - @override - Future get currentReceivingAddress => - _currentReceivingAddress ??= _getCurrentAddressForChain(0); - - @override - Future exit() async { - await stopSyncPercentTimer(); - _hasCalledExit = true; - isActive = false; - await walletBase?.save(prioritySave: true); - walletBase?.close(); - wowneroAutosaveTimer?.cancel(); - wowneroAutosaveTimer = null; - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } - + bool _isConnected = false; bool _hasCalledExit = false; - - @override - bool get hasCalledExit => _hasCalledExit; - - Future? _currentReceivingAddress; - - 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!, - ); - } - - @override - Future get fees => _feeObject ??= _getFees(); - Future? _feeObject; - - @override - // TODO: implement fullRescan - Future fullRescan( - int maxUnusedAddressGap, - int maxNumberOfIndexesToCheck, - ) async { - var restoreHeight = walletBase?.walletInfo.restoreHeight; - await walletBase?.rescan(height: restoreHeight); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - return; - } - - 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); - } - } - - /// 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; - } - - //TODO: take in the default language when creating wallet. - Future _generateNewWallet({int seedWordsLength = 14}) async { - Logging.instance - .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); - // TODO: ping wownero server and make sure the genesis hash matches - // 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!"); - // } - // } - // } - - // this should never fail - if ((await _secureStore.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(_secureStore); - 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: _secureStore, - 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 _secureStore.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()); - } - 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); - - _currentReceivingAddress = Future(() => initialReceivingAddress); - - Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); - } - - @override - // TODO: implement initializeWallet - Future initializeNew({int seedWordsLength = 14}) async { - await _prefs.init(); - // TODO: ping actual wownero network - // try { - // final hasNetwork = await _electrumXClient.ping(); - // if (!hasNetwork) { - // return false; - // } - // } catch (e, s) { - // Logging.instance.log("Caught in initializeWallet(): $e\n$s"); - // return false; - // } - walletService = - wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); - - await _generateNewWallet(seedWordsLength: seedWordsLength); - // var password; - // try { - // password = - // await keysStorage?.getWalletPassword(walletName: this._walletId); - // } catch (e, s) { - // Logging.instance.log("$e $s"); - // Logging.instance.log("Generating new ${coin.ticker} wallet."); - // // Triggers for new users automatically. Generates new wallet - // await _generateNewWallet(wallet); - // await wallet.put("id", this._walletId); - // return true; - // } - // walletBase = (await walletService?.openWallet(this._walletId, password)) - // as WowneroWalletBase; - // Logging.instance.log("Opening existing ${coin.ticker} wallet."); - // // Wallet already exists, triggers for a returning user - // final currentAddress = awaicurrentHeightt _getCurrentAddressForChain(0); - // this._currentReceivingAddress = Future(() => currentAddress); - // - // await walletBase?.connectToNode( - // node: Node( - // uri: "xmr-node.cakewallet.com:18081", type: WalletType.wownero)); - // walletBase?.startSync(); - - return true; - } - - @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(_secureStore); - - 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) { - //todo: check if print needed - // debugPrint("Exception was thrown $e $s"); - throw Exception("Password not found $e, $s"); - } - walletBase = (await walletService?.openWallet(_walletId, password!)) - as WowneroWalletBase; - debugPrint("walletBase $walletBase"); - 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 get maxFee async { - var bal = await availableBalance; - var fee = walletBase!.calculateEstimatedFee( - wownero.getDefaultTransactionPriority(), - Format.decimalAmountToSatoshis(bal, coin), - ); - - return fee; - } - - @override - // TODO: implement pendingBalance - Future get pendingBalance => throw UnimplementedError(); - + bool refreshMutex = false; bool longMutex = false; - // TODO: are these needed? - WalletService? walletService; KeyService? keysStorage; WowneroWalletBase? walletBase; WalletCreationService? _walletCreationService; + Timer? _autoSaveTimer; - String toStringForinfo(WalletInfo info) { - return "id: ${info.id} name: ${info.name} type: ${info.type} recovery: ${info.isRecovery}" - " restoreheight: ${info.restoreHeight} timestamp: ${info.timestamp} dirPath: ${info.dirPath} " - "path: ${info.path} address: ${info.address} addresses: ${info.addresses}"; - } - - 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'); - - // TODO: take in a dynamic height - @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 { - // 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 _secureStore.read(key: '${_walletId}_mnemonic')) != null) { - longMutex = false; - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - await _secureStore.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(_secureStore); - 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: _secureStore, - 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); - } 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 send({ - required String toAddress, - required int amount, - Map args = const {}, - }) async { - try { - final txData = await prepareSend( - address: toAddress, satoshiAmount: amount, args: args); - final txHash = await confirmSend(txData: txData); - return txHash; - } catch (e, s) { - Logging.instance - .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); - rethrow; - } - } - - @override - Future testNetworkConnection() async { - return await walletBase?.isConnected() ?? false; - } - - Timer? _networkAliveTimer; - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - _isConnected = hasNetwork; - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - GlobalEventBus.instance - .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); - } - } - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - bool _isConnected = false; - - @override - bool get isConnected => _isConnected; - - @override - Future get totalBalance async { - var 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!; - } - } - - // TODO: grab total balance - var bal = 0; - for (var element in walletBase!.balance!.entries) { - bal = bal + element.value.fullBalance; - } - //todo: check if print needed - // debugPrint("balances: $transactionBalance $bal"); - if (isActive) { - String am = wowneroAmountToString(amount: bal); - - return Decimal.parse(am); - } else { - String am = wowneroAmountToString(amount: transactionBalance); - - return Decimal.parse(am); - } - } - - @override - // TODO: implement onIsActiveWalletChanged - void Function(bool)? get onIsActiveWalletChanged => (isActive) async { - await walletBase?.save(); - walletBase?.close(); - wowneroAutosaveTimer?.cancel(); - wowneroAutosaveTimer = null; - timer?.cancel(); - timer = null; - await stopSyncPercentTimer(); - if (isActive) { - String? password; - try { - password = - await keysStorage?.getWalletPassword(walletName: _walletId); - } catch (e, s) { - //todo: check if print needed - // debugPrint("Exception was thrown $e $s"); - throw Exception("Password not fou" - "*nd $e, $s"); - } - walletBase = (await walletService?.openWallet(_walletId, password!)) - as WowneroWalletBase?; - 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.wownero)); - await walletBase?.startSync(); - } - await refresh(); - } - this.isActive = isActive; - }; - - bool isActive = false; - - @override - Future get transactionData => - _transactionData ??= _fetchTransactionData(); + Future? _currentReceivingAddress; + Future? _feeObject; Future? _transactionData; - // not used in wownero - TransactionData? cachedTxData; + Mutex prepareSendMutex = Mutex(); + Mutex estimateFeeMutex = Mutex(); - @override - Future updateSentCachedTxData(Map txData) async { - // not used in wownero - } - - Future _fetchTransactionData() async { - 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() ?? {}; - 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; - } - - @override - // TODO: implement unspentOutputs - Future> get unspentOutputs => throw UnimplementedError(); - - @override - bool validateAddress(String address) { - bool valid = walletBase!.validateAddress(address); - return valid; - } - - @override - String get walletId => _walletId; - late String _walletId; - - @override - String get walletName => _walletName; - late String _walletName; - - // setter for updating on rename - @override - set walletName(String newName) => _walletName = newName; - - @override - set isFavorite(bool markFavorite) { - DB.instance.put( - boxName: walletId, key: "isFavorite", value: markFavorite); - } + 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 { @@ -1408,17 +106,59 @@ class WowneroWallet extends CoinServiceAPI { } @override - // TODO: implement availableBalance - Future get availableBalance async { - var bal = 0; - for (var element in walletBase!.balance!.entries) { - bal = bal + element.value.unlockedBalance; - } - String am = wowneroAmountToString(amount: bal); - - return Decimal.parse(am); + 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; @@ -1446,97 +186,9 @@ class WowneroWallet extends CoinServiceAPI { } } - // TODO: fix the double free memory crash error. @override - Future> prepareSend( - {required String address, - required int satoshiAmount, - Map? args}) async { - int amount = satoshiAmount; - 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; - final satInDecimal = ((Decimal.fromInt(satoshiAmount) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal()); - if (satInDecimal == balance) { - isSendAll = true; - } - Logging.instance - .log("$toAddress $amount $args", level: LogLevel.Info); - String amountToSend = wowneroAmountToString(amount: amount); - Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); - - wownero_output.Output output = wownero_output.Output(walletBase!); - output.address = toAddress; - 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": toAddress, - "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"); - } - } - } - - Mutex prepareSendMutex = Mutex(); - Mutex estimateFeeMutex = Mutex(); + Future get currentReceivingAddress => + _currentReceivingAddress ??= _getCurrentAddressForChain(0); @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { @@ -1587,6 +239,32 @@ class WowneroWallet extends CoinServiceAPI { 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 { @@ -1615,4 +293,1025 @@ class WowneroWallet extends CoinServiceAPI { 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(); + } + }; + + @override + Future get totalBalance async { + final balanceEntries = walletBase?.balance?.entries; + if (balanceEntries != null) { + int bal = 0; + for (var element in balanceEntries) { + bal = bal + element.value.fullBalance; + } + 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!; + } + } + + return Format.satoshisToAmount(transactionBalance, 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, + ); } diff --git a/pubspec.lock b/pubspec.lock index e8f875d35..ecc0bdf8d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.11" + version: "3.3.0" args: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" barcode_scan2: dependency: "direct main" description: @@ -190,14 +190,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" checked_yaml: dependency: transitive description: @@ -218,7 +211,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -288,7 +281,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.5.0" cross_file: dependency: transitive description: @@ -442,7 +435,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: "direct main" description: @@ -871,21 +864,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -997,7 +990,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: @@ -1373,7 +1366,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -1417,7 +1410,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" string_validator: dependency: "direct main" description: @@ -1431,35 +1424,35 @@ packages: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.21.4" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.16" time: dependency: transitive description: @@ -1508,7 +1501,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" universal_io: dependency: transitive description: @@ -1592,7 +1585,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "8.2.2" + version: "9.0.0" wakelock: dependency: "direct main" description: