From a5d8fdde79b9737bace80880574af9c141a52287 Mon Sep 17 00:00:00 2001
From: julian <julian@cypherstack.com>
Date: Thu, 2 Mar 2023 15:07:25 -0600
Subject: [PATCH] parse eth tx json to data transfer objects

---
 lib/dto/ethereum/eth_token_tx_dto.dart        |  82 +++++++++++
 lib/dto/ethereum/eth_tx_dto.dart              | 131 +++++++++++++++++
 .../coins/ethereum/ethereum_wallet.dart       |  37 +++--
 lib/services/ethereum/ethereum_api.dart       | 132 ++++++------------
 4 files changed, 272 insertions(+), 110 deletions(-)
 create mode 100644 lib/dto/ethereum/eth_token_tx_dto.dart
 create mode 100644 lib/dto/ethereum/eth_tx_dto.dart

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(