stack_wallet/lib/services/coins/ethereum/ethereum_wallet.dart
2024-01-10 10:08:12 -06:00

796 lines
27 KiB
Dart

// /*
// * 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<dynamic>(
// 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<List<Transaction>> 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<String> get currentReceivingAddress async {
// final address = await _currentReceivingAddress;
// return checksumEthereumAddress(
// address?.value ?? _credentials.address.hexEip55);
// }
//
// @override
// Future<void> 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<void> 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<void> _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<Map<String, dynamic>> prepareSend({
// required String address,
// required Amount amount,
// Map<String, dynamic>? 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<String, dynamic> txData = {
// "fee": feeEstimate,
// "feeInWei": fee,
// "address": address,
// "recipientAmt": amount,
// "ethTx": tx,
// "chainId": (await client.getChainId()).toInt(),
// "nonce": tx.nonce,
// };
//
// return txData;
// }
//
// @override
// Future<String> confirmSend({required Map<String, dynamic> 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<void> 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<List<Address>> _fetchAllOwnAddresses() => db
// .getAddresses(walletId)
// .filter()
// .not()
// .typeEqualTo(AddressType.nonWallet)
// .and()
// .group((q) => q
// .subTypeEqualTo(AddressSubType.receiving)
// .or()
// .subTypeEqualTo(AddressSubType.change))
// .findAll();
//
// Future<bool> refreshIfThereIsNewData() async {
// if (longMutex) return false;
// if (_hasCalledExit) return false;
// final currentChainHeight = await chainHeight;
//
// try {
// bool needsRefresh = false;
// Set<String> 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<void> getAllTxsToWatch() async {
// if (_hasCalledExit) return;
// List<Transaction> unconfirmedTxnsToNotifyPending = [];
// List<Transaction> 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<void> updateSentCachedTxData(Map<String, dynamic> 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<void> _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<Tuple2<Transaction, Address?>> 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();
// },
// );
// }
// }