/* * 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:convert'; import 'package:http/http.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/dto/ethereum/eth_tx_dto.dart'; import 'package:stackwallet/dto/ethereum/pending_eth_tx_dto.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/networking/http.dart'; import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:tuple/tuple.dart'; class EthApiException implements Exception { EthApiException(this.message); final String message; @override String toString() => "$runtimeType: $message"; } class EthereumResponse { EthereumResponse(this.value, this.exception); final T? value; final EthApiException? exception; @override toString() => "EthereumResponse: { value: $value, exception: $exception }"; } abstract class EthereumAPI { static String get stackBaseServer => DefaultNodes.ethereum.host; static HTTP client = HTTP(); static Future>> getEthTransactions({ required String address, int firstBlock = 0, bool includeTokens = false, }) async { try { final response = await client.get( url: Uri.parse( "$stackBaseServer/export?addrs=$address&firstBlock=$firstBlock", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { if (response.body.isNotEmpty) { final json = jsonDecode(response.body) as Map; final list = json["data"] as List?; final List txns = []; for (final map in list!) { final txn = EthTxDTO.fromMap(Map.from(map as Map)); if (txn.hasToken == 0 || includeTokens) { txns.add(txn); } } return EthereumResponse( txns, null, ); } else { // nice that the api returns an empty body instead of being // consistent and returning a json object with no transactions return EthereumResponse( [], null, ); } } else { throw EthApiException( "getEthTransactions($address) failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getEthTransactions($address): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } static Future> getEthTransactionByHash( String txid) async { try { final response = await post( Uri.parse( "$stackBaseServer/v1/mainnet", ), headers: {'Content-Type': 'application/json'}, body: json.encode({ "jsonrpc": "2.0", "method": "eth_getTransactionByHash", "params": [ txid, ], "id": DateTime.now().millisecondsSinceEpoch, }), ); if (response.statusCode == 200) { if (response.body.isNotEmpty) { try { final json = jsonDecode(response.body) as Map; final result = json["result"] as Map; return EthereumResponse( PendingEthTxDto.fromMap(Map.from(result)), null, ); } catch (_) { throw EthApiException( "getEthTransactionByHash($txid) failed with response: " "${response.body}", ); } } else { throw EthApiException( "getEthTransactionByHash($txid) response is empty but status code is " "${response.statusCode}", ); } } else { throw EthApiException( "getEthTransactionByHash($txid) failed with status code: " "${response.statusCode}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getEthTransactionByHash($txid): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } static Future>>> getEthTransactionNonces( List txns, ) async { try { final response = await client.get( url: Uri.parse( "$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}&raw=true", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { if (response.body.isNotEmpty) { final json = jsonDecode(response.body) as Map; final list = List>.from(json["data"] as List); final List> result = []; for (final dto in txns) { final data = list.firstWhere((e) => e["hash"] == dto.hash, orElse: () => {}); final nonce = (data["nonce"] as String?)?.toBigIntFromHex.toInt(); result.add(Tuple2(dto, nonce)); } return EthereumResponse( result, null, ); } else { // nice that the api returns an empty body instead of being // consistent and returning a json object with no transactions return EthereumResponse( [], null, ); } } else { throw EthApiException( "getEthTransactionNonces($txns) failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getEthTransactionNonces($txns): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } static Future>> getEthTokenTransactionsByTxids(List txids) async { try { final response = await client.get( url: Uri.parse( "$stackBaseServer/transactions?transactions=${txids.join(" ")}", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { if (response.body.isNotEmpty) { final json = jsonDecode(response.body) as Map; final list = json["data"] as List?; final List txns = []; for (final map in list!) { final txn = EthTokenTxExtraDTO.fromMap( Map.from(map as Map), ); txns.add(txn); } return EthereumResponse( txns, null, ); } else { throw EthApiException( "getEthTokenTransactionsByTxids($txids) response is empty but status code is " "${response.code}", ); } } else { throw EthApiException( "getEthTokenTransactionsByTxids($txids) failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getEthTokenTransactionsByTxids($txids): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } static Future>> getTokenTransactions({ required String address, required String tokenContractAddress, }) async { try { final response = await client.get( url: Uri.parse( "$stackBaseServer/export?addrs=$address&emitter=$tokenContractAddress&logs=true", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { if (response.body.isNotEmpty) { final json = jsonDecode(response.body) as Map; final list = json["data"] as List?; final List txns = []; for (final map in list!) { final txn = EthTokenTxDto.fromMap(Map.from(map as Map)); txns.add(txn); } return EthereumResponse( txns, null, ); } else { // nice that the api returns an empty body instead of being // consistent and returning a json object with no transactions return EthereumResponse( [], null, ); } } else { throw EthApiException( "getTokenTransactions($address, $tokenContractAddress) failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getTokenTransactions($address, $tokenContractAddress): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } // ONLY FETCHES WALLET TOKENS WITH A NON ZERO BALANCE // static Future>> getWalletTokens({ // required String address, // }) async { // try { // final uri = Uri.parse( // "$blockExplorer?module=account&action=tokenlist&address=$address", // ); // final response = await get(uri); // // if (response.statusCode == 200) { // final json = jsonDecode(response.body); // if (json["message"] == "OK") { // final result = // List>.from(json["result"] as List); // final List tokens = []; // for (final map in result) { // if (map["type"] == "ERC-20") { // tokens.add( // Erc20Token( // balance: int.parse(map["balance"] as String), // contractAddress: map["contractAddress"] as String, // decimals: int.parse(map["decimals"] as String), // name: map["name"] as String, // symbol: map["symbol"] as String, // ), // ); // } else if (map["type"] == "ERC-721") { // tokens.add( // Erc721Token( // balance: int.parse(map["balance"] as String), // contractAddress: map["contractAddress"] as String, // decimals: int.parse(map["decimals"] as String), // name: map["name"] as String, // symbol: map["symbol"] as String, // ), // ); // } else { // throw EthApiException( // "Unsupported token type found: ${map["type"]}"); // } // } // // return EthereumResponse( // tokens, // null, // ); // } else { // throw EthApiException(json["message"] as String); // } // } else { // throw EthApiException( // "getWalletTokens($address) failed with status code: " // "${response.statusCode}", // ); // } // } on EthApiException catch (e) { // return EthereumResponse( // null, // e, // ); // } catch (e, s) { // Logging.instance.log( // "getWalletTokens(): $e\n$s", // level: LogLevel.Error, // ); // return EthereumResponse( // null, // EthApiException(e.toString()), // ); // } // } static Future> getWalletTokenBalance({ required String address, required String contractAddress, }) async { try { final uri = Uri.parse( "$stackBaseServer/tokens?addrs=$contractAddress $address", ); final response = await client.get( url: uri, proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { final json = jsonDecode(response.body); if (json["data"] is List) { final map = json["data"].first as Map; final balance = BigInt.tryParse(map["units"].toString()) ?? BigInt.zero; return EthereumResponse( Amount(rawValue: balance, fractionDigits: map["decimals"] as int), null, ); } else { throw EthApiException(json["message"] as String); } } else { throw EthApiException( "getWalletTokenBalance($address) failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getWalletTokenBalance(): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } static Future> getAddressNonce({ required String address, }) async { try { final uri = Uri.parse( "$stackBaseServer/state?addrs=$address&parts=all", ); final response = await client.get( url: uri, proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { final json = jsonDecode(response.body); if (json["data"] is List) { final map = json["data"].first as Map; final nonce = map["nonce"] as int; return EthereumResponse( nonce, null, ); } else { throw EthApiException(json["message"] as String); } } else { throw EthApiException( "getAddressNonce($address) failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getAddressNonce(): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } static Future> getGasOracle() async { try { final response = await client.get( url: Uri.parse( "$stackBaseServer/gas-prices", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { final json = jsonDecode(response.body) as Map; if (json["success"] == true) { try { return EthereumResponse( GasTracker.fromJson( Map.from(json["result"]["result"] as Map), ), null, ); } catch (_) { throw EthApiException( "getGasOracle() failed with response: " "${response.body}", ); } } else { throw EthApiException( "getGasOracle() failed with response: " "${response.body}", ); } } else { throw EthApiException( "getGasOracle() failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getGasOracle(): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } static Future getFees() async { final fees = (await getGasOracle()).value!; final feesFast = fees.fast.shift(9).toBigInt(); final feesStandard = fees.average.shift(9).toBigInt(); final feesSlow = fees.slow.shift(9).toBigInt(); return FeeObject( numberOfBlocksFast: fees.numberOfBlocksFast, numberOfBlocksAverage: fees.numberOfBlocksAverage, numberOfBlocksSlow: fees.numberOfBlocksSlow, fast: feesFast.toInt(), medium: feesStandard.toInt(), slow: feesSlow.toInt()); } static Future> getTokenContractInfoByAddress( String contractAddress) async { try { final response = await client.get( url: Uri.parse( "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { final json = jsonDecode(response.body) as Map; if (json["data"] is List) { final map = Map.from(json["data"].first as Map); EthContract? token; if (map["isErc20"] == true) { token = EthContract( address: map["address"] as String, decimals: map["decimals"] as int, name: map["name"] as String, symbol: map["symbol"] as String, type: EthContractType.erc20, ); } else if (map["isErc721"] == true) { token = EthContract( address: map["address"] as String, decimals: map["decimals"] as int, name: map["name"] as String, symbol: map["symbol"] as String, type: EthContractType.erc721, ); } else { throw EthApiException( "Unsupported token type found: ${map["type"]}"); } return EthereumResponse( token, null, ); } else { throw EthApiException(response.body); } } else { throw EthApiException( "getTokenByContractAddress($contractAddress) failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getTokenByContractAddress(): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } static Future> getTokenAbi({ required String name, required String contractAddress, }) async { try { final response = await client.get( url: Uri.parse( "$stackBaseServer/abis?addrs=$contractAddress&verbose=true", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { final json = jsonDecode(response.body)["data"] as List; return EthereumResponse( jsonEncode(json), null, ); } else { throw EthApiException( "getTokenAbi($name, $contractAddress) failed with status code: " "${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getTokenAbi($name, $contractAddress): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } /// Fetch the underlying contract address that a proxy contract points to static Future> getProxyTokenImplementationAddress( String contractAddress, ) async { try { final response = await client.get( url: Uri.parse( "$stackBaseServer/state?addrs=$contractAddress&parts=proxy"), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null, ); if (response.code == 200) { final json = jsonDecode(response.body); final list = json["data"] as List; final map = Map.from(list.first as Map); return EthereumResponse( map["proxy"] as String, null, ); } else { throw EthApiException( "getProxyTokenImplementationAddress($contractAddress) failed with" " status code: ${response.code}", ); } } on EthApiException catch (e) { return EthereumResponse( null, e, ); } catch (e, s) { Logging.instance.log( "getProxyTokenImplementationAddress($contractAddress) : $e\n$s", level: LogLevel.Error, ); return EthereumResponse( null, EthApiException(e.toString()), ); } } }