Merge pull request #706 from cypherstack/wallets_refactor_tezos_api

tezos api
This commit is contained in:
julian-CStack 2023-11-20 09:56:51 -06:00 committed by GitHub
commit 7d8473d863
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 326 additions and 141 deletions

View file

@ -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<List<TezosTransaction>?> 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<TezosTransaction> 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<int?> 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;
}
}

View file

@ -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<BigInt?> 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<int?> 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<bool> testNetworkConnection({
required ({String host, int port}) nodeInfo,
}) async {
final result = await getChainHeight(nodeInfo: nodeInfo);
return result != null;
}
}

View file

@ -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,
});
}

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:isar/isar.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/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_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/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/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/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/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_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.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/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
@ -102,8 +102,6 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
@override @override
bool get shouldAutoSync => _shouldAutoSync; bool get shouldAutoSync => _shouldAutoSync;
HTTP client = HTTP();
@override @override
set shouldAutoSync(bool shouldAutoSync) { set shouldAutoSync(bool shouldAutoSync) {
if (_shouldAutoSync != shouldAutoSync) { if (_shouldAutoSync != shouldAutoSync) {
@ -241,17 +239,8 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
@override @override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async { Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
var api = "https://api.tzstats.com/series/op?start_date=today&collapse=1d"; int? feePerTx = await TezosAPI.getFeeEstimationFromLastDays(1);
var response = jsonDecode((await client.get( feePerTx ??= 0;
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();
return Amount( return Amount(
rawValue: BigInt.from(feePerTx), rawValue: BigInt.from(feePerTx),
fractionDigits: coin.decimals, fractionDigits: coin.decimals,
@ -266,18 +255,9 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
@override @override
Future<FeeObject> get fees async { Future<FeeObject> get fees async {
var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d"; int? feePerTx = await TezosAPI.getFeeEstimationFromLastDays(1);
var response = jsonDecode((await client.get( feePerTx ??= 0;
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();
Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info); Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info);
// TODO: fix numberOfBlocks - Since there is only one fee no need to set blocks
return FeeObject( return FeeObject(
numberOfBlocksFast: 10, numberOfBlocksFast: 10,
numberOfBlocksAverage: 10, numberOfBlocksAverage: 10,
@ -504,18 +484,18 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
Future<void> updateBalance() async { Future<void> updateBalance() async {
try { try {
String balanceCall = "https://api.mainnet.tzkt.io/v1/accounts/" NodeModel currentNode = getCurrentNode();
"${await currentReceivingAddress}/balance"; BigInt? balance = await TezosRpcAPI.getBalance(
var response = jsonDecode(await client nodeInfo: (host: currentNode.host, port: currentNode.port),
.get( address: await currentReceivingAddress);
url: Uri.parse(balanceCall), if (balance == null) {
proxyInfo: return;
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, }
) Logging.instance.log(
.then((value) => value.body)); "Balance for ${await currentReceivingAddress}: $balance",
Amount balanceInAmount = Amount( level: LogLevel.Info);
rawValue: BigInt.parse(response.toString()), Amount balanceInAmount =
fractionDigits: coin.decimals); Amount(rawValue: balance, fractionDigits: coin.decimals);
_balance = Balance( _balance = Balance(
total: balanceInAmount, total: balanceInAmount,
spendable: balanceInAmount, spendable: balanceInAmount,
@ -526,55 +506,46 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
); );
await updateCachedBalance(_balance!); await updateCachedBalance(_balance!);
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance.log(
.log("ERROR GETTING BALANCE ${e.toString()}", level: LogLevel.Error); "Error getting balance in tezos_wallet.dart: ${e.toString()}",
level: LogLevel.Error);
} }
} }
Future<void> updateTransactions() async { Future<void> updateTransactions() async {
String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/" List<TezosTransaction>? txs =
"${await currentReceivingAddress}/operations"; await TezosAPI.getTransactions(await currentReceivingAddress);
var response = jsonDecode(await client Logging.instance.log("Transactions: $txs", level: LogLevel.Info);
.get( if (txs == null) {
url: Uri.parse(transactionsCall), return;
proxyInfo: } else if (txs.isEmpty) {
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, return;
)
.then((value) => value.body));
List<Tuple2<Transaction, Address>> 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;
} }
List<Tuple2<Transaction, Address>> transactions = [];
var theTx = Transaction( 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, walletId: walletId,
txid: tx["hash"].toString(), txid: theTx.hash,
timestamp: DateTime.parse(tx["timestamp"].toString()) timestamp: theTx.timestamp,
.toUtc()
.millisecondsSinceEpoch ~/
1000,
type: txType, type: txType,
subType: TransactionSubType.none, subType: TransactionSubType.none,
amount: tx["amount"] as int, amount: theTx.amountInMicroTez,
amountString: Amount( amountString: Amount(
rawValue: rawValue: BigInt.parse(theTx.amountInMicroTez.toString()),
BigInt.parse((tx["amount"] as int).toInt().toString()), fractionDigits: coin.decimals,
fractionDigits: coin.decimals) ).toJsonString(),
.toJsonString(), fee: theTx.feeInMicroTez,
fee: tx["bakerFee"] as int, height: theTx.height,
height: int.parse(tx["level"].toString()),
isCancelled: false, isCancelled: false,
isLelantus: false, isLelantus: false,
slateId: "", slateId: "",
@ -597,36 +568,33 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
final theAddress = Address( final theAddress = Address(
walletId: walletId, walletId: walletId,
value: targetAddress, value: theTx.receiverAddress,
publicKey: [], publicKey: [],
derivationIndex: 0, derivationIndex: 0,
derivationPath: null, derivationPath: null,
type: AddressType.unknown, type: AddressType.unknown,
subType: subType, subType: subType,
); );
txs.add(Tuple2(theTx, theAddress)); transactions.add(Tuple2(transaction, theAddress));
} }
} await db.addNewTransactionData(transactions, walletId);
Logging.instance.log("Transactions: $txs", level: LogLevel.Info);
await db.addNewTransactionData(txs, walletId);
} }
Future<void> updateChainHeight() async { Future<void> updateChainHeight() async {
try { try {
var api = "${getCurrentNode().host}/chains/main/blocks/head/header/shell"; NodeModel currentNode = getCurrentNode();
var jsonParsedResponse = jsonDecode(await client int? intHeight = await TezosRpcAPI.getChainHeight(
.get( nodeInfo: (host: currentNode.host, port: currentNode.port));
url: Uri.parse(api), if (intHeight == null) {
proxyInfo: return;
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, }
) Logging.instance
.then((value) => value.body)); .log("Chain height for tezos: $intHeight", level: LogLevel.Info);
final int intHeight = int.parse(jsonParsedResponse["level"].toString());
Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info);
await updateCachedChainHeight(intHeight); await updateCachedChainHeight(intHeight);
} catch (e, s) { } catch (e, s) {
Logging.instance Logging.instance.log(
.log("GET CHAIN HEIGHT ERROR ${e.toString()}", level: LogLevel.Error); "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) { } catch (e, s) {
Logging.instance.log( 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, level: LogLevel.Warning,
); );
GlobalEventBus.instance.fire( GlobalEventBus.instance.fire(
@ -699,17 +667,9 @@ class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
@override @override
Future<bool> testNetworkConnection() async { Future<bool> testNetworkConnection() async {
try { NodeModel currentNode = getCurrentNode();
await client.get( return await TezosRpcAPI.testNetworkConnection(
url: Uri.parse( nodeInfo: (host: currentNode.host, port: currentNode.port));
"${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"),
proxyInfo:
_prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
return true;
} catch (e) {
return false;
}
} }
@override @override