/* * 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 _coinToIdMap = { Bitcoin: "bitcoin", BitcoinFrost: "bitcoin", Litecoin: "litecoin", Bitcoincash: "bitcoin-cash", Dash: "dash", 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 _updateCachedPrices( Map> data, ) async { final Map 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(boxName: DB.boxNamePriceCache, key: 'cache', value: map); } Map> get _cachedPrices { final map = DB.instance.get(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>> 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> 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; 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?> 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; return List.from(json); } catch (e, s) { Logging.instance.log( "availableBaseCurrencies() using $uriString: $e\n$s", level: LogLevel.Error, ); return null; } } Future>> getPricesAnd24hChangeForEthTokens({ required Set contractAddresses, required String baseCurrency, }) async { final Map> tokenPrices = {}; if (AppConfig.coins.whereType().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; } } }