// /* // * This file is part of Stack Wallet. // * // * Copyright (c) 2023 Cypher Stack // * All Rights Reserved. // * The code is distributed under GPLv3 license, see LICENSE file for details. // * Generated by Cypher Stack on 2023-05-26 // * // */ // // import 'dart:async'; // // import 'package:bip39/bip39.dart' as bip39; // import 'package:ethereum_addresses/ethereum_addresses.dart'; // import 'package:http/http.dart'; // import 'package:isar/isar.dart'; // import 'package:stackwallet/db/hive/db.dart'; // import 'package:stackwallet/db/isar/main_db.dart'; // import 'package:stackwallet/models/balance.dart'; // import 'package:stackwallet/models/isar/models/isar_models.dart'; // import 'package:stackwallet/models/node_model.dart'; // import 'package:stackwallet/models/paymint/fee_object_model.dart'; // import 'package:stackwallet/services/coins/coin_service.dart'; // import 'package:stackwallet/services/ethereum/ethereum_api.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'; // import 'package:stackwallet/services/event_bus/global_event_bus.dart'; // import 'package:stackwallet/services/mixins/eth_token_cache.dart'; // import 'package:stackwallet/services/mixins/wallet_cache.dart'; // import 'package:stackwallet/services/mixins/wallet_db.dart'; // import 'package:stackwallet/services/node_service.dart'; // import 'package:stackwallet/services/transaction_notification_tracker.dart'; // import 'package:stackwallet/utilities/amount/amount.dart'; // import 'package:stackwallet/utilities/constants.dart'; // import 'package:stackwallet/utilities/default_nodes.dart'; // import 'package:stackwallet/utilities/enums/coin_enum.dart'; // import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; // import 'package:stackwallet/utilities/eth_commons.dart'; // import 'package:stackwallet/utilities/extensions/extensions.dart'; // import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; // import 'package:stackwallet/utilities/logger.dart'; // import 'package:stackwallet/utilities/prefs.dart'; // import 'package:stackwallet/widgets/crypto_notifications.dart'; // import 'package:tuple/tuple.dart'; // import 'package:web3dart/web3dart.dart' as web3; // // const int MINIMUM_CONFIRMATIONS = 3; // // class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { // EthereumWallet({ // required String walletId, // required String walletName, // required Coin coin, // required SecureStorageInterface secureStore, // required TransactionNotificationTracker tracker, // MainDB? mockableOverride, // }) { // txTracker = tracker; // _walletId = walletId; // _walletName = walletName; // _coin = coin; // _secureStore = secureStore; // initCache(walletId, coin); // initWalletDB(mockableOverride: mockableOverride); // } // // Balance getCachedTokenBalance(EthContract contract) { // final jsonString = DB.instance.get( // boxName: _walletId, // key: TokenCacheKeys.tokenBalance(contract.address), // ) as String?; // if (jsonString == null) { // return Balance( // total: Amount( // rawValue: BigInt.zero, // fractionDigits: contract.decimals, // ), // spendable: Amount( // rawValue: BigInt.zero, // fractionDigits: contract.decimals, // ), // blockedTotal: Amount( // rawValue: BigInt.zero, // fractionDigits: contract.decimals, // ), // pendingSpendable: Amount( // rawValue: BigInt.zero, // fractionDigits: contract.decimals, // ), // ); // } // return Balance.fromJson( // jsonString, // contract.decimals, // ); // } // // bool longMutex = false; // // @override // Future> get transactions => db // .getTransactions(walletId) // .filter() // .otherDataEqualTo( // null) // eth txns with other data where other data is the token contract address // .sortByTimestampDesc() // .findAll(); // // @override // Future get currentReceivingAddress async { // final address = await _currentReceivingAddress; // return checksumEthereumAddress( // address?.value ?? _credentials.address.hexEip55); // } // // @override // Future initializeExisting() async { // Logging.instance.log( // "initializeExisting() ${coin.prettyName} wallet", // level: LogLevel.Info, // ); // // await _initCredentials( // (await mnemonicString)!, // (await mnemonicPassphrase)!, // ); // // if (getCachedId() == null) { // throw Exception( // "Attempted to initialize an existing wallet using an unknown wallet ID!"); // } // await _prefs.init(); // } // // @override // Future initializeNew( // ({String mnemonicPassphrase, int wordCount})? data, // ) async { // Logging.instance.log( // "Generating new ${coin.prettyName} wallet.", // level: LogLevel.Info, // ); // // if (getCachedId() != null) { // throw Exception( // "Attempted to initialize a new wallet using an existing wallet ID!"); // } // // await _prefs.init(); // // try { // await _generateNewWallet(data); // } catch (e, s) { // Logging.instance.log( // "Exception rethrown from initializeNew(): $e\n$s", // level: LogLevel.Fatal, // ); // rethrow; // } // await Future.wait([ // updateCachedId(walletId), // updateCachedIsFavorite(false), // ]); // } // // Future _generateNewWallet( // ({String mnemonicPassphrase, int wordCount})? data, // ) async { // // Logging.instance // // .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); // // if (!integrationTestFlag) { // // try { // // final features = await electrumXClient.getServerFeatures(); // // Logging.instance.log("features: $features", level: LogLevel.Info); // // switch (coin) { // // case Coin.namecoin: // // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { // // throw Exception("genesis hash does not match main net!"); // // } // // break; // // default: // // throw Exception( // // "Attempted to generate a EthereumWallet using a non eth coin type: ${coin.name}"); // // } // // } catch (e, s) { // // Logging.instance.log("$e/n$s", level: LogLevel.Info); // // } // // } // // // this should never fail - sanity check // if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { // throw Exception( // "Attempted to overwrite mnemonic on generate new wallet!"); // } // // final int strength; // if (data == null || data.wordCount == 12) { // strength = 128; // } else if (data.wordCount == 24) { // strength = 256; // } else { // throw Exception("Invalid word count"); // } // final String mnemonic = bip39.generateMnemonic(strength: strength); // final String passphrase = data?.mnemonicPassphrase ?? ""; // await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); // await _secureStore.write( // key: '${_walletId}_mnemonicPassphrase', // value: passphrase, // ); // // await _generateAndSaveAddress(mnemonic, passphrase); // // Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); // } // // bool _isConnected = false; // // @override // bool get isConnected => _isConnected; // // @override // bool get isRefreshing => refreshMutex; // // bool refreshMutex = false; // // @override // Future> prepareSend({ // required String address, // required Amount amount, // Map? args, // }) async { // final feeRateType = args?["feeRate"]; // int fee = 0; // final feeObject = await fees; // switch (feeRateType) { // case FeeRateType.fast: // fee = feeObject.fast; // break; // case FeeRateType.average: // fee = feeObject.medium; // break; // case FeeRateType.slow: // fee = feeObject.slow; // break; // } // // final feeEstimate = await estimateFeeFor(amount, fee); // // // bool isSendAll = false; // // final availableBalance = balance.spendable; // // if (satoshiAmount == availableBalance) { // // isSendAll = true; // // } // // // // if (isSendAll) { // // //Subtract fee amount from send amount // // satoshiAmount -= feeEstimate; // // } // // final client = getEthClient(); // // final myAddress = await currentReceivingAddress; // final myWeb3Address = web3.EthereumAddress.fromHex(myAddress); // // final est = await client.estimateGas( // sender: myWeb3Address, // to: web3.EthereumAddress.fromHex(address), // gasPrice: web3.EtherAmount.fromUnitAndValue( // web3.EtherUnit.wei, // fee, // ), // amountOfGas: BigInt.from(_gasLimit), // value: web3.EtherAmount.inWei(amount.raw), // ); // // final nonce = args?["nonce"] as int? ?? // await client.getTransactionCount(myWeb3Address, // atBlock: const web3.BlockNum.pending()); // // final nResponse = await EthereumAPI.getAddressNonce(address: myAddress); // print("=============================================================="); // print("ETH client.estimateGas: $est"); // print("ETH estimateFeeFor : $feeEstimate"); // print("ETH nonce custom response: $nResponse"); // print("ETH actual nonce : $nonce"); // print("=============================================================="); // // final tx = web3.Transaction( // to: web3.EthereumAddress.fromHex(address), // gasPrice: web3.EtherAmount.fromUnitAndValue( // web3.EtherUnit.wei, // fee, // ), // maxGas: _gasLimit, // value: web3.EtherAmount.inWei(amount.raw), // nonce: nonce, // ); // // Map txData = { // "fee": feeEstimate, // "feeInWei": fee, // "address": address, // "recipientAmt": amount, // "ethTx": tx, // "chainId": (await client.getChainId()).toInt(), // "nonce": tx.nonce, // }; // // return txData; // } // // @override // Future confirmSend({required Map txData}) async { // web3.Web3Client client = getEthClient(); // // final txid = await client.sendTransaction( // _credentials, // txData["ethTx"] as web3.Transaction, // chainId: txData["chainId"] as int, // ); // // return txid; // } // // @override // Future recoverFromMnemonic({ // required String mnemonic, // String? mnemonicPassphrase, // required int maxUnusedAddressGap, // required int maxNumberOfIndexesToCheck, // required int height, // }) async { // longMutex = true; // final start = DateTime.now(); // // try { // // check to make sure we aren't overwriting a mnemonic // // this should never fail // if ((await mnemonicString) != null || // (await this.mnemonicPassphrase) != null) { // longMutex = false; // throw Exception("Attempted to overwrite mnemonic on restore!"); // } // // await _secureStore.write( // key: '${_walletId}_mnemonic', value: mnemonic.trim()); // await _secureStore.write( // key: '${_walletId}_mnemonicPassphrase', // value: mnemonicPassphrase ?? "", // ); // // String privateKey = // getPrivateKey(mnemonic.trim(), mnemonicPassphrase ?? ""); // _credentials = web3.EthPrivateKey.fromHex(privateKey); // // final address = Address( // walletId: walletId, // value: _credentials.address.hexEip55, // publicKey: [], // maybe store address bytes here? seems a waste of space though // derivationIndex: 0, // derivationPath: DerivationPath()..value = "$hdPathEthereum/0", // type: AddressType.ethereum, // subType: AddressSubType.receiving, // ); // // await db.putAddress(address); // // await Future.wait([ // updateCachedId(walletId), // updateCachedIsFavorite(false), // ]); // } 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); // } // // Future> _fetchAllOwnAddresses() => db // .getAddresses(walletId) // .filter() // .not() // .typeEqualTo(AddressType.nonWallet) // .and() // .group((q) => q // .subTypeEqualTo(AddressSubType.receiving) // .or() // .subTypeEqualTo(AddressSubType.change)) // .findAll(); // // Future refreshIfThereIsNewData() async { // if (longMutex) return false; // if (_hasCalledExit) return false; // final currentChainHeight = await chainHeight; // // try { // bool needsRefresh = false; // Set txnsToCheck = {}; // // for (final String txid in txTracker.pendings) { // if (!txTracker.wasNotifiedConfirmed(txid)) { // txnsToCheck.add(txid); // } // } // // for (String txid in txnsToCheck) { // final response = await EthereumAPI.getEthTransactionByHash(txid); // final txBlockNumber = response.value?.blockNumber; // // if (txBlockNumber != null) { // final int txConfirmations = currentChainHeight - txBlockNumber; // bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS; // if (!isUnconfirmed) { // needsRefresh = true; // break; // } // } // } // if (!needsRefresh) { // var allOwnAddresses = await _fetchAllOwnAddresses(); // final response = await EthereumAPI.getEthTransactions( // address: allOwnAddresses.elementAt(0).value, // ); // if (response.value != null) { // final allTxs = response.value!; // for (final element in allTxs) { // final txid = element.hash; // if ((await db // .getTransactions(walletId) // .filter() // .txidMatches(txid) // .findFirst()) == // null) { // Logging.instance.log( // " txid not found in address history already $txid", // level: LogLevel.Info); // needsRefresh = true; // break; // } // } // } else { // Logging.instance.log( // " refreshIfThereIsNewData get eth transactions failed: ${response.exception}", // level: LogLevel.Error, // ); // } // } // return needsRefresh; // } catch (e, s) { // Logging.instance.log( // "Exception caught in refreshIfThereIsNewData: $e\n$s", // level: LogLevel.Error); // rethrow; // } // } // // Future getAllTxsToWatch() async { // if (_hasCalledExit) return; // List unconfirmedTxnsToNotifyPending = []; // List unconfirmedTxnsToNotifyConfirmed = []; // // final currentChainHeight = await chainHeight; // // final txCount = await db.getTransactions(walletId).count(); // // const paginateLimit = 50; // // for (int i = 0; i < txCount; i += paginateLimit) { // final transactions = await db // .getTransactions(walletId) // .offset(i) // .limit(paginateLimit) // .findAll(); // for (final tx in transactions) { // if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { // // get all transactions that were notified as pending but not as confirmed // if (txTracker.wasNotifiedPending(tx.txid) && // !txTracker.wasNotifiedConfirmed(tx.txid)) { // unconfirmedTxnsToNotifyConfirmed.add(tx); // } // } else { // // get all transactions that were not notified as pending yet // if (!txTracker.wasNotifiedPending(tx.txid)) { // unconfirmedTxnsToNotifyPending.add(tx); // } // } // } // } // // // notify on unconfirmed transactions // for (final tx in unconfirmedTxnsToNotifyPending) { // final confirmations = tx.getConfirmations(currentChainHeight); // // if (tx.type == TransactionType.incoming) { // CryptoNotificationsEventBus.instance.fire( // CryptoNotificationEvent( // title: "Incoming transaction", // walletId: walletId, // date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), // shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, // txid: tx.txid, // confirmations: confirmations, // requiredConfirmations: MINIMUM_CONFIRMATIONS, // walletName: walletName, // coin: coin, // ), // ); // // await txTracker.addNotifiedPending(tx.txid); // } else if (tx.type == TransactionType.outgoing) { // CryptoNotificationsEventBus.instance.fire( // CryptoNotificationEvent( // title: "Sending transaction", // walletId: walletId, // date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), // shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, // txid: tx.txid, // confirmations: confirmations, // requiredConfirmations: MINIMUM_CONFIRMATIONS, // walletName: walletName, // coin: coin, // ), // ); // // await txTracker.addNotifiedPending(tx.txid); // } // } // // // notify on confirmed // for (final tx in unconfirmedTxnsToNotifyConfirmed) { // if (tx.type == TransactionType.incoming) { // CryptoNotificationsEventBus.instance.fire( // CryptoNotificationEvent( // title: "Incoming transaction confirmed", // walletId: walletId, // date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), // shouldWatchForUpdates: false, // txid: tx.txid, // requiredConfirmations: MINIMUM_CONFIRMATIONS, // walletName: walletName, // coin: coin, // ), // ); // // await txTracker.addNotifiedConfirmed(tx.txid); // } else if (tx.type == TransactionType.outgoing) { // CryptoNotificationsEventBus.instance.fire( // CryptoNotificationEvent( // title: "Outgoing transaction confirmed", // walletId: walletId, // date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), // shouldWatchForUpdates: false, // txid: tx.txid, // requiredConfirmations: MINIMUM_CONFIRMATIONS, // walletName: walletName, // coin: coin, // ), // ); // // await txTracker.addNotifiedConfirmed(tx.txid); // } // } // } // // @override // Future updateSentCachedTxData(Map txData) async { // final txid = txData["txid"] as String; // final addressString = checksumEthereumAddress(txData["address"] as String); // final response = await EthereumAPI.getEthTransactionByHash(txid); // // final transaction = Transaction( // walletId: walletId, // txid: txid, // timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, // type: TransactionType.outgoing, // subType: TransactionSubType.none, // // precision may be lost here hence the following amountString // amount: (txData["recipientAmt"] as Amount).raw.toInt(), // amountString: (txData["recipientAmt"] as Amount).toJsonString(), // fee: (txData["fee"] as Amount).raw.toInt(), // height: null, // isCancelled: false, // isLelantus: false, // otherData: null, // slateId: null, // nonce: (txData["nonce"] as int?) ?? // response.value?.nonce.toBigIntFromHex.toInt(), // inputs: [], // outputs: [], // numberOfMessages: null, // ); // // Address? address = await db.getAddress( // walletId, // addressString, // ); // // address ??= Address( // walletId: walletId, // value: addressString, // publicKey: [], // derivationIndex: -1, // derivationPath: null, // type: AddressType.ethereum, // subType: AddressSubType.nonWallet, // ); // // await db.addNewTransactionData( // [ // Tuple2(transaction, address), // ], // walletId, // ); // } // // // @override // // bool validateAddress(String address) { // // return isValidEthereumAddress(address); // // } // // Future _refreshTransactions({bool isRescan = false}) async { // String thisAddress = await currentReceivingAddress; // // int firstBlock = 0; // // if (!isRescan) { // firstBlock = // await db.getTransactions(walletId).heightProperty().max() ?? 0; // // if (firstBlock > 10) { // // add some buffer // firstBlock -= 10; // } // } // // final response = await EthereumAPI.getEthTransactions( // address: thisAddress, // firstBlock: isRescan ? 0 : firstBlock, // includeTokens: true, // ); // // if (response.value == null) { // Logging.instance.log( // "Failed to refresh transactions for ${coin.prettyName} $walletName " // "$walletId: ${response.exception}", // level: LogLevel.Warning, // ); // return; // } // // if (response.value!.isEmpty) { // // no new transactions found // return; // } // // final txsResponse = // await EthereumAPI.getEthTransactionNonces(response.value!); // // if (txsResponse.value != null) { // final allTxs = txsResponse.value!; // final List> txnsData = []; // for (final tuple in allTxs) { // final element = tuple.item1; // // Amount transactionAmount = element.value; // // bool isIncoming; // bool txFailed = false; // if (checksumEthereumAddress(element.from) == thisAddress) { // if (element.isError) { // txFailed = true; // } // isIncoming = false; // } else if (checksumEthereumAddress(element.to) == thisAddress) { // isIncoming = true; // } else { // continue; // } // // //Calculate fees (GasLimit * gasPrice) // // int txFee = element.gasPrice * element.gasUsed; // Amount txFee = element.gasCost; // // final String addressString = checksumEthereumAddress(element.to); // final int height = element.blockNumber; // // final txn = Transaction( // walletId: walletId, // txid: element.hash, // timestamp: element.timestamp, // type: // isIncoming ? TransactionType.incoming : TransactionType.outgoing, // subType: TransactionSubType.none, // amount: transactionAmount.raw.toInt(), // amountString: transactionAmount.toJsonString(), // fee: txFee.raw.toInt(), // height: height, // isCancelled: txFailed, // isLelantus: false, // slateId: null, // otherData: null, // nonce: tuple.item2, // inputs: [], // outputs: [], // numberOfMessages: null, // ); // // Address? transactionAddress = await db // .getAddresses(walletId) // .filter() // .valueEqualTo(addressString) // .findFirst(); // // if (transactionAddress == null) { // if (isIncoming) { // transactionAddress = Address( // walletId: walletId, // value: addressString, // publicKey: [], // derivationIndex: 0, // derivationPath: DerivationPath()..value = "$hdPathEthereum/0", // type: AddressType.ethereum, // subType: AddressSubType.receiving, // ); // } else { // final myRcvAddr = await currentReceivingAddress; // final isSentToSelf = myRcvAddr == addressString; // // transactionAddress = Address( // walletId: walletId, // value: addressString, // publicKey: [], // derivationIndex: isSentToSelf ? 0 : -1, // derivationPath: isSentToSelf // ? (DerivationPath()..value = "$hdPathEthereum/0") // : null, // type: AddressType.ethereum, // subType: isSentToSelf // ? AddressSubType.receiving // : AddressSubType.nonWallet, // ); // } // } // // txnsData.add(Tuple2(txn, transactionAddress)); // } // await db.addNewTransactionData(txnsData, walletId); // // // quick hack to notify manager to call notifyListeners if // // transactions changed // if (txnsData.isNotEmpty) { // GlobalEventBus.instance.fire( // UpdatedInBackgroundEvent( // "Transactions updated/added for: $walletId $walletName ", // walletId, // ), // ); // } // } else { // Logging.instance.log( // "Failed to refresh transactions with nonces for ${coin.prettyName} " // "$walletName $walletId: ${txsResponse.exception}", // level: LogLevel.Warning, // ); // } // } // // void stopNetworkAlivePinging() { // _networkAliveTimer?.cancel(); // _networkAliveTimer = null; // } // // void startNetworkAlivePinging() { // // call once on start right away // _periodicPingCheck(); // // // then periodically check // _networkAliveTimer = Timer.periodic( // Constants.networkAliveTimerDuration, // (_) async { // _periodicPingCheck(); // }, // ); // } // }