diff --git a/lib/services/coins/tezos/api/tezos_api.dart b/lib/services/coins/tezos/api/tezos_api.dart new file mode 100644 index 000000000..5d37fd382 --- /dev/null +++ b/lib/services/coins/tezos/api/tezos_api.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:stackwallet/networking/http.dart'; +import 'package:stackwallet/services/coins/tezos/api/tezos_transaction.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +abstract final class TezosAPI { + static final HTTP _client = HTTP(); + static const String _baseURL = 'https://api.tzstats.com'; + + static Future?> getTransactions(String address) async { + try { + final transactionsCall = "$_baseURL/explorer/account/$address/operations"; + + final response = await _client.get( + url: Uri.parse(transactionsCall), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final result = jsonDecode(response.body) as List; + + List txs = []; + for (var tx in result) { + if (tx["type"] == "transaction") { + int? burnedAmountInMicroTez; + int? storageLimit; + if (tx["burned"] != null) { + burnedAmountInMicroTez = double.parse( + (tx["burned"] * pow(10, Coin.tezos.decimals)).toString()) + .toInt(); + } + if (tx["storage_limit"] != null) { + storageLimit = tx["storage_limit"] as int; + } + final theTx = TezosTransaction( + id: tx["id"] as int, + hash: tx["hash"] as String, + type: tx["type"] as String, + height: tx["height"] as int, + timestamp: DateTime.parse(tx["time"].toString()) + .toUtc() + .millisecondsSinceEpoch ~/ + 1000, + cycle: tx["cycle"] as int, + counter: tx["counter"] as int, + opN: tx["op_n"] as int, + opP: tx["op_p"] as int, + status: tx["status"] as String, + isSuccess: tx["is_success"] as bool, + gasLimit: tx["gas_limit"] as int, + gasUsed: tx["gas_used"] as int, + storageLimit: storageLimit, + amountInMicroTez: double.parse( + (tx["volume"] * pow(10, Coin.tezos.decimals)).toString()) + .toInt(), + feeInMicroTez: double.parse( + (tx["fee"] * pow(10, Coin.tezos.decimals)).toString()) + .toInt(), + burnedAmountInMicroTez: burnedAmountInMicroTez, + senderAddress: tx["sender"] as String, + receiverAddress: tx["receiver"] as String, + confirmations: tx["confirmations"] as int, + ); + txs.add(theTx); + } + } + return txs; + } catch (e) { + Logging.instance.log( + "Error occurred in tezos_api.dart while getting transactions for $address: $e", + level: LogLevel.Error, + ); + } + return null; + } + + static Future getFeeEstimationFromLastDays(int days) async { + try { + var api = "$_baseURL/series/op?start_date=today&collapse=$days"; + + final response = await _client.get( + url: Uri.parse(api), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final result = jsonDecode(response.body); + + double totalFees = result[0][4] as double; + int totalTxs = result[0][8] as int; + return ((totalFees / totalTxs * Coin.tezos.decimals).floor()); + } catch (e) { + Logging.instance.log( + "Error occurred in tezos_api.dart while getting fee estimation for tezos: $e", + level: LogLevel.Error, + ); + } + return null; + } +} diff --git a/lib/services/coins/tezos/api/tezos_rpc_api.dart b/lib/services/coins/tezos/api/tezos_rpc_api.dart new file mode 100644 index 000000000..d5faa060f --- /dev/null +++ b/lib/services/coins/tezos/api/tezos_rpc_api.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:stackwallet/networking/http.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +abstract final class TezosRpcAPI { + static final HTTP _client = HTTP(); + + static Future getBalance({ + required ({String host, int port}) nodeInfo, + required String address, + }) async { + try { + String balanceCall = + "${nodeInfo.host}:${nodeInfo.port}/chains/main/blocks/head/context/contracts/$address/balance"; + + final response = await _client.get( + url: Uri.parse(balanceCall), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final balance = + BigInt.parse(response.body.substring(1, response.body.length - 2)); + return balance; + } catch (e) { + Logging.instance.log( + "Error occurred in tezos_rpc_api.dart while getting balance for $address: $e", + level: LogLevel.Error, + ); + } + return null; + } + + static Future getChainHeight({ + required ({String host, int port}) nodeInfo, + }) async { + try { + final api = + "${nodeInfo.host}:${nodeInfo.port}/chains/main/blocks/head/header/shell"; + + final response = await _client.get( + url: Uri.parse(api), + headers: {'Content-Type': 'application/json'}, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + final jsonParsedResponse = jsonDecode(response.body); + return int.parse(jsonParsedResponse["level"].toString()); + } catch (e) { + Logging.instance.log( + "Error occurred in tezos_rpc_api.dart while getting chain height for tezos: $e", + level: LogLevel.Error, + ); + } + return null; + } + + static Future testNetworkConnection({ + required ({String host, int port}) nodeInfo, + }) async { + final result = await getChainHeight(nodeInfo: nodeInfo); + return result != null; + } +} diff --git a/lib/services/coins/tezos/api/tezos_transaction.dart b/lib/services/coins/tezos/api/tezos_transaction.dart new file mode 100644 index 000000000..28a21ad91 --- /dev/null +++ b/lib/services/coins/tezos/api/tezos_transaction.dart @@ -0,0 +1,45 @@ +class TezosTransaction { + final int? id; + final String hash; + final String? type; + final int height; + final int timestamp; + final int? cycle; + final int? counter; + final int? opN; + final int? opP; + final String? status; + final bool? isSuccess; + final int? gasLimit; + final int? gasUsed; + final int? storageLimit; + final int amountInMicroTez; + final int feeInMicroTez; + final int? burnedAmountInMicroTez; + final String senderAddress; + final String receiverAddress; + final int? confirmations; + + TezosTransaction({ + this.id, + required this.hash, + this.type, + required this.height, + required this.timestamp, + this.cycle, + this.counter, + this.opN, + this.opP, + this.status, + this.isSuccess, + this.gasLimit, + this.gasUsed, + this.storageLimit, + required this.amountInMicroTez, + required this.feeInMicroTez, + this.burnedAmountInMicroTez, + required this.senderAddress, + required this.receiverAddress, + this.confirmations, + }); +} diff --git a/lib/services/coins/tezos/tezos_wallet.dart b/lib/services/coins/tezos/tezos_wallet.dart index fa5abc30e..604a0800c 100644 --- a/lib/services/coins/tezos/tezos_wallet.dart +++ b/lib/services/coins/tezos/tezos_wallet.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:isar/isar.dart'; @@ -10,8 +9,10 @@ import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/networking/http.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/coins/tezos/api/tezos_api.dart'; +import 'package:stackwallet/services/coins/tezos/api/tezos_rpc_api.dart'; +import 'package:stackwallet/services/coins/tezos/api/tezos_transaction.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.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'; @@ -19,7 +20,6 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -102,8 +102,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override bool get shouldAutoSync => _shouldAutoSync; - HTTP client = HTTP(); - @override set shouldAutoSync(bool shouldAutoSync) { if (_shouldAutoSync != shouldAutoSync) { @@ -241,17 +239,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future estimateFeeFor(Amount amount, int feeRate) async { - var api = "https://api.tzstats.com/series/op?start_date=today&collapse=1d"; - var response = jsonDecode((await client.get( - url: Uri.parse(api), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - )) - .body)[0]; - double totalFees = response[4] as double; - int totalTxs = response[8] as int; - int feePerTx = (totalFees / totalTxs * 1000000).floor(); - + int? feePerTx = await TezosAPI.getFeeEstimationFromLastDays(1); + feePerTx ??= 0; return Amount( rawValue: BigInt.from(feePerTx), fractionDigits: coin.decimals, @@ -266,18 +255,9 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get fees async { - var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d"; - var response = jsonDecode((await client.get( - url: Uri.parse(api), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - )) - .body); - double totalFees = response[0][4] as double; - int totalTxs = response[0][8] as int; - int feePerTx = (totalFees / totalTxs * 1000000).floor(); + int? feePerTx = await TezosAPI.getFeeEstimationFromLastDays(1); + feePerTx ??= 0; Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info); - // TODO: fix numberOfBlocks - Since there is only one fee no need to set blocks return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, @@ -504,18 +484,18 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { Future updateBalance() async { try { - String balanceCall = "https://api.mainnet.tzkt.io/v1/accounts/" - "${await currentReceivingAddress}/balance"; - var response = jsonDecode(await client - .get( - url: Uri.parse(balanceCall), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) - .then((value) => value.body)); - Amount balanceInAmount = Amount( - rawValue: BigInt.parse(response.toString()), - fractionDigits: coin.decimals); + NodeModel currentNode = getCurrentNode(); + BigInt? balance = await TezosRpcAPI.getBalance( + nodeInfo: (host: currentNode.host, port: currentNode.port), + address: await currentReceivingAddress); + if (balance == null) { + return; + } + Logging.instance.log( + "Balance for ${await currentReceivingAddress}: $balance", + level: LogLevel.Info); + Amount balanceInAmount = + Amount(rawValue: balance, fractionDigits: coin.decimals); _balance = Balance( total: balanceInAmount, spendable: balanceInAmount, @@ -526,107 +506,95 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { ); await updateCachedBalance(_balance!); } catch (e, s) { - Logging.instance - .log("ERROR GETTING BALANCE ${e.toString()}", level: LogLevel.Error); + Logging.instance.log( + "Error getting balance in tezos_wallet.dart: ${e.toString()}", + level: LogLevel.Error); } } Future updateTransactions() async { - String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/" - "${await currentReceivingAddress}/operations"; - var response = jsonDecode(await client - .get( - url: Uri.parse(transactionsCall), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) - .then((value) => value.body)); - List> txs = []; - for (var tx in response as List) { - if (tx["type"] == "transaction") { - TransactionType txType; - final String myAddress = await currentReceivingAddress; - final String senderAddress = tx["sender"]["address"] as String; - final String targetAddress = tx["target"]["address"] as String; - if (senderAddress == myAddress && targetAddress == myAddress) { - txType = TransactionType.sentToSelf; - } else if (senderAddress == myAddress) { - txType = TransactionType.outgoing; - } else if (targetAddress == myAddress) { - txType = TransactionType.incoming; - } else { - txType = TransactionType.unknown; - } - - var theTx = Transaction( - walletId: walletId, - txid: tx["hash"].toString(), - timestamp: DateTime.parse(tx["timestamp"].toString()) - .toUtc() - .millisecondsSinceEpoch ~/ - 1000, - type: txType, - subType: TransactionSubType.none, - amount: tx["amount"] as int, - amountString: Amount( - rawValue: - BigInt.parse((tx["amount"] as int).toInt().toString()), - fractionDigits: coin.decimals) - .toJsonString(), - fee: tx["bakerFee"] as int, - height: int.parse(tx["level"].toString()), - isCancelled: false, - isLelantus: false, - slateId: "", - otherData: "", - inputs: [], - outputs: [], - nonce: 0, - numberOfMessages: null, - ); - final AddressSubType subType; - switch (txType) { - case TransactionType.incoming: - case TransactionType.sentToSelf: - subType = AddressSubType.receiving; - break; - case TransactionType.outgoing: - case TransactionType.unknown: - subType = AddressSubType.unknown; - break; - } - final theAddress = Address( - walletId: walletId, - value: targetAddress, - publicKey: [], - derivationIndex: 0, - derivationPath: null, - type: AddressType.unknown, - subType: subType, - ); - txs.add(Tuple2(theTx, theAddress)); - } - } + List? txs = + await TezosAPI.getTransactions(await currentReceivingAddress); Logging.instance.log("Transactions: $txs", level: LogLevel.Info); - await db.addNewTransactionData(txs, walletId); + if (txs == null) { + return; + } else if (txs.isEmpty) { + return; + } + List> transactions = []; + for (var theTx in txs) { + var txType = TransactionType.unknown; + var selfAddress = await currentReceivingAddress; + if (selfAddress == theTx.senderAddress) { + txType = TransactionType.outgoing; + } else if (selfAddress == theTx.receiverAddress) { + txType = TransactionType.incoming; + } else if (selfAddress == theTx.receiverAddress && + selfAddress == theTx.senderAddress) { + txType = TransactionType.sentToSelf; + } + var transaction = Transaction( + walletId: walletId, + txid: theTx.hash, + timestamp: theTx.timestamp, + type: txType, + subType: TransactionSubType.none, + amount: theTx.amountInMicroTez, + amountString: Amount( + rawValue: BigInt.parse(theTx.amountInMicroTez.toString()), + fractionDigits: coin.decimals, + ).toJsonString(), + fee: theTx.feeInMicroTez, + height: theTx.height, + isCancelled: false, + isLelantus: false, + slateId: "", + otherData: "", + inputs: [], + outputs: [], + nonce: 0, + numberOfMessages: null, + ); + final AddressSubType subType; + switch (txType) { + case TransactionType.incoming: + case TransactionType.sentToSelf: + subType = AddressSubType.receiving; + break; + case TransactionType.outgoing: + case TransactionType.unknown: + subType = AddressSubType.unknown; + break; + } + final theAddress = Address( + walletId: walletId, + value: theTx.receiverAddress, + publicKey: [], + derivationIndex: 0, + derivationPath: null, + type: AddressType.unknown, + subType: subType, + ); + transactions.add(Tuple2(transaction, theAddress)); + } + await db.addNewTransactionData(transactions, walletId); } Future updateChainHeight() async { try { - var api = "${getCurrentNode().host}/chains/main/blocks/head/header/shell"; - var jsonParsedResponse = jsonDecode(await client - .get( - url: Uri.parse(api), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) - .then((value) => value.body)); - final int intHeight = int.parse(jsonParsedResponse["level"].toString()); - Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info); + NodeModel currentNode = getCurrentNode(); + int? intHeight = await TezosRpcAPI.getChainHeight( + nodeInfo: (host: currentNode.host, port: currentNode.port)); + if (intHeight == null) { + return; + } + Logging.instance + .log("Chain height for tezos: $intHeight", level: LogLevel.Info); await updateCachedChainHeight(intHeight); } catch (e, s) { - Logging.instance - .log("GET CHAIN HEIGHT ERROR ${e.toString()}", level: LogLevel.Error); + Logging.instance.log( + "Error occured in tezos_wallet.dart while getting chain height for tezos: ${e.toString()}", + level: LogLevel.Error); } } @@ -679,7 +647,7 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { } } catch (e, s) { Logging.instance.log( - "Failed to refresh stellar wallet $walletId: '$walletName': $e\n$s", + "Failed to refresh tezos wallet $walletId: '$walletName': $e\n$s", level: LogLevel.Warning, ); GlobalEventBus.instance.fire( @@ -699,17 +667,9 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future testNetworkConnection() async { - try { - await client.get( - url: Uri.parse( - "${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"), - proxyInfo: - _prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ); - return true; - } catch (e) { - return false; - } + NodeModel currentNode = getCurrentNode(); + return await TezosRpcAPI.testNetworkConnection( + nodeInfo: (host: currentNode.host, port: currentNode.port)); } @override