diff --git a/lib/dto/ethereum/eth_token_tx_dto.dart b/lib/dto/ethereum/eth_token_tx_dto.dart new file mode 100644 index 000000000..797fe2f7e --- /dev/null +++ b/lib/dto/ethereum/eth_token_tx_dto.dart @@ -0,0 +1,82 @@ +import 'package:stackwallet/utilities/logger.dart'; + +class EthTokenTxDTO { + final String blockHash; + final int blockNumber; + final int confirmations; + final String contractAddress; + final int cumulativeGasUsed; + final String from; + final int gas; + final BigInt gasPrice; + final int gasUsed; + final String hash; + final String input; + final int logIndex; + final int nonce; + final int timeStamp; + final String to; + final int tokenDecimal; + final String tokenName; + final String tokenSymbol; + final int transactionIndex; + final BigInt value; + + EthTokenTxDTO({ + required this.blockHash, + required this.blockNumber, + required this.confirmations, + required this.contractAddress, + required this.cumulativeGasUsed, + required this.from, + required this.gas, + required this.gasPrice, + required this.gasUsed, + required this.hash, + required this.input, + required this.logIndex, + required this.nonce, + required this.timeStamp, + required this.to, + required this.tokenDecimal, + required this.tokenName, + required this.tokenSymbol, + required this.transactionIndex, + required this.value, + }); + + factory EthTokenTxDTO.fromMap({ + required Map<String, dynamic> map, + }) { + try { + return EthTokenTxDTO( + blockHash: map["blockHash"] as String, + blockNumber: int.parse(map["blockNumber"] as String), + confirmations: int.parse(map["confirmations"] as String), + contractAddress: map["contractAddress"] as String, + cumulativeGasUsed: int.parse(map["cumulativeGasUsed"] as String), + from: map["from"] as String, + gas: int.parse(map["gas"] as String), + gasPrice: BigInt.parse(map["gasPrice"] as String), + gasUsed: int.parse(map["gasUsed"] as String), + hash: map["hash"] as String, + input: map["input"] as String, + logIndex: int.parse(map["logIndex"] as String? ?? "-1"), + nonce: int.parse(map["nonce"] as String), + timeStamp: int.parse(map["timeStamp"] as String), + to: map["to"] as String, + tokenDecimal: int.parse(map["tokenDecimal"] as String), + tokenName: map["tokenName"] as String, + tokenSymbol: map["tokenSymbol"] as String, + transactionIndex: int.parse(map["transactionIndex"] as String), + value: BigInt.parse(map["value"] as String), + ); + } catch (e, s) { + Logging.instance.log( + "EthTokenTxDTO.fromMap() failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } +} diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart new file mode 100644 index 000000000..f839711bb --- /dev/null +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; + +class EthTxDTO { + EthTxDTO({ + required this.hash, + required this.blockHash, + required this.blockNumber, + required this.transactionIndex, + required this.timestamp, + required this.from, + required this.to, + required this.value, + required this.gas, + required this.gasPrice, + required this.maxFeePerGas, + required this.maxPriorityFeePerGas, + required this.isError, + required this.hasToken, + required this.compressedTx, + required this.gasCost, + required this.gasUsed, + }); + + factory EthTxDTO.fromMap(Map<String, dynamic> map) => EthTxDTO( + hash: map['hash'] as String, + blockHash: map['blockHash'] as String, + blockNumber: map['blockNumber'] as int, + transactionIndex: map['transactionIndex'] as int, + timestamp: map['timestamp'] as int, + from: map['from'] as String, + to: map['to'] as String, + value: map['value'] as int, + gas: map['gas'] as int, + gasPrice: map['gasPrice'] as int, + maxFeePerGas: map['maxFeePerGas'] as int, + maxPriorityFeePerGas: map['maxPriorityFeePerGas'] as int, + isError: map['isError'] as int, + hasToken: map['hasToken'] as int, + compressedTx: map['compressedTx'] as String, + gasCost: map['gasCost'] as int, + gasUsed: map['gasUsed'] as int, + ); + + factory EthTxDTO.fromJsonString(String jsonString) => EthTxDTO.fromMap( + Map<String, dynamic>.from( + jsonDecode(jsonString) as Map, + ), + ); + + final String hash; + final String blockHash; + final int blockNumber; + final int transactionIndex; + final int timestamp; + final String from; + final String to; + final int value; + final int gas; + final int gasPrice; + final int maxFeePerGas; + final int maxPriorityFeePerGas; + final int isError; + final int hasToken; + final String compressedTx; + final int gasCost; + final int gasUsed; + + EthTxDTO copyWith({ + String? hash, + String? blockHash, + int? blockNumber, + int? transactionIndex, + int? timestamp, + String? from, + String? to, + int? value, + int? gas, + int? gasPrice, + int? maxFeePerGas, + int? maxPriorityFeePerGas, + int? isError, + int? hasToken, + String? compressedTx, + int? gasCost, + int? gasUsed, + }) => + EthTxDTO( + hash: hash ?? this.hash, + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + transactionIndex: transactionIndex ?? this.transactionIndex, + timestamp: timestamp ?? this.timestamp, + from: from ?? this.from, + to: to ?? this.to, + value: value ?? this.value, + gas: gas ?? this.gas, + gasPrice: gasPrice ?? this.gasPrice, + maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, + isError: isError ?? this.isError, + hasToken: hasToken ?? this.hasToken, + compressedTx: compressedTx ?? this.compressedTx, + gasCost: gasCost ?? this.gasCost, + gasUsed: gasUsed ?? this.gasUsed, + ); + + Map<String, dynamic> toMap() { + final map = <String, dynamic>{}; + map['hash'] = hash; + map['blockHash'] = blockHash; + map['blockNumber'] = blockNumber; + map['transactionIndex'] = transactionIndex; + map['timestamp'] = timestamp; + map['from'] = from; + map['to'] = to; + map['value'] = value; + map['gas'] = gas; + map['gasPrice'] = gasPrice; + map['maxFeePerGas'] = maxFeePerGas; + map['maxPriorityFeePerGas'] = maxPriorityFeePerGas; + map['isError'] = isError; + map['hasToken'] = hasToken; + map['compressedTx'] = compressedTx; + map['gasCost'] = gasCost; + map['gasUsed'] = gasUsed; + return map; + } + + @override + String toString() => jsonEncode(toMap()); +} diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 699f43d34..d7fb2cb48 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -567,14 +567,13 @@ class EthereumWallet extends CoinServiceAPI } if (!needsRefresh) { var allOwnAddresses = await _fetchAllOwnAddresses(); - AddressTransaction addressTransactions = - await EthereumAPI.fetchAddressTransactions( + final response = await EthereumAPI.getEthTransactions( allOwnAddresses.elementAt(0).value, ); - if (addressTransactions.message == "OK") { - final allTxs = addressTransactions.result; + if (response.value != null) { + final allTxs = response.value!; for (final element in allTxs) { - final txid = element["hash"] as String; + final txid = element.hash; if ((await db .getTransactions(walletId) .filter() @@ -582,7 +581,7 @@ class EthereumWallet extends CoinServiceAPI .findFirst()) == null) { Logging.instance.log( - " txid not found in address history already ${element['hash']}", + " txid not found in address history already $txid", level: LogLevel.Info); needsRefresh = true; break; @@ -845,20 +844,18 @@ class EthereumWallet extends CoinServiceAPI Future<void> _refreshTransactions() async { String thisAddress = await currentReceivingAddress; - AddressTransaction txs = - await EthereumAPI.fetchAddressTransactions(thisAddress); + final txsResponse = await EthereumAPI.getEthTransactions(thisAddress); - if (txs.message == "OK") { - final allTxs = txs.result; + if (txsResponse.value != null) { + final allTxs = txsResponse.value!; final List<Tuple2<Transaction, Address?>> txnsData = []; for (final element in allTxs) { - int transactionAmount = int.parse(element['value'].toString()); + int transactionAmount = element.value; bool isIncoming; bool txFailed = false; - if (checksumEthereumAddress(element["from"].toString()) == - thisAddress) { - if (!(int.parse(element["isError"] as String) == 0)) { + if (checksumEthereumAddress(element.from) == thisAddress) { + if (element.isError != 0) { txFailed = true; } isIncoming = false; @@ -867,16 +864,16 @@ class EthereumWallet extends CoinServiceAPI } //Calculate fees (GasLimit * gasPrice) - int txFee = int.parse(element['gasPrice'].toString()) * - int.parse(element['gasUsed'].toString()); + // int txFee = element.gasPrice * element.gasUsed; + int txFee = element.gasCost; - final String addressString = element["to"] as String; - final int height = int.parse(element['blockNumber'].toString()); + final String addressString = checksumEthereumAddress(element.to); + final int height = element.blockNumber; final txn = Transaction( walletId: walletId, - txid: element["hash"] as String, - timestamp: int.parse(element["timeStamp"].toString()), + txid: element.hash, + timestamp: element.timestamp, type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.none, diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 15ba969d3..b73b2f1f4 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -3,6 +3,8 @@ import 'dart:math'; import 'package:decimal/decimal.dart'; import 'package:http/http.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; +import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart'; import 'package:stackwallet/models/ethereum/erc20_token.dart'; import 'package:stackwallet/models/ethereum/erc721_token.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; @@ -11,87 +13,6 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/logger.dart'; -class EthTokenTx { - final String blockHash; - final int blockNumber; - final int confirmations; - final String contractAddress; - final int cumulativeGasUsed; - final String from; - final int gas; - final BigInt gasPrice; - final int gasUsed; - final String hash; - final String input; - final int logIndex; - final int nonce; - final int timeStamp; - final String to; - final int tokenDecimal; - final String tokenName; - final String tokenSymbol; - final int transactionIndex; - final BigInt value; - - EthTokenTx({ - required this.blockHash, - required this.blockNumber, - required this.confirmations, - required this.contractAddress, - required this.cumulativeGasUsed, - required this.from, - required this.gas, - required this.gasPrice, - required this.gasUsed, - required this.hash, - required this.input, - required this.logIndex, - required this.nonce, - required this.timeStamp, - required this.to, - required this.tokenDecimal, - required this.tokenName, - required this.tokenSymbol, - required this.transactionIndex, - required this.value, - }); - - factory EthTokenTx.fromMap({ - required Map<String, dynamic> map, - }) { - try { - return EthTokenTx( - blockHash: map["blockHash"] as String, - blockNumber: int.parse(map["blockNumber"] as String), - confirmations: int.parse(map["confirmations"] as String), - contractAddress: map["contractAddress"] as String, - cumulativeGasUsed: int.parse(map["cumulativeGasUsed"] as String), - from: map["from"] as String, - gas: int.parse(map["gas"] as String), - gasPrice: BigInt.parse(map["gasPrice"] as String), - gasUsed: int.parse(map["gasUsed"] as String), - hash: map["hash"] as String, - input: map["input"] as String, - logIndex: int.parse(map["logIndex"] as String? ?? "-1"), - nonce: int.parse(map["nonce"] as String), - timeStamp: int.parse(map["timeStamp"] as String), - to: map["to"] as String, - tokenDecimal: int.parse(map["tokenDecimal"] as String), - tokenName: map["tokenName"] as String, - tokenSymbol: map["tokenSymbol"] as String, - transactionIndex: int.parse(map["transactionIndex"] as String), - value: BigInt.parse(map["value"] as String), - ); - } catch (e, s) { - Logging.instance.log( - "EthTokenTx.fromMap() failed: $e\n$s", - level: LogLevel.Fatal, - ); - rethrow; - } - } -} - class EthApiException with Exception { EthApiException(this.message); @@ -123,7 +44,7 @@ abstract class EthereumAPI { static const gasTrackerUrl = "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; - static Future<AddressTransaction> fetchAddressTransactions( + static Future<EthereumResponse<List<EthTxDTO>>> getEthTransactions( String address) async { try { final response = await get( @@ -135,19 +56,50 @@ abstract class EthereumAPI { ); // "$etherscanApi?module=account&action=txlist&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + if (response.statusCode == 200) { - return AddressTransaction.fromJson( - jsonDecode(response.body)["data"] as List); + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = json["data"] as List?; + + final List<EthTxDTO> txns = []; + for (final map in list!) { + txns.add(EthTxDTO.fromMap(Map<String, dynamic>.from(map as Map))); + } + return EthereumResponse( + txns, + null, + ); + } else { + throw EthApiException( + "getEthTransactions($address) response is empty but status code is " + "${response.statusCode}", + ); + } } else { - throw Exception( - 'ERROR GETTING TRANSACTIONS WITH STATUS ${response.statusCode}'); + throw EthApiException( + "getEthTransactions($address) failed with status code: " + "${response.statusCode}", + ); } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); } catch (e, s) { - throw Exception('ERROR GETTING TRANSACTIONS ${e.toString()}'); + Logging.instance.log( + "getEthTransactions(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); } } - static Future<EthereumResponse<List<EthTokenTx>>> getTokenTransactions({ + static Future<EthereumResponse<List<EthTokenTxDTO>>> getTokenTransactions({ required String address, String? contractAddress, int? startBlock, @@ -170,9 +122,9 @@ abstract class EthereumAPI { if (json["message"] == "OK") { final result = List<Map<String, dynamic>>.from(json["result"] as List); - final List<EthTokenTx> tokenTxns = []; + final List<EthTokenTxDTO> tokenTxns = []; for (final map in result) { - tokenTxns.add(EthTokenTx.fromMap(map: map)); + tokenTxns.add(EthTokenTxDTO.fromMap(map: map)); } return EthereumResponse(