diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 11aef1374..69d8ec93e 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; - const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com'; const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion'; const _fiatApiPath = '/v2/rates'; @@ -19,7 +18,7 @@ Future _fetchPrice(Map args) async { 'interval_count': '1', 'base': crypto.toString(), 'quote': fiat.toString(), - 'key' : secrets.fiatApiKey, + 'key': secrets.fiatApiKey, }; double price = 0.0; @@ -51,9 +50,65 @@ Future _fetchPrice(Map args) async { } } +Future _fetchHistoricalPrice(Map args) async { + final crypto = args['crypto'] as CryptoCurrency; + final fiat = args['fiat'] as FiatCurrency; + final torOnly = args['torOnly'] as bool; + final date = args['date'] as DateTime; + final intervalFromNow = DateTime.now().difference(date).inMinutes; + + final Map queryParams = { + 'interval_count': '5', + 'base': crypto.toString(), + 'quote': fiat.toString(), + 'key': secrets.fiatApiKey, + 'interval_minutes': intervalFromNow.toString() + }; + + double price = 0.0; + + try { + late final Uri uri; + if (torOnly) { + uri = Uri.http(_fiatApiOnionAuthority, _fiatApiPath, queryParams); + } else { + uri = Uri.https(_fiatApiClearNetAuthority, _fiatApiPath, queryParams); + } + + final response = await get(uri); + + if (response.statusCode != 200) { + return 0.0; + } + + final data = json.decode(response.body) as Map; + final errors = data['errors'] as Map; + + if (errors.isNotEmpty) { + return 0.0; + } + + final results = data['results'] as Map; + + if (results.isNotEmpty) { + price = results.values.first as double; + } + + return price; + } catch (e) { + print(e.toString()); + return 0.0; + } +} + Future _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async => compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly}); +Future _fetchHistoricalAsync( + CryptoCurrency crypto, FiatCurrency fiat, bool torOnly, DateTime date) async => + compute( + _fetchHistoricalPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly, 'date': date}); + class FiatConversionService { static Future fetchPrice({ required CryptoCurrency crypto, @@ -61,4 +116,12 @@ class FiatConversionService { required bool torOnly, }) async => await _fetchPriceAsync(crypto, fiat, torOnly); + + static Future fetchHistoricalPrice({ + required CryptoCurrency crypto, + required FiatCurrency fiat, + required bool torOnly, + required DateTime date, + }) async => + await _fetchHistoricalAsync(crypto, fiat, torOnly, date); } diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 90b57668d..a324ea524 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -45,4 +45,5 @@ class PreferencesKey { static const lastSeenAppVersion = 'last_seen_app_version'; static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; + static const showHistoricalFiatRateKey = 'show_historical_fiat_rate'; } diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart index c7baa9b6a..a7fd21f29 100644 --- a/lib/src/screens/settings/display_settings_page.dart +++ b/lib/src/screens/settings/display_settings_page.dart @@ -56,6 +56,13 @@ class DisplaySettingsPage extends BasePage { currency.fullName.toLowerCase().contains(searchText); }, ), + SettingsSwitcherCell( + title: S.current.historical_fiat_rate, + value: _displaySettingsViewModel.showHistoricalFiatRate, + onValueChange: (_, bool value) { + _displaySettingsViewModel.setShowHistoricalFiatRate(value); + }, + ), SettingsPickerCell( title: S.current.settings_change_language, searchHintText: S.current.search_language, diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index 1b79ceeb0..4ef68ecdd 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_i import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -27,7 +28,9 @@ class TransactionDetailsPage extends BasePage { @override Widget body(BuildContext context) { // FIX-ME: Added `context` it was not used here before, maby bug ? - return SectionStandardList( + return Observer( + builder: (_) { + return SectionStandardList( context: context, sectionCount: 1, itemCounter: (int _) => transactionDetailsViewModel.items.length, @@ -63,6 +66,6 @@ class TransactionDetailsPage extends BasePage { } return Container(); - }); + });}); } } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 400f7ac88..9afdb5236 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -36,6 +36,7 @@ abstract class SettingsStoreBase with Store { required bool initialAppSecure, required FiatApiMode initialFiatMode, required bool initialAllowBiometricalAuthentication, + required bool initialShowHistoricalFiatRate, required ExchangeApiMode initialExchangeStatus, required ThemeBase initialTheme, required int initialPinLength, @@ -59,6 +60,7 @@ abstract class SettingsStoreBase with Store { isAppSecure = initialAppSecure, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, + showHistoricalFiatRate = initialShowHistoricalFiatRate, shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard, exchangeStatus = initialExchangeStatus, currentTheme = initialTheme, @@ -150,6 +152,12 @@ abstract class SettingsStoreBase with Store { PreferencesKey.allowBiometricalAuthenticationKey, biometricalAuthentication)); + reaction( + (_) => showHistoricalFiatRate, + (bool historicalFiatRate) => sharedPreferences.setBool( + PreferencesKey.showHistoricalFiatRateKey, + historicalFiatRate)); + reaction( (_) => shouldShowMarketPlaceInDashboard, (bool value) => @@ -220,6 +228,9 @@ abstract class SettingsStoreBase with Store { @observable bool allowBiometricalAuthentication; + @observable + bool showHistoricalFiatRate; + @observable ExchangeApiMode exchangeStatus; @@ -315,6 +326,9 @@ abstract class SettingsStoreBase with Store { final allowBiometricalAuthentication = sharedPreferences .getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false; + final showHistoricalFiatRate = sharedPreferences + .getBool(PreferencesKey.showHistoricalFiatRateKey) ?? + false; final shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? true; final exchangeStatus = ExchangeApiMode.deserialize( @@ -390,6 +404,7 @@ abstract class SettingsStoreBase with Store { initialAppSecure: isAppSecure, initialFiatMode: currentFiatApiMode, initialAllowBiometricalAuthentication: allowBiometricalAuthentication, + initialShowHistoricalFiatRate: showHistoricalFiatRate, initialExchangeStatus: exchangeStatus, initialTheme: savedTheme, actionlistDisplayMode: actionListDisplayMode, @@ -438,6 +453,9 @@ abstract class SettingsStoreBase with Store { allowBiometricalAuthentication = sharedPreferences .getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? allowBiometricalAuthentication; + showHistoricalFiatRate = sharedPreferences + .getBool(PreferencesKey.showHistoricalFiatRateKey) ?? + showHistoricalFiatRate; shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? shouldShowMarketPlaceInDashboard; diff --git a/lib/view_model/settings/display_settings_view_model.dart b/lib/view_model/settings/display_settings_view_model.dart index 69d82eff4..3711dff6b 100644 --- a/lib/view_model/settings/display_settings_view_model.dart +++ b/lib/view_model/settings/display_settings_view_model.dart @@ -37,6 +37,9 @@ abstract class DisplaySettingsViewModelBase with Store { @computed bool get disabledFiatApiMode => _settingsStore.fiatApiMode == FiatApiMode.disabled; + @computed + bool get showHistoricalFiatRate => _settingsStore.showHistoricalFiatRate; + @action void setBalanceDisplayMode(BalanceDisplayMode value) => _settingsStore.balanceDisplayMode = value; @@ -66,4 +69,7 @@ abstract class DisplaySettingsViewModelBase with Store { void setShouldShowMarketPlaceInDashbaord(bool value) { _settingsStore.shouldShowMarketPlaceInDashboard = value; } + + @action + void setShowHistoricalFiatRate(bool value) => _settingsStore.showHistoricalFiatRate = value; } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index a37a6fa2f..cb0828a33 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,3 +1,6 @@ +import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -14,6 +17,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/core/fiat_conversion_service.dart'; part 'transaction_details_view_model.g.dart'; @@ -26,9 +30,16 @@ abstract class TransactionDetailsViewModelBase with Store { required this.transactionDescriptionBox, required this.wallet, required this.settingsStore}) - : items = [], + : items = ObservableList(), isRecipientAddressShown = false, - showRecipientAddress = settingsStore.shouldSaveRecipientAddress { + showRecipientAddress = settingsStore.shouldSaveRecipientAddress, + fiatRateListItem = StandartListItem( + title: settingsStore.showHistoricalFiatRate + ? S.current.historical_fiat_rate + : S.current.fiat_rate, + value: settingsStore.showHistoricalFiatRate + ? '${S.current.fetching.toLowerCase()} ...' + : transactionInfo.fiatAmount() + ' ${settingsStore.fiatCurrency}') { final dateFormat = DateFormatter.withCurrentLocal(); final tx = transactionInfo; @@ -51,6 +62,8 @@ abstract class TransactionDetailsViewModelBase with Store { if (feeFormatted != null) StandartListItem( title: S.current.transaction_details_fee, value: feeFormatted), + if (settingsStore.fiatApiMode != FiatApiMode.disabled) + fiatRateListItem, if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!) ]; @@ -105,6 +118,8 @@ abstract class TransactionDetailsViewModelBase with Store { StandartListItem( title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + if (settingsStore.fiatApiMode != FiatApiMode.disabled) + fiatRateListItem, ]; items.addAll(_items); @@ -125,6 +140,8 @@ abstract class TransactionDetailsViewModelBase with Store { if (tx.feeFormatted()?.isNotEmpty ?? false) StandartListItem( title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + if (settingsStore.fiatApiMode != FiatApiMode.disabled) + fiatRateListItem, ]); } @@ -171,17 +188,54 @@ abstract class TransactionDetailsViewModelBase with Store { transactionDescriptionBox.add(description); } })); + if (settingsStore.showHistoricalFiatRate) { + getHistoricalFiatRate(); + } } final TransactionInfo transactionInfo; final Box transactionDescriptionBox; final SettingsStore settingsStore; final WalletBase wallet; + final StandartListItem fiatRateListItem; - final List items; + final ObservableList items; bool showRecipientAddress; bool isRecipientAddressShown; + @action + Future getHistoricalFiatRate() async { + final fiatRateItemIndex = items.indexWhere((element) => element == fiatRateListItem); + if (fiatRateItemIndex == -1) return; + + final fiat = settingsStore.fiatCurrency; + + final historicalFiatRate = await FiatConversionService.fetchHistoricalPrice( + crypto: wallet.currency, + fiat: fiat, + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly, + date: transactionInfo.date); + var formattedFiatAmount = 0.0; + switch (wallet.type) { + case WalletType.bitcoin: + case WalletType.litecoin: + formattedFiatAmount = bitcoinAmountToDouble(amount: transactionInfo.amount); + break; + case WalletType.monero: + case WalletType.haven: + formattedFiatAmount = moneroAmountToDouble(amount: transactionInfo.amount); + break; + default: + formattedFiatAmount; + } + final historicalFiatAmountFormatted = formattedFiatAmount * historicalFiatRate; + items[fiatRateItemIndex] = StandartListItem( + title: S.current.historical_fiat_rate, + value: historicalFiatAmountFormatted > 0.0 + ? historicalFiatAmountFormatted.toStringAsFixed(2) + ' ${fiat}' + : 'no historical data'); + } + String _explorerUrl(WalletType type, String txId) { switch (type) { case WalletType.monero: diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 22cc9b950..8979ca5e8 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -708,5 +708,7 @@ "error_text_input_below_minimum_limit" : "Amount is less than the minimum", "error_text_input_above_maximum_limit" : "Amount is more than the maximum", "show_market_place" :"Show Marketplace", - "prevent_screenshots": "Prevent screenshots and screen recording" + "prevent_screenshots": "Prevent screenshots and screen recording", + "fiat_rate": "Fiat rate", + "historical_fiat_rate": "Historical fiat rate" }