import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libepiccash/lib.dart' as epiccash; import 'package:flutter_libepiccash/models/transaction.dart' as epic_models; import 'package:isar/isar.dart'; import 'package:mutex/mutex.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/epicbox_config_model.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.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/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_epicboxes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; import 'package:websocket_universal/websocket_universal.dart'; // // refactor of https://github.com/cypherstack/stack_wallet/blob/1d9fb4cd069f22492ece690ac788e05b8f8b1209/lib/services/coins/epiccash/epiccash_wallet.dart // class EpiccashWallet extends Bip39Wallet { EpiccashWallet(CryptoCurrencyNetwork network) : super(Epiccash(network)); final syncMutex = Mutex(); NodeModel? _epicNode; Timer? timer; double highestPercent = 0; Future get getSyncPercent async { int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; final _chainHeight = await chainHeight; double restorePercent = lastScannedBlock / _chainHeight; GlobalEventBus.instance .fire(RefreshPercentChangedEvent(highestPercent, walletId)); if (restorePercent > highestPercent) { highestPercent = restorePercent; } final int blocksRemaining = _chainHeight - lastScannedBlock; GlobalEventBus.instance .fire(BlocksRemainingEvent(blocksRemaining, walletId)); return restorePercent < 0 ? 0.0 : restorePercent; } Future updateEpicboxConfig(String host, int port) async { String stringConfig = jsonEncode({ "epicbox_domain": host, "epicbox_port": port, "epicbox_protocol_unsecure": false, "epicbox_address_index": 0, }); await secureStorageInterface.write( key: '${walletId}_epicboxConfig', value: stringConfig, ); // TODO: refresh anything that needs to be refreshed/updated due to epicbox info changed } /// returns an empty String on success, error message on failure Future cancelPendingTransactionAndPost(String txSlateId) async { try { final String wallet = (await secureStorageInterface.read( key: '${walletId}_wallet', ))!; final result = await epiccash.LibEpiccash.cancelTransaction( wallet: wallet, transactionId: txSlateId, ); Logging.instance.log( "cancel $txSlateId result: $result", level: LogLevel.Info, ); return result; } catch (e, s) { Logging.instance.log("$e, $s", level: LogLevel.Error); return e.toString(); } } Future getEpicBoxConfig() async { EpicBoxConfigModel? _epicBoxConfig; // read epicbox config from secure store String? storedConfig = await secureStorageInterface.read(key: '${walletId}_epicboxConfig'); // we should move to storing the primary server model like we do with nodes, and build the config from that (see epic-mobile) // EpicBoxServerModel? _epicBox = epicBox ?? // DB.instance.get( // boxName: DB.boxNamePrimaryEpicBox, key: 'primary'); // Logging.instance.log( // "Read primary Epic Box config: ${jsonEncode(_epicBox)}", // level: LogLevel.Info); if (storedConfig == null) { // if no config stored, use the default epicbox server as config _epicBoxConfig = EpicBoxConfigModel.fromServer(DefaultEpicBoxes.defaultEpicBoxServer); } else { // if a config is stored, test it _epicBoxConfig = EpicBoxConfigModel.fromString( storedConfig); // fromString handles checking old config formats } bool isEpicboxConnected = await _testEpicboxServer( _epicBoxConfig.host, _epicBoxConfig.port ?? 443); if (!isEpicboxConnected) { // default Epicbox is not connected, default to Europe _epicBoxConfig = EpicBoxConfigModel.fromServer(DefaultEpicBoxes.europe); // example of selecting another random server from the default list // alternative servers: copy list of all default EB servers but remove the default default // List alternativeServers = DefaultEpicBoxes.all; // alternativeServers.removeWhere((opt) => opt.name == DefaultEpicBoxes.defaultEpicBoxServer.name); // alternativeServers.shuffle(); // randomize which server is used // _epicBoxConfig = EpicBoxConfigModel.fromServer(alternativeServers.first); // TODO test this connection before returning it } return _epicBoxConfig; } // ================= Private ================================================= Future _getConfig() async { if (_epicNode == null) { await updateNode(); } final NodeModel node = _epicNode!; final String nodeAddress = node.host; final int port = node.port; final uri = Uri.parse(nodeAddress).replace(port: port); final String nodeApiAddress = uri.toString(); final walletDir = await _currentWalletDirPath(); final Map config = {}; config["wallet_dir"] = walletDir; config["check_node_api_http_addr"] = nodeApiAddress; config["chain"] = "mainnet"; config["account"] = "default"; config["api_listen_port"] = port; config["api_listen_interface"] = nodeApiAddress.replaceFirst(uri.scheme, ""); String stringConfig = jsonEncode(config); return stringConfig; } Future _currentWalletDirPath() async { Directory appDir = await StackFileSystem.applicationRootDirectory(); final path = "${appDir.path}/epiccash"; final String name = walletId.trim(); return '$path/$name'; } Future _nativeFee( int satoshiAmount, { bool ifErrorEstimateFee = false, }) async { final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); try { final available = info.cachedBalance.spendable.raw.toInt(); var transactionFees = await epiccash.LibEpiccash.getTransactionFees( wallet: wallet!, amount: satoshiAmount, minimumConfirmations: cryptoCurrency.minConfirms, available: available, ); int realFee = 0; try { realFee = (Decimal.parse(transactionFees.fee.toString())).toBigInt().toInt(); } catch (e, s) { //todo: come back to this debugPrint("$e $s"); } return realFee; } catch (e, s) { Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error); rethrow; } } Future _startSync() async { Logging.instance.log("request start sync", level: LogLevel.Info); final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); const int refreshFromNode = 1; if (!syncMutex.isLocked) { await syncMutex.protect(() async { // How does getWalletBalances start syncing???? await epiccash.LibEpiccash.getWalletBalances( wallet: wallet!, refreshFromNode: refreshFromNode, minimumConfirmations: 10, ); }); } else { Logging.instance.log("request start sync denied", level: LogLevel.Info); } } Future< ({ double awaitingFinalization, double pending, double spendable, double total })> _allWalletBalances() async { final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); const refreshFromNode = 0; return await epiccash.LibEpiccash.getWalletBalances( wallet: wallet!, refreshFromNode: refreshFromNode, minimumConfirmations: cryptoCurrency.minConfirms, ); } Future _testEpicboxServer(String host, int port) async { // TODO use an EpicBoxServerModel as the only param final websocketConnectionUri = 'wss://$host:$port'; const connectionOptions = SocketConnectionOptions( pingIntervalMs: 3000, timeoutConnectionMs: 4000, /// see ping/pong messages in [logEventStream] stream skipPingMessages: true, /// Set this attribute to `true` if do not need any ping/pong /// messages and ping measurement. Default is `false` pingRestrictionForce: true, ); final IMessageProcessor textSocketProcessor = SocketSimpleTextProcessor(); final textSocketHandler = IWebSocketHandler.createClient( websocketConnectionUri, textSocketProcessor, connectionOptions: connectionOptions, ); // Listening to server responses: bool isConnected = true; textSocketHandler.incomingMessagesStream.listen((inMsg) { Logging.instance.log( '> webSocket got text message from server: "$inMsg" ' '[ping: ${textSocketHandler.pingDelayMs}]', level: LogLevel.Info); }); // Connecting to server: final isTextSocketConnected = await textSocketHandler.connect(); if (!isTextSocketConnected) { // ignore: avoid_print Logging.instance.log( 'Connection to [$websocketConnectionUri] failed for some reason!', level: LogLevel.Error); isConnected = false; } return isConnected; } Future _putSendToAddresses( ({String slateId, String commitId}) slateData, Map txAddressInfo, ) async { try { final slatesToCommits = info.epicData?.slatesToCommits ?? {}; final from = txAddressInfo['from']; final to = txAddressInfo['to']; slatesToCommits[slateData.slateId] = { "commitId": slateData.commitId, "from": from, "to": to, }; await info.updateExtraEpiccashWalletInfo( epicData: info.epicData!.copyWith( slatesToCommits: slatesToCommits, ), isar: mainDB.isar, ); return true; } catch (e, s) { Logging.instance .log("ERROR STORING ADDRESS $e $s", level: LogLevel.Error); return false; } } Future _getCurrentIndex() async { try { final int receivingIndex = info.epicData!.receivingIndex; // TODO: go through pendingarray and processed array and choose the index // of the last one that has not been processed, or the index after the one most recently processed; return receivingIndex; } catch (e, s) { Logging.instance.log("$e $s", level: LogLevel.Error); return 0; } } Future
_generateAndStoreReceivingAddressForIndex( int index, ) async { Address? address = await getCurrentReceivingAddress(); if (address == null) { final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); final walletAddress = await epiccash.LibEpiccash.getAddressInfo( wallet: wallet!, index: index, epicboxConfig: epicboxConfig.toString(), ); Logging.instance.log( "WALLET_ADDRESS_IS $walletAddress", level: LogLevel.Info, ); address = Address( walletId: walletId, value: walletAddress, derivationIndex: index, derivationPath: null, type: AddressType.mimbleWimble, subType: AddressSubType.receiving, publicKey: [], // ?? ); await mainDB.updateOrPutAddresses([address]); } return address; } Future _startScans() async { try { //First stop the current listener epiccash.LibEpiccash.stopEpicboxListener(); final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); // max number of blocks to scan per loop iteration const scanChunkSize = 10000; // force firing of scan progress event await getSyncPercent; // fetch current chain height and last scanned block (should be the // restore height if full rescan or a wallet restore) int chainHeight = await this.chainHeight; int lastScannedBlock = info.epicData!.lastScannedBlock; // loop while scanning in chain in chunks (of blocks?) while (lastScannedBlock < chainHeight) { Logging.instance.log( "chainHeight: $chainHeight, lastScannedBlock: $lastScannedBlock", level: LogLevel.Info, ); int nextScannedBlock = await epiccash.LibEpiccash.scanOutputs( wallet: wallet!, startHeight: lastScannedBlock, numberOfBlocks: scanChunkSize, ); // update local cache await info.updateExtraEpiccashWalletInfo( epicData: info.epicData!.copyWith( lastScannedBlock: nextScannedBlock, ), isar: mainDB.isar, ); // force firing of scan progress event await getSyncPercent; // update while loop condition variables chainHeight = await this.chainHeight; lastScannedBlock = nextScannedBlock; } Logging.instance.log( "_startScans successfully at the tip", level: LogLevel.Info, ); //Once scanner completes restart listener await _listenToEpicbox(); } catch (e, s) { Logging.instance.log( "_startScans failed: $e\n$s", level: LogLevel.Error, ); rethrow; } } Future _listenToEpicbox() async { Logging.instance.log("STARTING WALLET LISTENER ....", level: LogLevel.Info); final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); epiccash.LibEpiccash.startEpicboxListener( wallet: wallet!, epicboxConfig: epicboxConfig.toString(), ); } // As opposed to fake config? Future _getRealConfig() async { String? config = await secureStorageInterface.read( key: '${walletId}_config', ); if (Platform.isIOS) { final walletDir = await _currentWalletDirPath(); var editConfig = jsonDecode(config as String); editConfig["wallet_dir"] = walletDir; config = jsonEncode(editConfig); } return config!; } // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index int _calculateRestoreHeightFrom({required DateTime date}) { int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; const int epicCashFirstBlock = 1565370278; const double overestimateSecondsPerBlock = 61; int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock; //todo: check if print needed // debugPrint( // "approximate height: $approximateHeight chosen_seconds: $chosenSeconds"); int height = approximateHeight; if (height < 0) { height = 0; } return height; } // ============== Overrides ================================================== @override int get isarTransactionVersion => 2; @override FilterOperation? get changeAddressFilterOperation => FilterGroup.and(standardChangeAddressFilters); @override FilterOperation? get receivingAddressFilterOperation => FilterGroup.and(standardReceivingAddressFilters); @override Future checkSaveInitialReceivingAddress() async { // epiccash seems ok with nothing here? } @override Future init({bool? isRestore}) async { if (isRestore != true) { String? encodedWallet = await secureStorageInterface.read(key: "${walletId}_wallet"); // check if should create a new wallet if (encodedWallet == null) { await updateNode(); final mnemonicString = await getMnemonic(); final String password = generatePassword(); final String stringConfig = await _getConfig(); final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); await secureStorageInterface.write( key: '${walletId}_config', value: stringConfig); await secureStorageInterface.write( key: '${walletId}_password', value: password); await secureStorageInterface.write( key: '${walletId}_epicboxConfig', value: epicboxConfig.toString()); String name = walletId; await epiccash.LibEpiccash.initializeNewWallet( config: stringConfig, mnemonic: mnemonicString, password: password, name: name, ); //Open wallet encodedWallet = await epiccash.LibEpiccash.openWallet( config: stringConfig, password: password, ); await secureStorageInterface.write( key: '${walletId}_wallet', value: encodedWallet, ); //Store Epic box address info await _generateAndStoreReceivingAddressForIndex(0); // subtract a couple days to ensure we have a buffer for SWB final bufferedCreateHeight = _calculateRestoreHeightFrom( date: DateTime.now().subtract(const Duration(days: 2))); final epicData = ExtraEpiccashWalletInfo( receivingIndex: 0, changeIndex: 0, slatesToAddresses: {}, slatesToCommits: {}, lastScannedBlock: bufferedCreateHeight, restoreHeight: bufferedCreateHeight, creationHeight: bufferedCreateHeight, ); await info.updateExtraEpiccashWalletInfo( epicData: epicData, isar: mainDB.isar, ); } else { try { Logging.instance.log( "initializeExisting() ${cryptoCurrency.coin.prettyName} wallet", level: LogLevel.Info); final config = await _getRealConfig(); final password = await secureStorageInterface.read(key: '${walletId}_password'); final walletOpen = await epiccash.LibEpiccash.openWallet( config: config, password: password!, ); await secureStorageInterface.write( key: '${walletId}_wallet', value: walletOpen); await updateNode(); } catch (e, s) { // do nothing, still allow user into wallet Logging.instance.log( "$runtimeType init() failed: $e\n$s", level: LogLevel.Error, ); } } } return await super.init(); } @override Future confirmSend({required TxData txData}) async { try { final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); // TODO determine whether it is worth sending change to a change address. final String receiverAddress = txData.recipients!.first.address; if (!receiverAddress.startsWith("http://") || !receiverAddress.startsWith("https://")) { bool isEpicboxConnected = await _testEpicboxServer( epicboxConfig.host, epicboxConfig.port ?? 443); if (!isEpicboxConnected) { throw Exception("Failed to send TX : Unable to reach epicbox server"); } } ({String commitId, String slateId}) transaction; if (receiverAddress.startsWith("http://") || receiverAddress.startsWith("https://")) { transaction = await epiccash.LibEpiccash.txHttpSend( wallet: wallet!, selectionStrategyIsAll: 0, minimumConfirmations: cryptoCurrency.minConfirms, message: txData.noteOnChain!, amount: txData.recipients!.first.amount.raw.toInt(), address: txData.recipients!.first.address, ); } else { transaction = await epiccash.LibEpiccash.createTransaction( wallet: wallet!, amount: txData.recipients!.first.amount.raw.toInt(), address: txData.recipients!.first.address, secretKeyIndex: 0, epicboxConfig: epicboxConfig.toString(), minimumConfirmations: cryptoCurrency.minConfirms, note: txData.noteOnChain!, ); } final Map txAddressInfo = {}; txAddressInfo['from'] = (await getCurrentReceivingAddress())!.value; txAddressInfo['to'] = txData.recipients!.first.address; await _putSendToAddresses(transaction, txAddressInfo); return txData.copyWith( txid: transaction.slateId, ); } catch (e, s) { Logging.instance.log( "Epic cash confirmSend: $e\n$s", level: LogLevel.Error, ); rethrow; } } @override Future prepareSend({required TxData txData}) async { try { if (txData.recipients?.length != 1) { throw Exception("Epic cash prepare send requires a single recipient!"); } ({String address, Amount amount, bool isChange}) recipient = txData.recipients!.first; final int realFee = await _nativeFee(recipient.amount.raw.toInt()); final feeAmount = Amount( rawValue: BigInt.from(realFee), fractionDigits: cryptoCurrency.fractionDigits, ); if (feeAmount > info.cachedBalance.spendable) { throw Exception( "Epic cash prepare send fee is greater than available balance!"); } if (info.cachedBalance.spendable == recipient.amount) { recipient = ( address: recipient.address, amount: recipient.amount - feeAmount, isChange: recipient.isChange, ); } return txData.copyWith( recipients: [recipient], fee: feeAmount, ); } catch (e, s) { Logging.instance .log("Epic cash prepareSend: $e\n$s", level: LogLevel.Error); rethrow; } } @override Future recover({required bool isRescan}) async { try { await refreshMutex.protect(() async { if (isRescan) { // clear blockchain info await mainDB.deleteWalletBlockchainData(walletId); await info.updateExtraEpiccashWalletInfo( epicData: info.epicData!.copyWith( lastScannedBlock: info.epicData!.restoreHeight, ), isar: mainDB.isar, ); unawaited(_startScans()); } else { await updateNode(); final String password = generatePassword(); final String stringConfig = await _getConfig(); final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); await secureStorageInterface.write( key: '${walletId}_config', value: stringConfig, ); await secureStorageInterface.write( key: '${walletId}_password', value: password, ); await secureStorageInterface.write( key: '${walletId}_epicboxConfig', value: epicboxConfig.toString(), ); await epiccash.LibEpiccash.recoverWallet( config: stringConfig, password: password, mnemonic: await getMnemonic(), name: info.walletId, ); final epicData = ExtraEpiccashWalletInfo( receivingIndex: 0, changeIndex: 0, slatesToAddresses: {}, slatesToCommits: {}, lastScannedBlock: info.restoreHeight, restoreHeight: info.restoreHeight, creationHeight: info.epicData?.creationHeight ?? info.restoreHeight, ); await info.updateExtraEpiccashWalletInfo( epicData: epicData, isar: mainDB.isar, ); //Open Wallet final walletOpen = await epiccash.LibEpiccash.openWallet( config: stringConfig, password: password, ); await secureStorageInterface.write( key: '${walletId}_wallet', value: walletOpen, ); await _generateAndStoreReceivingAddressForIndex( epicData.receivingIndex); } }); unawaited(refresh()); } catch (e, s) { Logging.instance.log( "Exception rethrown from electrumx_mixin recover(): $e\n$s", level: LogLevel.Info); rethrow; } } @override Future refresh() async { // Awaiting this lock could be dangerous. // Since refresh is periodic (generally) if (refreshMutex.isLocked) { return; } try { // this acquire should be almost instant due to above check. // Slight possibility of race but should be irrelevant await refreshMutex.acquire(); GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.syncing, walletId, cryptoCurrency.coin, ), ); // if (info.epicData?.creationHeight == null) { // await info.updateExtraEpiccashWalletInfo(epicData: inf, isar: isar) // await epicUpdateCreationHeight(await chainHeight); // } // this will always be zero???? final int curAdd = await _getCurrentIndex(); await _generateAndStoreReceivingAddressForIndex(curAdd); await _startScans(); unawaited(_startSync()); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); await updateChainHeight(); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); // if (this is MultiAddressInterface) { // await (this as MultiAddressInterface) // .checkReceivingAddressForTransactions(); // } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); // // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. // if (this is MultiAddressInterface) { // await (this as MultiAddressInterface) // .checkChangeAddressForTransactions(); // } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.50, walletId)); final fetchFuture = updateTransactions(); // if (currentHeight != storedHeight) { GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.60, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.70, walletId)); await fetchFuture; GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.80, walletId)); // await getAllTxsToWatch(); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.90, walletId)); await updateBalance(); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.synced, walletId, cryptoCurrency.coin, ), ); if (shouldAutoSync) { timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { // chain height check currently broken // if ((await chainHeight) != (await storedChainHeight)) { // TODO: [prio=med] some kind of quick check if wallet needs to refresh to replace the old refreshIfThereIsNewData call // if (await refreshIfThereIsNewData()) { unawaited(refresh()); // } // } }); } } catch (error, strace) { GlobalEventBus.instance.fire( NodeConnectionStatusChangedEvent( NodeConnectionStatus.disconnected, walletId, cryptoCurrency.coin, ), ); GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.unableToSync, walletId, cryptoCurrency.coin, ), ); Logging.instance.log( "Caught exception in refreshWalletData(): $error\n$strace", level: LogLevel.Error, ); } finally { refreshMutex.release(); } } @override Future updateBalance() async { try { final balances = await _allWalletBalances(); final balance = Balance( total: Amount.fromDecimal( Decimal.parse(balances.total.toString()) + Decimal.parse(balances.awaitingFinalization.toString()), fractionDigits: cryptoCurrency.fractionDigits, ), spendable: Amount.fromDecimal( Decimal.parse(balances.spendable.toString()), fractionDigits: cryptoCurrency.fractionDigits, ), blockedTotal: Amount.zeroWith( fractionDigits: cryptoCurrency.fractionDigits, ), pendingSpendable: Amount.fromDecimal( Decimal.parse(balances.pending.toString()), fractionDigits: cryptoCurrency.fractionDigits, ), ); await info.updateBalance( newBalance: balance, isar: mainDB.isar, ); } catch (e, s) { Logging.instance.log( "Epic cash wallet failed to update balance: $e\n$s", level: LogLevel.Warning, ); } } @override Future updateTransactions() async { try { final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); const refreshFromNode = 1; final myAddresses = await mainDB .getAddresses(walletId) .filter() .typeEqualTo(AddressType.mimbleWimble) .and() .subTypeEqualTo(AddressSubType.receiving) .and() .valueIsNotEmpty() .valueProperty() .findAll(); final myAddressesSet = myAddresses.toSet(); final transactions = await epiccash.LibEpiccash.getTransactions( wallet: wallet!, refreshFromNode: refreshFromNode, ); final List txns = []; final slatesToCommits = info.epicData?.slatesToCommits ?? {}; for (final tx in transactions) { // Logging.instance.log("tx: $tx", level: LogLevel.Info); final isIncoming = tx.txType == epic_models.TransactionType.TxReceived || tx.txType == epic_models.TransactionType.TxReceivedCancelled; final slateId = tx.txSlateId; final commitId = slatesToCommits[slateId]?['commitId'] as String?; final numberOfMessages = tx.messages?.messages.length; final onChainNote = tx.messages?.messages[0].message; final addressFrom = slatesToCommits[slateId]?["from"] as String?; final addressTo = slatesToCommits[slateId]?["to"] as String?; final credit = int.parse(tx.amountCredited); final debit = int.parse(tx.amountDebited); final fee = int.tryParse(tx.fee ?? "0") ?? 0; // hack epic tx data into inputs and outputs final List outputs = []; final List inputs = []; final addressFromIsMine = myAddressesSet.contains(addressFrom); final addressToIsMine = myAddressesSet.contains(addressTo); OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: credit.toString(), addresses: [ if (addressFrom != null) addressFrom, ], walletOwns: true, ); InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, outpoint: null, addresses: [if (addressTo != null) addressTo], valueStringSats: debit.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, walletOwns: true, ); final TransactionType txType; if (isIncoming) { if (addressToIsMine && addressFromIsMine) { txType = TransactionType.sentToSelf; } else { txType = TransactionType.incoming; } output = output.copyWith( addresses: [ myAddressesSet .first, // Must be changed if we ever do more than a single wallet address!!! ], walletOwns: true, ); } else { txType = TransactionType.outgoing; } outputs.add(output); inputs.add(input); final otherData = { "isEpiccashTransaction": true, "numberOfMessages": numberOfMessages, "slateId": slateId, "onChainNote": onChainNote, "isCancelled": tx.txType == epic_models.TransactionType.TxSentCancelled || tx.txType == epic_models.TransactionType.TxReceivedCancelled, "overrideFee": Amount( rawValue: BigInt.from(fee), fractionDigits: cryptoCurrency.fractionDigits, ).toJsonString(), }; final txn = TransactionV2( walletId: walletId, blockHash: null, hash: commitId ?? tx.id.toString(), txid: commitId ?? tx.id.toString(), timestamp: DateTime.parse(tx.creationTs).millisecondsSinceEpoch ~/ 1000, height: tx.confirmed ? tx.kernelLookupMinHeight ?? 1 : null, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: 0, type: txType, subType: TransactionSubType.none, otherData: jsonEncode(otherData), ); txns.add(txn); } await mainDB.updateOrPutTransactionV2s(txns); } catch (e, s) { Logging.instance.log( "${cryptoCurrency.runtimeType} ${cryptoCurrency.network} net wallet" " \"${info.name}\"_${info.walletId} updateTransactions() failed: $e\n$s", level: LogLevel.Warning, ); } } @override Future updateUTXOs() async { // not used for epiccash return false; } @override Future updateNode() async { _epicNode = getCurrentNode(); // TODO: [prio=low] move this out of secure storage if secure storage not needed final String stringConfig = await _getConfig(); await secureStorageInterface.write( key: '${walletId}_config', value: stringConfig, ); // unawaited(refresh()); } @override Future pingCheck() async { try { final node = nodeService.getPrimaryNodeFor(coin: cryptoCurrency.coin); // force unwrap optional as we want connection test to fail if wallet // wasn't initialized or epicbox node was set to null return await testEpicNodeConnection( NodeFormData() ..host = node!.host ..useSSL = node.useSSL ..port = node.port, ) != null; } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Info); return false; } } @override Future updateChainHeight() async { final config = await _getRealConfig(); final latestHeight = await epiccash.LibEpiccash.getChainHeight(config: config); await info.updateCachedChainHeight( newHeight: latestHeight, isar: mainDB.isar, ); } @override Future estimateFeeFor(Amount amount, int feeRate) async { // setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function????? int currentFee = await _nativeFee( amount.raw.toInt(), ifErrorEstimateFee: true, ); return Amount( rawValue: BigInt.from(currentFee), fractionDigits: cryptoCurrency.fractionDigits, ); } @override Future get fees async { // this wasn't done before the refactor either so... // TODO: implement _getFees return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, numberOfBlocksSlow: 10, fast: 1, medium: 1, slow: 1); } @override Future updateSentCachedTxData({required TxData txData}) async { // TODO: [prio=low] Was not used before refactor so maybe not required(?) return txData; } @override Future exit() async { timer?.cancel(); timer = null; await super.exit(); Logging.instance.log("EpicCash_wallet exit finished", level: LogLevel.Info); } } Future deleteEpicWallet({ required String walletId, required SecureStorageInterface secureStore, }) async { final wallet = await secureStore.read(key: '${walletId}_wallet'); String? config = await secureStore.read(key: '${walletId}_config'); if (Platform.isIOS) { Directory appDir = await StackFileSystem.applicationRootDirectory(); final path = "${appDir.path}/epiccash"; final String name = walletId.trim(); final walletDir = '$path/$name'; var editConfig = jsonDecode(config as String); editConfig["wallet_dir"] = walletDir; config = jsonEncode(editConfig); } if (wallet == null) { return "Tried to delete non existent epic wallet file with walletId=$walletId"; } else { try { return epiccash.LibEpiccash.deleteWallet( wallet: wallet, config: config!, ); } catch (e, s) { Logging.instance.log("$e\n$s", level: LogLevel.Error); return "deleteEpicWallet($walletId) failed..."; } } }