stack_wallet/lib/services/price.dart

235 lines
7.5 KiB
Dart
Raw Normal View History

2023-05-26 21:21:16 +00:00
/*
* 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
*
*/
2022-08-26 08:11:35 +00:00
import 'dart:async';
import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart';
2023-03-01 21:52:13 +00:00
import 'package:stackwallet/db/hive/db.dart';
2023-09-11 20:20:40 +00:00
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/tor_service.dart';
2022-08-26 08:11:35 +00:00
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart';
2022-08-26 08:11:35 +00:00
class PriceAPI {
static const refreshInterval = 60;
// initialize to older than current time minus at least refreshInterval
static DateTime _lastCalled =
DateTime.now().subtract(const Duration(seconds: refreshInterval + 10));
static String _lastUsedBaseCurrency = "";
static const Duration refreshIntervalDuration =
Duration(seconds: refreshInterval);
2023-09-13 16:58:02 +00:00
final HTTP client;
2022-08-26 08:11:35 +00:00
PriceAPI(this.client);
@visibleForTesting
void resetLastCalledToForceNextCallToUpdateCache() {
_lastCalled = DateTime(1970);
}
Future<void> _updateCachedPrices(
Map<Coin, Tuple2<Decimal, double>> data) async {
final Map<String, dynamic> map = {};
for (final coin in Coin.values) {
final entry = data[coin];
if (entry == null) {
map[coin.prettyName] = ["0", 0.0];
} else {
map[coin.prettyName] = [entry.item1.toString(), entry.item2];
}
}
await DB.instance
.put<dynamic>(boxName: DB.boxNamePriceCache, key: 'cache', value: map);
}
Map<Coin, Tuple2<Decimal, double>> get _cachedPrices {
final map =
DB.instance.get<dynamic>(boxName: DB.boxNamePriceCache, key: 'cache')
as Map? ??
{};
// init with 0
final result = {
for (final coin in Coin.values) coin: Tuple2(Decimal.zero, 0.0)
};
for (final entry in map.entries) {
result[coinFromPrettyName(entry.key as String)] = Tuple2(
Decimal.parse(entry.value[0] as String), entry.value[1] as double);
}
return result;
}
Future<Map<Coin, Tuple2<Decimal, double>>> getPricesAnd24hChange(
{required String baseCurrency}) async {
final now = DateTime.now();
if (_lastUsedBaseCurrency != baseCurrency ||
2023-06-30 21:30:33 +00:00
now.difference(_lastCalled) > refreshIntervalDuration) {
2022-08-26 08:11:35 +00:00
_lastCalled = now;
_lastUsedBaseCurrency = baseCurrency;
} else {
return _cachedPrices;
}
final externalCalls = Prefs.instance.externalCalls;
2022-10-19 22:51:50 +00:00
if ((!Logger.isTestEnv && !externalCalls) ||
!(await Prefs.instance.isExternalCallsSet())) {
Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info);
return _cachedPrices;
}
2022-08-26 08:11:35 +00:00
Map<Coin, Tuple2<Decimal, double>> result = {};
try {
2023-07-28 16:50:05 +00:00
final uri = Uri.parse(
"https://api.coingecko.com/api/v3/coins/markets?vs_currency"
"=${baseCurrency.toLowerCase()}"
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
2024-03-20 00:50:42 +00:00
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos,solana"
2023-07-28 16:50:05 +00:00
"&order=market_cap_desc&per_page=50&page=1&sparkline=false");
2022-08-26 08:11:35 +00:00
final coinGeckoResponse = await client.get(
2023-09-11 20:20:40 +00:00
url: uri,
2022-08-26 08:11:35 +00:00
headers: {'Content-Type': 'application/json'},
2023-09-15 19:51:20 +00:00
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
2022-08-26 08:11:35 +00:00
);
final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List<dynamic>;
for (final map in coinGeckoData) {
final String coinName = map["name"] as String;
final coin = coinFromPrettyName(coinName);
final price = Decimal.parse(map["current_price"].toString());
2023-06-08 18:21:28 +00:00
final change24h = map["price_change_percentage_24h"] != null
? double.parse(map["price_change_percentage_24h"].toString())
: 0.0;
2022-08-26 08:11:35 +00:00
result[coin] = Tuple2(price, change24h);
}
// update cache
await _updateCachedPrices(result);
return _cachedPrices;
} catch (e, s) {
Logging.instance.log("getPricesAnd24hChange($baseCurrency): $e\n$s",
level: LogLevel.Error);
// return previous cached values
return _cachedPrices;
}
}
static Future<List<String>?> availableBaseCurrencies() async {
final externalCalls = Prefs.instance.externalCalls;
2023-09-11 20:20:40 +00:00
HTTP client = HTTP();
2022-10-19 22:51:50 +00:00
if ((!Logger.isTestEnv && !externalCalls) ||
!(await Prefs.instance.isExternalCallsSet())) {
Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info);
return null;
}
2022-08-26 08:11:35 +00:00
const uriString =
"https://api.coingecko.com/api/v3/simple/supported_vs_currencies";
try {
final uri = Uri.parse(uriString);
2023-09-11 20:20:40 +00:00
final response = await client.get(
url: uri,
2022-08-26 08:11:35 +00:00
headers: {'Content-Type': 'application/json'},
2023-09-15 19:51:20 +00:00
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
2022-08-26 08:11:35 +00:00
);
final json = jsonDecode(response.body) as List<dynamic>;
return List<String>.from(json);
} catch (e, s) {
Logging.instance.log("availableBaseCurrencies() using $uriString: $e\n$s",
level: LogLevel.Error);
return null;
}
}
2023-02-28 16:36:24 +00:00
Future<Map<String, Tuple2<Decimal, double>>>
getPricesAnd24hChangeForEthTokens({
required Set<String> contractAddresses,
required String baseCurrency,
}) async {
final Map<String, Tuple2<Decimal, double>> tokenPrices = {};
if (contractAddresses.isEmpty) return tokenPrices;
final externalCalls = Prefs.instance.externalCalls;
if ((!Logger.isTestEnv && !externalCalls) ||
!(await Prefs.instance.isExternalCallsSet())) {
Logging.instance.log("User does not want to use external calls",
level: LogLevel.Info);
return tokenPrices;
}
try {
for (final contractAddress in contractAddresses) {
final uri = Uri.parse(
"https://api.coingecko.com/api/v3/simple/token_price/ethereum"
"?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses"
"=$contractAddress&include_24hr_change=true");
final coinGeckoResponse = await client.get(
url: uri,
headers: {'Content-Type': 'application/json'},
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
try {
final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map;
final map = coinGeckoData[contractAddress] as Map;
final price =
Decimal.parse(map[baseCurrency.toLowerCase()].toString());
final change24h = double.parse(
map["${baseCurrency.toLowerCase()}_24h_change"].toString());
tokenPrices[contractAddress] = Tuple2(price, change24h);
} catch (e, s) {
// only log the error as we don't want to interrupt the rest of the loop
Logging.instance.log(
"getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddress): $e\n$s\nRESPONSE: $coinGeckoResponse.body",
level: LogLevel.Warning,
);
}
2023-02-28 16:36:24 +00:00
}
return tokenPrices;
} catch (e, s) {
Logging.instance.log(
"getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddresses): $e\n$s",
level: LogLevel.Error,
);
// return previous cached values
return tokenPrices;
}
}
2022-08-26 08:11:35 +00:00
}