import 'dart:async'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.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/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.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/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.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/extensions/impl/contract_abi.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; class EthTokenWallet extends ChangeNotifier with EthTokenCache { final EthereumWallet ethWallet; final TransactionNotificationTracker tracker; final SecureStorageInterface _secureStore; // late web3dart.EthereumAddress _contractAddress; late web3dart.EthPrivateKey _credentials; late web3dart.DeployedContract _deployedContract; late web3dart.ContractFunction _sendFunction; late web3dart.Web3Client _client; static const _gasLimit = 200000; EthTokenWallet({ required EthContract token, required this.ethWallet, required SecureStorageInterface secureStore, required this.tracker, }) : _secureStore = secureStore, _tokenContract = token { // _contractAddress = web3dart.EthereumAddress.fromHex(token.address); initCache(ethWallet.walletId, token); } EthContract get tokenContract => _tokenContract; EthContract _tokenContract; Balance get balance => _balance ??= getCachedBalance(); Balance? _balance; Coin get coin => Coin.ethereum; 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 = estimateFeeFor(fee); final client = await getEthClient(); final myAddress = await currentReceivingAddress; final myWeb3Address = web3dart.EthereumAddress.fromHex(myAddress); final nonce = args?["nonce"] as int? ?? await client.getTransactionCount(myWeb3Address, atBlock: const web3dart.BlockNum.pending()); final tx = web3dart.Transaction.callContract( contract: _deployedContract, function: _sendFunction, parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw], maxGas: _gasLimit, gasPrice: web3dart.EtherAmount.fromUnitAndValue( web3dart.EtherUnit.wei, fee, ), nonce: nonce, ); Map txData = { "fee": feeEstimate, "feeInWei": fee, "address": address, "recipientAmt": amount, "ethTx": tx, "chainId": (await client.getChainId()).toInt(), "nonce": tx.nonce, }; return txData; } Future confirmSend({required Map txData}) async { try { final txid = await _client.sendTransaction( _credentials, txData["ethTx"] as web3dart.Transaction, chainId: txData["chainId"] as int, ); try { txData["txid"] = txid; await updateSentCachedTxData(txData); } catch (e, s) { // do not rethrow as that would get handled as a send failure further up // also this is not critical code and transaction should show up on \ // refresh regardless Logging.instance.log("$e\n$s", level: LogLevel.Warning); } notifyListeners(); return txid; } catch (e) { // rethrow to pass error in alert rethrow; } } 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: ethWallet.walletId, txid: txid, timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: TransactionType.outgoing, subType: TransactionSubType.ethToken, // 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: tokenContract.address, slateId: null, nonce: (txData["nonce"] as int?) ?? response.value?.nonce.toBigIntFromHex.toInt(), inputs: [], outputs: [], numberOfMessages: null, ); Address? address = await ethWallet.db.getAddress( ethWallet.walletId, addressString, ); address ??= Address( walletId: ethWallet.walletId, value: addressString, publicKey: [], derivationIndex: -1, derivationPath: null, type: AddressType.ethereum, subType: AddressSubType.nonWallet, ); await ethWallet.db.addNewTransactionData( [ Tuple2(transaction, address), ], ethWallet.walletId, ); } Future get currentReceivingAddress async { final address = await _currentReceivingAddress; return checksumEthereumAddress( address?.value ?? _credentials.address.toString()); } Future get _currentReceivingAddress => ethWallet.db .getAddresses(ethWallet.walletId) .filter() .typeEqualTo(AddressType.ethereum) .subTypeEqualTo(AddressSubType.receiving) .sortByDerivationIndexDesc() .findFirst(); Amount estimateFeeFor(int feeRate) { return estimateFee(feeRate, _gasLimit, coin.decimals); } Future get fees => EthereumAPI.getFees(); Future _updateTokenABI({ required EthContract forContract, required String usingContractAddress, }) async { final abiResponse = await EthereumAPI.getTokenAbi( name: forContract.name, contractAddress: usingContractAddress, ); // Fetch token ABI so we can call token functions if (abiResponse.value != null) { final updatedToken = forContract.copyWith(abi: abiResponse.value!); // Store updated contract final id = await MainDB.instance.putEthContract(updatedToken); return updatedToken..id = id; } else { throw abiResponse.exception!; } } Future initialize() async { final contractAddress = web3dart.EthereumAddress.fromHex(tokenContract.address); if (tokenContract.abi == null) { _tokenContract = await _updateTokenABI( forContract: tokenContract, usingContractAddress: contractAddress.hex, ); } String? mnemonicString = await ethWallet.mnemonicString; //Get private key for given mnemonic String privateKey = getPrivateKey( mnemonicString!, (await ethWallet.mnemonicPassphrase) ?? "", ); _credentials = web3dart.EthPrivateKey.fromHex(privateKey); try { _deployedContract = web3dart.DeployedContract( ContractAbiExtensions.fromJsonList( jsonList: tokenContract.abi!, name: tokenContract.name, ), contractAddress, ); } catch (_) { rethrow; } try { _sendFunction = _deployedContract.function('transfer'); } catch (_) { //==================================================================== // final list = List>.from( // jsonDecode(tokenContract.abi!) as List); // final functionNames = list.map((e) => e["name"] as String); // // if (!functionNames.contains("balanceOf")) { // list.add( // { // "encoding": "0x70a08231", // "inputs": [ // {"name": "account", "type": "address"} // ], // "name": "balanceOf", // "outputs": [ // {"name": "val_0", "type": "uint256"} // ], // "signature": "balanceOf(address)", // "type": "function" // }, // ); // } // // if (!functionNames.contains("transfer")) { // list.add( // { // "encoding": "0xa9059cbb", // "inputs": [ // {"name": "dst", "type": "address"}, // {"name": "rawAmount", "type": "uint256"} // ], // "name": "transfer", // "outputs": [ // {"name": "val_0", "type": "bool"} // ], // "signature": "transfer(address,uint256)", // "type": "function" // }, // ); // } //-------------------------------------------------------------------- //==================================================================== // function not found so likely a proxy so we need to fetch the impl //==================================================================== // final updatedToken = tokenContract.copyWith(abi: jsonEncode(list)); // // Store updated contract // final id = await MainDB.instance.putEthContract(updatedToken); // _tokenContract = updatedToken..id = id; //-------------------------------------------------------------------- final contractAddressResponse = await EthereumAPI.getProxyTokenImplementationAddress( contractAddress.hex); if (contractAddressResponse.value != null) { _tokenContract = await _updateTokenABI( forContract: tokenContract, usingContractAddress: contractAddressResponse.value!, ); } else { throw contractAddressResponse.exception!; } //==================================================================== } try { _deployedContract = web3dart.DeployedContract( ContractAbiExtensions.fromJsonList( jsonList: tokenContract.abi!, name: tokenContract.name, ), contractAddress, ); } catch (_) { rethrow; } _sendFunction = _deployedContract.function('transfer'); _client = await getEthClient(); unawaited(refresh()); } bool get isRefreshing => _refreshLock; bool _refreshLock = false; Future refresh() async { if (!_refreshLock) { _refreshLock = true; try { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.syncing, ethWallet.walletId + tokenContract.address, coin, ), ); await refreshCachedBalance(); await _refreshTransactions(); } catch (e, s) { Logging.instance.log( "Caught exception in ${tokenContract.name} ${ethWallet.walletName} ${ethWallet.walletId} refresh(): $e\n$s", level: LogLevel.Warning, ); } finally { _refreshLock = false; GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.synced, ethWallet.walletId + tokenContract.address, coin, ), ); notifyListeners(); } } } Future refreshCachedBalance() async { final response = await EthereumAPI.getWalletTokenBalance( address: _credentials.address.hex, contractAddress: tokenContract.address, ); if (response.value != null) { await updateCachedBalance( Balance( total: response.value!, spendable: response.value!, blockedTotal: Amount( rawValue: BigInt.zero, fractionDigits: tokenContract.decimals, ), pendingSpendable: Amount( rawValue: BigInt.zero, fractionDigits: tokenContract.decimals, ), ), ); notifyListeners(); } else { Logging.instance.log( "CachedEthTokenBalance.fetchAndUpdateCachedBalance failed: ${response.exception}", level: LogLevel.Warning, ); } } Future> get transactions => ethWallet.db .getTransactions(ethWallet.walletId) .filter() .otherDataEqualTo(tokenContract.address) .sortByTimestampDesc() .findAll(); String _addressFromTopic(String topic) => checksumEthereumAddress("0x${topic.substring(topic.length - 40)}"); Future _refreshTransactions() async { String addressString = checksumEthereumAddress(await currentReceivingAddress); final response = await EthereumAPI.getTokenTransactions( address: addressString, tokenContractAddress: tokenContract.address, ); if (response.value == null) { throw response.exception ?? Exception("Failed to fetch token transaction data"); } // no need to continue if no transactions found if (response.value!.isEmpty) { return; } final response2 = await EthereumAPI.getEthTokenTransactionsByTxids( response.value!.map((e) => e.transactionHash).toList(), ); if (response2.value == null) { throw response2.exception ?? Exception("Failed to fetch token transactions"); } final List> data = []; for (final tokenDto in response.value!) { data.add( Tuple2( tokenDto, response2.value!.firstWhere( (e) => e.hash == tokenDto.transactionHash, ), ), ); } final List> txnsData = []; for (final tuple in data) { // ignore all non Transfer events (for now) if (tuple.item1.topics[0] == kTransferEventSignature) { final Amount amount; String fromAddress, toAddress; amount = Amount( rawValue: tuple.item1.data.toBigIntFromHex, fractionDigits: tokenContract.decimals, ); fromAddress = _addressFromTopic( tuple.item1.topics[1], ); toAddress = _addressFromTopic( tuple.item1.topics[2], ); bool isIncoming; bool isSentToSelf = false; if (fromAddress == addressString) { isIncoming = false; if (toAddress == addressString) { isSentToSelf = true; } } else if (toAddress == addressString) { isIncoming = true; } else { throw Exception("Unknown token transaction found for " "${ethWallet.walletName} ${ethWallet.walletId}: " "${tuple.item1.toString()}"); } final txn = Transaction( walletId: ethWallet.walletId, txid: tuple.item1.transactionHash, timestamp: tuple.item2.timestamp, type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.ethToken, amount: amount.raw.toInt(), amountString: amount.toJsonString(), fee: (tuple.item2.gasUsed.raw * tuple.item2.gasPrice.raw).toInt(), height: tuple.item1.blockNumber, isCancelled: false, isLelantus: false, slateId: null, nonce: tuple.item2.nonce, otherData: tuple.item1.address, inputs: [], outputs: [], numberOfMessages: null, ); Address? transactionAddress = await ethWallet.db .getAddresses(ethWallet.walletId) .filter() .valueEqualTo(toAddress) .findFirst(); transactionAddress ??= Address( walletId: ethWallet.walletId, value: toAddress, 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 ethWallet.db.addNewTransactionData(txnsData, ethWallet.walletId); // quick hack to notify manager to call notifyListeners if // transactions changed if (txnsData.isNotEmpty) { GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( "${tokenContract.name} transactions updated/added for: ${ethWallet.walletId} ${ethWallet.walletName}", ethWallet.walletId, ), ); } } bool validateAddress(String address) { return isValidEthereumAddress(address); } NodeModel getCurrentNode() { return NodeService(secureStorageInterface: _secureStore) .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } Future getEthClient() async { final node = getCurrentNode(); return web3dart.Web3Client(node.host, Client()); } }