import 'dart:async'; import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; import 'package:stackwallet/models/paymint/utxo_model.dart'; import 'package:stackwallet/services/price.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:string_to_hex/string_to_hex.dart'; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart' as Transaction; import 'package:http/http.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/services/coins/coin_service.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'; const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"; class EthereumWallet extends CoinServiceAPI { @override set isFavorite(bool markFavorite) { DB.instance.put( boxName: walletId, key: "isFavorite", value: markFavorite); } @override bool get isFavorite { try { return DB.instance.get(boxName: walletId, key: "isFavorite") as bool; } catch (e, s) { Logging.instance.log( "isFavorite fetch failed (returning false by default): $e\n$s", level: LogLevel.Error); return false; } } @override Coin get coin => _coin; late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; final _prefs = Prefs.instance; final _client = Web3Client( "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); late EthPrivateKey _credentials; EthereumWallet( {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; } @override bool shouldAutoSync = false; @override String get walletName => _walletName; late String _walletName; late Coin _coin; @override // TODO: implement allOwnAddresses Future> get allOwnAddresses => _allOwnAddresses ??= _fetchAllOwnAddresses(); Future>? _allOwnAddresses; Future> _fetchAllOwnAddresses() async { List addresses = []; final ownAddress = _credentials.address; addresses.add(ownAddress.toString()); return addresses; } @override Future get availableBalance async { EtherAmount ethBalance = await _client.getBalance(_credentials.address); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); } @override // TODO: implement balanceMinusMaxFee Future get balanceMinusMaxFee => throw UnimplementedError(); @override Future confirmSend({required Map txData}) async { print("CALLING CONFIRM SEND WITH $txData"); final gasPrice = await _client.getGasPrice(); print("GAS PRICE IS $gasPrice"); // final fee = "21,000" * (gasPrice! + 2 ); final tx = Transaction.Transaction( to: EthereumAddress.fromHex(txData['addresss'] as String), gasPrice: gasPrice, maxGas: 21000, value: EtherAmount.fromUnitAndValue(EtherUnit.ether, 1)); final transaction = await _client.sendTransaction( _credentials, tx, ); return transaction; } @override Future get currentReceivingAddress async { final _currentReceivingAddress = _credentials.address; return _currentReceivingAddress.toString(); } @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { print("CALLING ESTIMATE FEE"); // TODO: implement estimateFeeFor // throw UnimplementedError(); return 1; } @override Future exit() { // TODO: implement exit throw UnimplementedError(); } @override Future get fees => _feeObject ??= _getFees(); Future? _feeObject; Future _getFees() async { return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, numberOfBlocksSlow: 10, fast: 1, medium: 1, slow: 1); } @override Future fullRescan( int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { // TODO: implement fullRescan throw UnimplementedError(); } @override Future generateNewAddress() { // TODO: implement generateNewAddress throw UnimplementedError(); } bool _hasCalledExit = false; @override bool get hasCalledExit => _hasCalledExit; @override Future initializeExisting() async { Logging.instance.log("Opening existing ${coin.prettyName} wallet.", level: LogLevel.Info); if ((DB.instance.get(boxName: walletId, key: "id")) == null) { throw Exception( "Attempted to initialize an existing wallet using an unknown wallet ID!"); } await _prefs.init(); final data = DB.instance.get(boxName: walletId, key: "latest_tx_model") as TransactionData?; if (data != null) { _transactionData = Future(() => data); } } @override Future initializeNew() async { await _prefs.init(); final String mnemonic = bip39.generateMnemonic(strength: 256); print("Mnemonic is $mnemonic"); _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); final String password = generatePassword(); var rng = Random.secure(); Wallet wallet = Wallet.createNew(_credentials, password, rng); await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance.put( boxName: walletId, key: 'receivingAddresses', value: ["0"]); 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); } bool _isConnected = false; @override bool get isConnected => _isConnected; bool refreshMutex = false; @override bool get isRefreshing => refreshMutex; @override // TODO: implement maxFee Future get maxFee => throw UnimplementedError(); @override Future> get mnemonic => _getMnemonicList(); Future get chainHeight async { try { final result = await _client.getBlockNumber(); return result; } catch (e, s) { Logging.instance.log("Exception caught in chainHeight: $e\n$s", level: LogLevel.Error); return -1; } } int get storedChainHeight { final storedHeight = DB.instance .get(boxName: walletId, key: "storedChainHeight") as int?; return storedHeight ?? 0; } Future updateStoredChainHeight({required int newHeight}) async { await DB.instance.put( boxName: walletId, key: "storedChainHeight", value: newHeight); } Future> _getMnemonicList() async { final mnemonicString = await _secureStore.read(key: '${_walletId}_mnemonic'); if (mnemonicString == null) { return []; } final List data = mnemonicString.split(' '); return data; } @override // TODO: implement pendingBalance Future get pendingBalance => throw UnimplementedError(); // Future transactionFee(int satoshiAmount) {} @override Future> prepareSend( {required String address, required int satoshiAmount, Map? args}) async { print("CALLING PREPARE SEND ${Decimal.fromInt(satoshiAmount)}"); Map txData = { "fee": 0, "addresss": address, "recipientAmt": satoshiAmount, }; return txData; } @override Future recoverFromMnemonic( {required String mnemonic, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height}) async { await _prefs.init(); print("Mnemonic is $mnemonic"); _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); final String password = generatePassword(); var rng = Random.secure(); Wallet wallet = Wallet.createNew(_credentials, password, rng); await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance.put( boxName: walletId, key: 'receivingAddresses', value: ["0"]); 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); } @override Future refresh() async { print("CALLING REFRESH"); if (refreshMutex) { Logging.instance.log("$walletId $walletName refreshMutex denied", level: LogLevel.Info); return; } else { refreshMutex = true; } print("SYNC STATUS IS "); final blockNumber = await _client.getBlockNumber(); print("BLOCK NUMBER IS ::: ${blockNumber}"); try { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.syncing, walletId, coin, ), ); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); final currentHeight = await chainHeight; const storedHeight = 1; //await storedChainHeight; Logging.instance .log("chain height: $currentHeight", level: LogLevel.Info); Logging.instance .log("cached height: $storedHeight", level: LogLevel.Info); if (currentHeight != storedHeight) { if (currentHeight != -1) { // -1 failed to fetch current height unawaited(updateStoredChainHeight(newHeight: currentHeight)); } final newTxData = _fetchTransactionData(); print("RETREIVED TX DATA IS $newTxData"); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.50, walletId)); } } catch (error, strace) { refreshMutex = false; 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.Warning); } } @override Future send( {required String toAddress, required int amount, Map args = const {}}) { // TODO: implement send throw UnimplementedError(); } @override Future testNetworkConnection() { // TODO: implement testNetworkConnection throw UnimplementedError(); } @override // TODO: Check difference between total and available balance for eth Future get totalBalance async { EtherAmount ethBalance = await _client.getBalance(_credentials.address); print( "BALANCE NOW IS ${ethBalance.getValueInUnit(EtherUnit.ether).toString()}"); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); } @override Future get transactionData => _transactionData ??= _fetchTransactionData(); Future? _transactionData; TransactionData? cachedTxData; @override // TODO: implement unspentOutputs Future> get unspentOutputs => throw UnimplementedError(); @override Future updateNode(bool shouldRefresh) { // TODO: implement updateNode throw UnimplementedError(); } @override Future updateSentCachedTxData(Map txData) { // TODO: implement updateSentCachedTxData throw UnimplementedError(); } @override bool validateAddress(String address) { // TODO: implement validateAddress return true; } Future _fetchTransactionData() async { String thisAddress = await currentReceivingAddress; int currentBlock = await chainHeight; var balance = await availableBalance; var n = _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); print("THIS CURRECT ADDRESS IS $thisAddress"); print("THIS CURRECT BLOCK IS $currentBlock"); print("THIS BALANCE IS $balance"); print("THIS COUNT TRANSACTIONS IS $n"); throw UnimplementedError(); } @override String get walletId => _walletId; late String _walletId; @override @override set walletName(String newName) => _walletName = newName; }