mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-26 21:39:21 +00:00
281 lines
8.5 KiB
Dart
281 lines
8.5 KiB
Dart
/*
|
|
* 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:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:decimal/decimal.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
import '../app_config.dart';
|
|
import '../db/hive/db.dart';
|
|
import '../networking/http.dart';
|
|
import '../utilities/logger.dart';
|
|
import '../utilities/prefs.dart';
|
|
import '../wallets/crypto_currency/crypto_currency.dart';
|
|
import 'tor_service.dart';
|
|
|
|
class PriceAPI {
|
|
// coingecko coin ids
|
|
static const Map<Type, String> _coinToIdMap = {
|
|
Bitcoin: "bitcoin",
|
|
BitcoinFrost: "bitcoin",
|
|
Litecoin: "litecoin",
|
|
Bitcoincash: "bitcoin-cash",
|
|
Dogecoin: "dogecoin",
|
|
Epiccash: "epic-cash",
|
|
Ecash: "ecash",
|
|
Ethereum: "ethereum",
|
|
Firo: "zcoin",
|
|
Monero: "monero",
|
|
Particl: "particl",
|
|
Peercoin: "peercoin",
|
|
Solana: "solana",
|
|
Stellar: "stellar",
|
|
Tezos: "tezos",
|
|
Wownero: "wownero",
|
|
Namecoin: "namecoin",
|
|
Nano: "nano",
|
|
Banano: "banano",
|
|
};
|
|
|
|
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);
|
|
|
|
final HTTP client;
|
|
|
|
PriceAPI(this.client);
|
|
|
|
@visibleForTesting
|
|
void resetLastCalledToForceNextCallToUpdateCache() {
|
|
_lastCalled = DateTime(1970);
|
|
}
|
|
|
|
Future<void> _updateCachedPrices(
|
|
Map<CryptoCurrency, Tuple2<Decimal, double>> data,
|
|
) async {
|
|
final Map<String, dynamic> map = {};
|
|
|
|
for (final coin in AppConfig.coins) {
|
|
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<CryptoCurrency, 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 AppConfig.coins) coin: Tuple2(Decimal.zero, 0.0),
|
|
};
|
|
|
|
for (final entry in map.entries) {
|
|
result[AppConfig.getCryptoCurrencyByPrettyName(
|
|
entry.key as String,
|
|
)] = Tuple2(
|
|
Decimal.parse(entry.value[0] as String),
|
|
entry.value[1] as double,
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
String get _coinIds => AppConfig.coins
|
|
.where((e) => e.network == CryptoCurrencyNetwork.main)
|
|
.map((e) => _coinToIdMap[e.runtimeType])
|
|
.where((e) => e != null)
|
|
.join(",");
|
|
|
|
Future<Map<CryptoCurrency, Tuple2<Decimal, double>>> getPricesAnd24hChange({
|
|
required String baseCurrency,
|
|
}) async {
|
|
final now = DateTime.now();
|
|
if (_lastUsedBaseCurrency != baseCurrency ||
|
|
now.difference(_lastCalled) > refreshIntervalDuration) {
|
|
_lastCalled = now;
|
|
_lastUsedBaseCurrency = baseCurrency;
|
|
} else {
|
|
return _cachedPrices;
|
|
}
|
|
|
|
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 _cachedPrices;
|
|
}
|
|
final Map<CryptoCurrency, Tuple2<Decimal, double>> result = {};
|
|
try {
|
|
final uri = Uri.parse(
|
|
"https://api.coingecko.com/api/v3/coins/markets?vs_currency"
|
|
"=${baseCurrency.toLowerCase()}&ids=$_coinIds&order=market_cap_desc"
|
|
"&per_page=50&page=1&sparkline=false",
|
|
);
|
|
|
|
final coinGeckoResponse = await client.get(
|
|
url: uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
proxyInfo: Prefs.instance.useTor
|
|
? TorService.sharedInstance.getProxyInfo()
|
|
: null,
|
|
);
|
|
|
|
final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List<dynamic>;
|
|
|
|
for (final map in coinGeckoData) {
|
|
final String coinName = map["name"] as String;
|
|
final coin = AppConfig.getCryptoCurrencyByPrettyName(coinName);
|
|
|
|
final price = Decimal.parse(map["current_price"].toString());
|
|
final change24h = map["price_change_percentage_24h"] != null
|
|
? double.parse(map["price_change_percentage_24h"].toString())
|
|
: 0.0;
|
|
|
|
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;
|
|
final HTTP client = HTTP();
|
|
|
|
if ((!Logger.isTestEnv && !externalCalls) ||
|
|
!(await Prefs.instance.isExternalCallsSet())) {
|
|
Logging.instance.log(
|
|
"User does not want to use external calls",
|
|
level: LogLevel.Info,
|
|
);
|
|
return null;
|
|
}
|
|
const uriString =
|
|
"https://api.coingecko.com/api/v3/simple/supported_vs_currencies";
|
|
try {
|
|
final uri = Uri.parse(uriString);
|
|
final response = await client.get(
|
|
url: uri,
|
|
headers: {'Content-Type': 'application/json'},
|
|
proxyInfo: Prefs.instance.useTor
|
|
? TorService.sharedInstance.getProxyInfo()
|
|
: null,
|
|
);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
Future<Map<String, Tuple2<Decimal, double>>>
|
|
getPricesAnd24hChangeForEthTokens({
|
|
required Set<String> contractAddresses,
|
|
required String baseCurrency,
|
|
}) async {
|
|
final Map<String, Tuple2<Decimal, double>> tokenPrices = {};
|
|
|
|
if (AppConfig.coins.whereType<Ethereum>().isEmpty ||
|
|
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,
|
|
// );
|
|
// }
|
|
// }
|
|
|
|
return tokenPrices;
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddresses): $e\n$s",
|
|
level: LogLevel.Error,
|
|
);
|
|
// return previous cached values
|
|
return tokenPrices;
|
|
}
|
|
}
|
|
}
|