From 11349159206f3895b4e9beab4f6963418b7a7294 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Tue, 3 Jun 2025 03:22:04 +0300 Subject: [PATCH 01/29] fix cakepay terms and conditions link [skip ci] --- .../cake_pay/cards/cake_pay_confirm_purchase_card_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index f3c679468..bf37dfa18 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -668,7 +668,7 @@ class _ThreeCheckboxAlertContentState extends State { GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => launchUrl( - Uri.parse("https://cakepay.com/cakepay-web-terms.txt"), + Uri.parse("https://www.cakepay.com/terms/"), mode: LaunchMode.externalApplication, ), child: Padding( From 1d6e594e045c4871cfcddd0e494ce3db10c90e80 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:24:56 +0100 Subject: [PATCH 02/29] CW-959: Swap Status on Transaction Screen (#2299) * feat(swap-status-monitor): add real-time swap status monitoring and UI updates - Introduce SwapManager for automatic tracking of active-wallet swaps. - Automatically queues new or updated trades from the Hive box. - Periodically fetch and persist swap statuses via the corresponding trade provider. - Implement start(wallet, providers), stop(), and dispose() for lifecycle control. - Apply user's ExchangeApiMode(disabled, tor-only, enabled) when fetching updates. - Remove swaps from the watchlist on any final state (completed, expired, failed). - Dispose SwapManager in AppState.dispose() to cancel polling and the Hive subscription. * refactor(swap-status): replace SwapManager with TradeMonitor for improved trade monitoring. This change improves the flow by simplifying the trade monitoring logic. - Removes SwapManager class and replace with TradeMonitor implementation - Update di and Appstate to register and dispose TradeMonitor - Modify DashboardViewModel to use TradeMonitor instead of SwapManager * fix: Modify trade monitoring logic to ensure trade timers are properly disposed when wallet switching occurs * fix(swap-status): Fix receive amount for exchanges showing as .00 because of null values * feat(swap-status): Enhance Trade Monitoring This change: - Adds a privacy settings option to disable automatic exchange status updates. - Prevents trade monitoring when privacy settings option is enabled. - Disables trade monitoring when the app is in background, we only want to run these checks in foreground. - Refactors the trade monitoring logic to remove unneccessary checks and use of resources. * feat(swap-status): Enhance Trade Monitoring This change: - Adds a privacy settings option to disable automatic exchange status updates. - Prevents trade monitoring when privacy settings option is enabled. - Disables trade monitoring when the app is in background, we only want to run these checks in foreground. - Refactors the trade monitoring logic to remove unneccessary checks and use of resources. * fix(swap-staus): Prevent unneccessary calls * feat(swap-status): Prevent api request calls as long as last update time is less than specified interval --- lib/core/trade_monitor.dart | 247 ++++++++++++++++++ lib/di.dart | 61 +++-- lib/entities/preferences_key.dart | 1 + lib/main.dart | 3 + .../dashboard/pages/transactions_page.dart | 22 +- .../screens/dashboard/widgets/trade_row.dart | 73 ++++-- lib/src/screens/root/root.dart | 7 + lib/src/screens/settings/privacy_page.dart | 51 ++-- lib/store/settings_store.dart | 11 + .../dashboard/dashboard_view_model.dart | 13 +- .../settings/privacy_settings_view_model.dart | 18 +- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 3 +- res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + 39 files changed, 455 insertions(+), 82 deletions(-) create mode 100644 lib/core/trade_monitor.dart diff --git a/lib/core/trade_monitor.dart b/lib/core/trade_monitor.dart new file mode 100644 index 000000000..3b696b88a --- /dev/null +++ b/lib/core/trade_monitor.dart @@ -0,0 +1,247 @@ +import 'dart:async'; +import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/store/dashboard/trades_store.dart'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; +import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:hive/hive.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class TradeMonitor { + static const int _tradeCheckIntervalMinutes = 5; + static const int _maxTradeAgeHours = 24; + + TradeMonitor({ + required this.tradesStore, + required this.trades, + required this.appStore, + required this.preferences, + }); + + final TradesStore tradesStore; + final Box trades; + final AppStore appStore; + final Map _tradeTimers = {}; + final SharedPreferences preferences; + + ExchangeProvider? _getProviderByDescription(ExchangeProviderDescription description) { + switch (description) { + case ExchangeProviderDescription.changeNow: + return ChangeNowExchangeProvider(settingsStore: appStore.settingsStore); + case ExchangeProviderDescription.sideShift: + return SideShiftExchangeProvider(); + case ExchangeProviderDescription.simpleSwap: + return SimpleSwapExchangeProvider(); + case ExchangeProviderDescription.trocador: + return TrocadorExchangeProvider(); + case ExchangeProviderDescription.exolix: + return ExolixExchangeProvider(); + case ExchangeProviderDescription.thorChain: + return ThorChainExchangeProvider(tradesStore: trades); + case ExchangeProviderDescription.swapTrade: + return SwapTradeExchangeProvider(); + case ExchangeProviderDescription.letsExchange: + return LetsExchangeExchangeProvider(); + case ExchangeProviderDescription.stealthEx: + return StealthExExchangeProvider(); + case ExchangeProviderDescription.chainflip: + return ChainflipExchangeProvider(tradesStore: trades); + case ExchangeProviderDescription.xoSwap: + return XOSwapExchangeProvider(); + } + return null; + } + + void monitorActiveTrades(String walletId) { + // Checks if the trade monitoring is permitted + // i.e the user has not disabled the exchange api mode or the status updates + final isTradeMonitoringPermitted = _isTradeMonitoringPermitted(); + if (!isTradeMonitoringPermitted) { + return; + } + + final trades = tradesStore.trades; + final tradesToCancel = []; + + for (final item in trades) { + final trade = item.trade; + + final provider = _getProviderByDescription(trade.provider); + + // Multiple checks to see if to skip the trade, if yes, we cancel the timer if it exists + if (_shouldSkipTrade(trade, walletId, provider)) { + tradesToCancel.add(trade.id); + continue; + } + + if (_tradeTimers.containsKey(trade.id)) { + printV('Trade ${trade.id} is already being monitored'); + continue; + } else { + _startTradeMonitoring(trade, provider!); + } + } + + // After going through the list of available trades, we cancel the timers in the tradesToCancel list + _cancelMultipleTradeTimers(tradesToCancel); + } + + bool _isTradeMonitoringPermitted() { + final disableAutomaticExchangeStatusUpdates = + appStore.settingsStore.disableAutomaticExchangeStatusUpdates; + if (disableAutomaticExchangeStatusUpdates) { + printV('Automatic exchange status updates are disabled'); + return false; + } + + final exchangeApiMode = appStore.settingsStore.exchangeStatus; + if (exchangeApiMode == ExchangeApiMode.disabled) { + printV('Exchange API mode is disabled'); + return false; + } + + return true; + } + + bool _shouldSkipTrade(Trade trade, String walletId, ExchangeProvider? provider) { + if (trade.walletId != walletId) { + printV('Skipping trade ${trade.id} because it\'s not for this wallet'); + return true; + } + + final createdAt = trade.createdAt; + if (createdAt == null) { + printV('Skipping trade ${trade.id} because it has no createdAt'); + return true; + } + + if (DateTime.now().difference(createdAt).inHours > _maxTradeAgeHours) { + printV('Skipping trade ${trade.id} because it\'s older than ${_maxTradeAgeHours} hours'); + return true; + } + + if (_isFinalState(trade.state)) { + printV('Skipping trade ${trade.id} because it\'s in a final state'); + return true; + } + + if (provider == null) { + printV('Skipping trade ${trade.id} because the provider is not supported'); + return true; + } + + if (appStore.settingsStore.exchangeStatus == ExchangeApiMode.torOnly && + !provider.supportsOnionAddress) { + printV('Skipping ${provider.description}, no TOR support'); + return true; + } + + return false; + } + + void _startTradeMonitoring(Trade trade, ExchangeProvider provider) { + final timer = Timer.periodic( + Duration(minutes: _tradeCheckIntervalMinutes), + (_) => _checkTradeStatus(trade, provider), + ); + + _checkTradeStatus(trade, provider); + + _tradeTimers[trade.id] = timer; + } + + Future _checkTradeStatus(Trade trade, ExchangeProvider provider) async { + final lastUpdatedAtFromPrefs = preferences.getString('trade_${trade.id}_updated_at'); + + if (lastUpdatedAtFromPrefs != null) { + final lastUpdatedAtDateTime = DateTime.parse(lastUpdatedAtFromPrefs); + final timeSinceLastUpdate = DateTime.now().difference(lastUpdatedAtDateTime).inMinutes; + + if (timeSinceLastUpdate < _tradeCheckIntervalMinutes) { + printV( + 'Skipping trade ${trade.id} status update check because it was updated less than ${_tradeCheckIntervalMinutes} minutes ago ($timeSinceLastUpdate minutes ago)', + ); + return; + } + } + + try { + final updated = await provider.findTradeById(id: trade.id); + trade + ..stateRaw = updated.state.raw + ..receiveAmount = updated.receiveAmount ?? trade.receiveAmount + ..outputTransaction = updated.outputTransaction ?? trade.outputTransaction; + printV('Trade ${trade.id} updated: ${trade.state}'); + await trade.save(); + + await preferences.setString('trade_${trade.id}_updated_at', DateTime.now().toIso8601String()); + printV('Trade ${trade.id} updated at: ${DateTime.now().toIso8601String()}'); + + // If the updated trade is in a final state, we cancel the timer + if (_isFinalState(updated.state)) { + printV('Trade ${trade.id} is in final state'); + _cancelSingleTradeTimer(trade.id); + } + } catch (e) { + printV('Error fetching status for ${trade.id}: $e'); + } + } + + bool _isFinalState(TradeState state) { + return { + TradeState.completed.raw, + TradeState.success.raw, + TradeState.confirmed.raw, + TradeState.settled.raw, + TradeState.finished.raw, + TradeState.expired.raw, + TradeState.failed.raw, + TradeState.notFound.raw, + }.contains(state.raw); + } + + void _cancelSingleTradeTimer(String tradeId) { + if (_tradeTimers.containsKey(tradeId)) { + _tradeTimers[tradeId]?.cancel(); + _tradeTimers.remove(tradeId); + printV('Trade timer for ${tradeId} cancelled'); + } + } + + void _cancelMultipleTradeTimers(List tradeIds) { + for (final tradeId in tradeIds) { + _cancelSingleTradeTimer(tradeId); + } + } + + /// This is called when the app is brought back to foreground. + void resumeTradeMonitoring() { + if (appStore.wallet != null) { + monitorActiveTrades(appStore.wallet!.id); + } + } + + /// There's no need to run the trade checks when the app is in background. + /// We only want to update the trade status when the app is in foreground. + /// This helps to reduce the battery usage, network usage and enhance overall privacy. + /// + /// This is called when the app is sent to background or when the app is closed. + void stopTradeMonitoring() { + printV('Stopping trade monitoring'); + _cancelMultipleTradeTimers(_tradeTimers.keys.toList()); + } +} diff --git a/lib/di.dart b/lib/di.dart index 140a26698..1d925150f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -275,6 +275,7 @@ import 'src/screens/buy/buy_sell_page.dart'; import 'cake_pay/cake_pay_payment_credantials.dart'; import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart'; import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart'; +import 'package:cake_wallet/core/trade_monitor.dart'; final getIt = GetIt.instance; @@ -507,19 +508,42 @@ Future setup({ settingsStore: getIt.get(), fiatConvertationStore: getIt.get())); - getIt.registerFactory(() => DashboardViewModel( - balanceViewModel: getIt.get(), - appStore: getIt.get(), + getIt.registerFactory( + () => ExchangeViewModel( + getIt.get(), + _tradesSource, + getIt.get(), + getIt.get(), + getIt.get().settingsStore, + getIt.get(), + getIt.get(), + getIt.get(), + ), + ); + + getIt.registerSingleton( + TradeMonitor( tradesStore: getIt.get(), - tradeFilterStore: getIt.get(), - transactionFilterStore: getIt.get(), - settingsStore: settingsStore, - yatStore: getIt.get(), - ordersStore: getIt.get(), - anonpayTransactionsStore: getIt.get(), - payjoinTransactionsStore: getIt.get(), - sharedPreferences: getIt.get(), - keyService: getIt.get())); + trades: _tradesSource, + appStore: getIt.get(), + preferences: getIt.get(), + ), + ); + + getIt.registerFactory(() => DashboardViewModel( + tradeMonitor: getIt.get(), + balanceViewModel: getIt.get(), + appStore: getIt.get(), + tradesStore: getIt.get(), + tradeFilterStore: getIt.get(), + transactionFilterStore: getIt.get(), + settingsStore: settingsStore, + yatStore: getIt.get(), + ordersStore: getIt.get(), + anonpayTransactionsStore: getIt.get(), + payjoinTransactionsStore: getIt.get(), + sharedPreferences: getIt.get(), + keyService: getIt.get())); getIt.registerFactory( () => AuthService( @@ -1051,19 +1075,6 @@ Future setup({ getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); - getIt.registerFactory( - () => ExchangeViewModel( - getIt.get(), - _tradesSource, - getIt.get(), - getIt.get(), - getIt.get(), - getIt.get(), - getIt.get(), - getIt.get(), - ), - ); - getIt.registerFactory( () => FeesViewModel( getIt.get(), diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 238ad500c..9e384f462 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -24,6 +24,7 @@ class PreferencesKey { static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const isAppSecureKey = 'is_app_secure'; static const disableTradeOption = 'disable_buy'; + static const disableAutomaticExchangeStatusUpdates = 'disable_automatic_exchange_status_updates'; static const disableBulletinKey = 'disable_bulletin'; static const walletListOrder = 'wallet_list_order'; static const contactListOrder = 'contact_list_order'; diff --git a/lib/main.dart b/lib/main.dart index 2ea31bf03..6795b7ff9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,6 +50,7 @@ import 'package:cw_core/root_dir.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/window_size.dart'; import 'package:logging/logging.dart'; +import 'package:cake_wallet/core/trade_monitor.dart'; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); @@ -297,6 +298,7 @@ class AppState extends State with SingleTickerProviderStateMixin { final appStore = getIt.get(); final authService = getIt.get(); final linkViewModel = getIt.get(); + final tradeMonitor = getIt.get(); final statusBarColor = Colors.transparent; final authenticationStore = getIt.get(); final initialRoute = authenticationStore.state == AuthenticationState.uninitialized @@ -317,6 +319,7 @@ class AppState extends State with SingleTickerProviderStateMixin { navigatorKey: navigatorKey, authService: authService, linkViewModel: linkViewModel, + tradeMonitor: tradeMonitor, child: ThemeProvider( themeStore: appStore.themeStore, materialAppBuilder: (context, theme, darkTheme, themeMode) => MaterialApp( diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 47e900959..7a3aa156a 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -166,17 +166,19 @@ class TransactionsPage extends StatelessWidget { return Observer( builder: (_) => TradeRow( - key: item.key, - onTap: () => Navigator.of(context) - .pushNamed(Routes.tradeDetails, arguments: trade), + key: item.key, + onTap: () => Navigator.of(context) + .pushNamed(Routes.tradeDetails, arguments: trade), + swapState: trade.state, provider: trade.provider, - from: trade.from, - to: trade.to, - createdAtFormattedDate: trade.createdAt != null - ? DateFormat('HH:mm').format(trade.createdAt!) - : null, - formattedAmount: item.tradeFormattedAmount, - formattedReceiveAmount: item.tradeFormattedReceiveAmount), + from: trade.from, + to: trade.to, + createdAtFormattedDate: trade.createdAt != null + ? DateFormat('HH:mm').format(trade.createdAt!) + : null, + formattedAmount: item.tradeFormattedAmount, + formattedReceiveAmount: item.tradeFormattedReceiveAmount + ), ); } if (item is OrderListItem) { diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index 9f04d587d..e50cb9718 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -1,7 +1,9 @@ +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/utils/image_utill.dart'; import 'package:flutter/material.dart'; -import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cw_core/crypto_currency.dart'; class TradeRow extends StatelessWidget { TradeRow({ @@ -12,6 +14,7 @@ class TradeRow extends StatelessWidget { this.onTap, this.formattedAmount, this.formattedReceiveAmount, + required this.swapState, super.key, }); @@ -22,6 +25,7 @@ class TradeRow extends StatelessWidget { final String? createdAtFormattedDate; final String? formattedAmount; final String? formattedReceiveAmount; + final TradeState swapState; @override Widget build(BuildContext context) { @@ -29,25 +33,39 @@ class TradeRow extends StatelessWidget { final receiveAmountCrypto = to.toString(); return InkWell( - onTap: onTap, - child: Container( - padding: EdgeInsets.fromLTRB(24, 8, 24, 8), - color: Colors.transparent, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(50), - child: ImageUtil.getImageFromPath( - imagePath: provider.image, - height: 36, - width: 36, + onTap: onTap, + child: Container( + padding: EdgeInsets.fromLTRB(24, 8, 24, 8), + color: Colors.transparent, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(50), + child: ImageUtil.getImageFromPath( + imagePath: provider.image, height: 36, width: 36),), + Positioned( + right: 0, + bottom: 2, + child: Container( + height: 8, + width: 8, + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _statusColor(context, swapState), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], ), - ), - SizedBox(width: 12), - Expanded( - child: Column( + SizedBox(width: 12), + Expanded( + child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( @@ -104,4 +122,21 @@ class TradeRow extends StatelessWidget { ), ); } + + Color _statusColor(BuildContext context, TradeState status) { + switch (status) { + case TradeState.complete: + case TradeState.completed: + case TradeState.finished: + case TradeState.success: + case TradeState.settled: + return PaletteDark.brightGreen; + case TradeState.failed: + case TradeState.expired: + case TradeState.notFound: + return Palette.darkRed; + default: + return const Color(0xffff6600); + } + } } diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 6112673b1..19e8cab5f 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; +import 'package:cake_wallet/core/trade_monitor.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -27,6 +28,7 @@ class Root extends StatefulWidget { required this.navigatorKey, required this.authService, required this.linkViewModel, + required this.tradeMonitor, }) : super(key: key); final AuthenticationStore authenticationStore; @@ -35,6 +37,7 @@ class Root extends StatefulWidget { final AuthService authService; final Widget child; final LinkViewModel linkViewModel; + final TradeMonitor tradeMonitor; @override RootState createState() => RootState(); @@ -141,6 +144,8 @@ class RootState extends State with WidgetsBindingObserver { bitcoin!.stopPayjoinSessions(widget.appStore.wallet!); } + widget.tradeMonitor.stopTradeMonitoring(); + break; case AppLifecycleState.resumed: widget.authService.requireAuth().then((value) { @@ -154,6 +159,8 @@ class RootState extends State with WidgetsBindingObserver { widget.appStore.settingsStore.usePayjoin) { bitcoin!.resumePayjoinSessions(widget.appStore.wallet!); } + + widget.tradeMonitor.resumeTradeMonitoring(); break; default: break; diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 2d1856eca..792891453 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -75,30 +75,41 @@ class PrivacyPage extends BasePage { ), if (DeviceInfo.instance.isMobile) SettingsSwitcherCell( - title: S.current.prevent_screenshots, - value: _privacySettingsViewModel.isAppSecure, - onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setIsAppSecure(value); - }), - SettingsSwitcherCell( - title: S.current.disable_buy, - value: _privacySettingsViewModel.disableTradeOption, + title: S.current.prevent_screenshots, + value: _privacySettingsViewModel.isAppSecure, onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setDisableTradeOption(value); - }), + _privacySettingsViewModel.setIsAppSecure(value); + }, + ), SettingsSwitcherCell( - title: S.current.disable_bulletin, - value: _privacySettingsViewModel.disableBulletin, - onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setDisableBulletin(value); - }), + title: S.current.disable_buy, + value: _privacySettingsViewModel.disableTradeOption, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setDisableTradeOption(value); + }, + ), + SettingsSwitcherCell( + title: S.current.disable_automatic_exchange_status_updates, + value: _privacySettingsViewModel.disableAutomaticExchangeStatusUpdates, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setDisableAutomaticExchangeStatusUpdates(value); + }, + ), + SettingsSwitcherCell( + title: S.current.disable_bulletin, + value: _privacySettingsViewModel.disableBulletin, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setDisableBulletin(value); + }, + ), if (_privacySettingsViewModel.canUseEtherscan) SettingsSwitcherCell( - title: S.current.etherscan_history, - value: _privacySettingsViewModel.useEtherscan, - onValueChange: (BuildContext _, bool value) { - _privacySettingsViewModel.setUseEtherscan(value); - }), + title: S.current.etherscan_history, + value: _privacySettingsViewModel.useEtherscan, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setUseEtherscan(value); + }, + ), if (_privacySettingsViewModel.canUsePolygonScan) SettingsSwitcherCell( title: S.current.polygonscan_history, diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index fb02dcc21..c88590475 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -64,6 +64,7 @@ abstract class SettingsStoreBase with Store { required NanoSeedType initialNanoSeedType, required bool initialAppSecure, required bool initialDisableTrade, + required bool initialDisableAutomaticExchangeStatusUpdates, required FilterListOrderType initialWalletListOrder, required FilterListOrderType initialContactListOrder, required bool initialDisableBulletin, @@ -156,6 +157,7 @@ abstract class SettingsStoreBase with Store { numberOfFailedTokenTrials = initialFailedTokenTrial, isAppSecure = initialAppSecure, disableTradeOption = initialDisableTrade, + disableAutomaticExchangeStatusUpdates = initialDisableAutomaticExchangeStatusUpdates, disableBulletin = initialDisableBulletin, walletListOrder = initialWalletListOrder, contactListOrder = initialContactListOrder, @@ -307,6 +309,9 @@ abstract class SettingsStoreBase with Store { reaction((_) => disableTradeOption, (bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption)); + reaction((_) => disableAutomaticExchangeStatusUpdates, + (bool disableAutomaticExchangeStatusUpdates) => sharedPreferences.setBool(PreferencesKey.disableAutomaticExchangeStatusUpdates, disableAutomaticExchangeStatusUpdates)); + reaction( (_) => disableBulletin, (bool disableBulletin) => @@ -675,6 +680,9 @@ abstract class SettingsStoreBase with Store { @observable bool disableTradeOption; + @observable + bool disableAutomaticExchangeStatusUpdates; + @observable FilterListOrderType contactListOrder; @@ -956,6 +964,7 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false; final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false; + final disableAutomaticExchangeStatusUpdates = sharedPreferences.getBool(PreferencesKey.disableAutomaticExchangeStatusUpdates) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final walletListOrder = FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; @@ -1273,6 +1282,7 @@ abstract class SettingsStoreBase with Store { initialNanoSeedType: nanoSeedType, initialAppSecure: isAppSecure, initialDisableTrade: disableTradeOption, + initialDisableAutomaticExchangeStatusUpdates: disableAutomaticExchangeStatusUpdates, initialDisableBulletin: disableBulletin, initialWalletListOrder: walletListOrder, initialWalletListAscending: walletListAscending, @@ -1436,6 +1446,7 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption; + disableAutomaticExchangeStatusUpdates = sharedPreferences.getBool(PreferencesKey.disableAutomaticExchangeStatusUpdates) ?? disableAutomaticExchangeStatusUpdates; disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; walletListOrder = diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 0202d0d13..30bd1c8b3 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -23,7 +23,6 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; @@ -56,6 +55,8 @@ import 'package:mobx/mobx.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cake_wallet/core/trade_monitor.dart'; + part 'dashboard_view_model.g.dart'; class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel; @@ -63,6 +64,7 @@ class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel; abstract class DashboardViewModelBase with Store { DashboardViewModelBase( {required this.balanceViewModel, + required this.tradeMonitor, required this.appStore, required this.tradesStore, required this.tradeFilterStore, @@ -270,6 +272,9 @@ abstract class DashboardViewModelBase with Store { _checkMweb(); showDecredInfoCard = wallet?.type == WalletType.decred && sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) != false; + + tradeMonitor.stopTradeMonitoring(); + tradeMonitor.monitorActiveTrades(wallet!.id); }); _transactionDisposer?.reaction.dispose(); @@ -298,6 +303,10 @@ abstract class DashboardViewModelBase with Store { _checkMweb(); reaction((_) => settingsStore.mwebAlwaysScan, (bool value) => _checkMweb()); + + reaction((_) => tradesStore.trades, (_) => tradeMonitor.monitorActiveTrades(wallet.id)); + + tradeMonitor.monitorActiveTrades(wallet.id); } bool _isTransactionDisposerCallbackRunning = false; @@ -773,6 +782,8 @@ abstract class DashboardViewModelBase with Store { BalanceViewModel balanceViewModel; + TradeMonitor tradeMonitor; + AppStore appStore; SettingsStore settingsStore; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 05913befb..e78f61ec4 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -33,9 +33,8 @@ abstract class PrivacySettingsViewModelBase with Store { @action void setAutoGenerateSubaddresses(bool value) { _wallet.isEnabledAutoGenerateSubaddress = value; - _settingsStore.autoGenerateSubaddressStatus = value - ? AutoGenerateSubaddressStatus.enabled - : AutoGenerateSubaddressStatus.disabled; + _settingsStore.autoGenerateSubaddressStatus = + value ? AutoGenerateSubaddressStatus.enabled : AutoGenerateSubaddressStatus.disabled; } bool get isAutoGenerateSubaddressesVisible => [ @@ -61,6 +60,10 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get disableTradeOption => _settingsStore.disableTradeOption; + @computed + bool get disableAutomaticExchangeStatusUpdates => + _settingsStore.disableAutomaticExchangeStatusUpdates; + @computed bool get disableBulletin => _settingsStore.disableBulletin; @@ -129,6 +132,10 @@ abstract class PrivacySettingsViewModelBase with Store { @action void setDisableTradeOption(bool value) => _settingsStore.disableTradeOption = value; + @action + void setDisableAutomaticExchangeStatusUpdates(bool value) => + _settingsStore.disableAutomaticExchangeStatusUpdates = value; + @action void setDisableBulletin(bool value) => _settingsStore.disableBulletin = value; @@ -146,7 +153,7 @@ abstract class PrivacySettingsViewModelBase with Store { @action void setLookupsWellKnown(bool value) => _settingsStore.lookupsWellKnown = value; - + @action void setLookupsYatService(bool value) => _settingsStore.lookupsYatService = value; @@ -175,8 +182,7 @@ abstract class PrivacySettingsViewModelBase with Store { } @action - void setUseMempoolFeeAPI(bool value) => - _settingsStore.useMempoolFeeAPI = value; + void setUseMempoolFeeAPI(bool value) => _settingsStore.useMempoolFeeAPI = value; @action void setUsePayjoin(bool value) { diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 2b8452428..3c0d4f48e 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -250,6 +250,7 @@ "digit_pin": "-رقم PIN", "digital_and_physical_card": " بطاقة ائتمان رقمية ومادية مسبقة الدفع", "disable": "إبطال", + "disable_automatic_exchange_status_updates": "تعطيل تحديثات حالة التبادل التلقائي", "disable_bulletin": "تعطيل نشرة حالة الخدمة", "disable_buy": "تعطيل إجراء الشراء", "disable_cake_2fa": "تعطيل 2 عامل المصادقة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 166a47a60..e86a17f95 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -250,6 +250,7 @@ "digit_pin": "-цифрен PIN", "digital_and_physical_card": " дигитална или физическа предплатена дебитна карта", "disable": "Деактивиране", + "disable_automatic_exchange_status_updates": "Деактивирайте актуализациите на състоянието на автоматичния обмен", "disable_bulletin": "Деактивирайте бюлетина за състоянието на услугата", "disable_buy": "Деактивирайте действието за покупка", "disable_cake_2fa": "Деактивирайте Cake 2FA", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 1029e70a6..21a8b2cd4 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -250,6 +250,7 @@ "digit_pin": "-číselný PIN", "digital_and_physical_card": " digitální a fyzické předplacené debetní karty,", "disable": "Zakázat", + "disable_automatic_exchange_status_updates": "Zakázat aktualizace stavu automatické výměny", "disable_bulletin": "Zakázat status servisního stavu", "disable_buy": "Zakázat akci nákupu", "disable_cake_2fa": "Zakázat Cake 2FA", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 023efb7d8..17e75ded0 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -250,6 +250,7 @@ "digit_pin": "-stellige PIN", "digital_and_physical_card": "digitale und physische Prepaid-Debitkarte", "disable": "Deaktivieren", + "disable_automatic_exchange_status_updates": "Deaktivieren Sie die automatischen Austauschstatusaktualisierungen", "disable_bulletin": "Deaktivieren Sie das Bulletin des Service Status", "disable_buy": "Kaufaktion deaktivieren", "disable_cake_2fa": "Cake 2FA deaktivieren", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index bdc89c62b..59533730e 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -250,6 +250,7 @@ "digit_pin": "-digit PIN", "digital_and_physical_card": " digital and physical prepaid debit card", "disable": "Disable", + "disable_automatic_exchange_status_updates": "Disable Automatic Exchange Status Updates", "disable_bulletin": "Disable service status bulletin", "disable_buy": "Disable buy action", "disable_cake_2fa": "Disable Cake 2FA", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 51362512d..5d43cb1a2 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -250,6 +250,7 @@ "digit_pin": "-dígito PIN", "digital_and_physical_card": " tarjeta de débito prepago digital y física", "disable": "Desactivar", + "disable_automatic_exchange_status_updates": "Deshabilitar actualizaciones de estado de intercambio automático", "disable_bulletin": "Desactivar el boletín de estado del servicio", "disable_buy": "Desactivar acción de compra", "disable_cake_2fa": "Desactivar 2FA", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 60411d481..302435dcb 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -250,6 +250,7 @@ "digit_pin": " chiffres", "digital_and_physical_card": "carte de débit prépayée numérique et physique", "disable": "Désactiver", + "disable_automatic_exchange_status_updates": "Désactiver les mises à jour de l'état d'échange automatique", "disable_bulletin": "Désactiver le bulletin de statut de service", "disable_buy": "Désactiver l'action d'achat", "disable_cake_2fa": "Désactiver Cake 2FA", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 2dae85c50..6a3d2f2f0 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -250,6 +250,7 @@ "digit_pin": "-lambar PIN", "digital_and_physical_card": "katin zare kudi na dijital da na zahiri", "disable": "Kashe", + "disable_automatic_exchange_status_updates": "Musaki sabuntawar yanayin canji na atomatik", "disable_bulletin": "Musaki ma'aunin sabis na sabis", "disable_buy": "Kashe alama", "disable_cake_2fa": "Musaki Cake 2FA", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index fc1ffc296..bad13b469 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -250,6 +250,7 @@ "digit_pin": "-अंक पिन", "digital_and_physical_card": "डिजिटल और भौतिक प्रीपेड डेबिट कार्ड", "disable": "अक्षम करना", + "disable_automatic_exchange_status_updates": "स्वचालित एक्सचेंज स्टेटस अपडेट अक्षम करें", "disable_bulletin": "सेवा स्थिति बुलेटिन अक्षम करें", "disable_buy": "खरीद कार्रवाई अक्षम करें", "disable_cake_2fa": "केक 2FA अक्षम करें", @@ -568,8 +569,8 @@ "payjoin_unavailable_sheet_title": "Payjoin अनुपलब्ध क्यों है?", "payment_id": "भुगतान ID: ", "payment_made_easy": "भुगतान आसान किया गया", - "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "payment_was_received": "आपका भुगतान प्राप्त हुआ था।", + "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "payments": "भुगतान", "pending": " (अपूर्ण)", "percentageOf": "${amount} का", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 3330c52d7..286ced787 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -250,6 +250,7 @@ "digit_pin": "-znamenkasti PIN", "digital_and_physical_card": "digitalna i fizička unaprijed plaćena debitna kartica", "disable": "Onemogući", + "disable_automatic_exchange_status_updates": "Onemogućite ažuriranja automatskog statusa razmjene", "disable_bulletin": "Onemogućite bilten o statusu usluge", "disable_buy": "Onemogući kupnju", "disable_cake_2fa": "Onemogući Cake 2FA", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 3eec382cc..b4d095027 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -250,6 +250,7 @@ "digit_pin": "-նիշ ՊԻՆ", "digital_and_physical_card": " թվային և ֆիզիկական նախավճարային դեբետային քարտ", "disable": "Անջատել", + "disable_automatic_exchange_status_updates": "Անջատեք ավտոմատ փոխանակման կարգավիճակի թարմացումները", "disable_bulletin": "Անջատել ծառայության վիճակի տեղեկագիրը", "disable_buy": "Անջատել գնում գործողությունը", "disable_cake_2fa": "Անջատել Cake 2FA", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 122730d02..d1d699150 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -250,6 +250,7 @@ "digit_pin": "-digit PIN", "digital_and_physical_card": " kartu debit pra-bayar digital dan fisik", "disable": "Cacat", + "disable_automatic_exchange_status_updates": "Nonaktifkan Pembaruan Status Pertukaran Otomatis", "disable_bulletin": "Nonaktifkan Buletin Status Layanan", "disable_buy": "Nonaktifkan tindakan beli", "disable_cake_2fa": "Nonaktifkan Kue 2FA", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 485b79a4d..019885f42 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -250,6 +250,7 @@ "digit_pin": "-cifre PIN", "digital_and_physical_card": "carta di debito prepagata digitale e fisica", "disable": "Disabilita", + "disable_automatic_exchange_status_updates": "Disabilita gli aggiornamenti sullo stato automatico di scambio", "disable_bulletin": "Disabilita bollettino dello stato del servizio", "disable_buy": "Disabilita l'azione di acquisto", "disable_cake_2fa": "Disabilita Cake 2FA", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 717e23ecf..c9021d7da 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -250,6 +250,7 @@ "digit_pin": "桁ピン", "digital_and_physical_card": "デジタルおよび物理プリペイドデビットカード", "disable": "無効にする", + "disable_automatic_exchange_status_updates": "自動交換ステータスの更新を無効にします", "disable_bulletin": "サービスステータス速報を無効にします", "disable_buy": "購入アクションを無効にする", "disable_cake_2fa": "Cake 2FA を無効にする", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index e9c332a10..a7ada45e7 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -250,6 +250,7 @@ "digit_pin": "자리 PIN", "digital_and_physical_card": " 디지털 및 실물 선불 직불 카드", "disable": "비활성화", + "disable_automatic_exchange_status_updates": "자동 교환 상태 업데이트를 비활성화합니다", "disable_bulletin": "서비스 상태 게시판 비활성화", "disable_buy": "구매 기능 비활성화", "disable_cake_2fa": "Cake 2FA 비활성화", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index f6d37f8b2..47f7acd20 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -250,6 +250,7 @@ "digit_pin": "-ဂဏန်း PIN", "digital_and_physical_card": " ဒစ်ဂျစ်တယ်နှင့် ရုပ်ပိုင်းဆိုင်ရာ ကြိုတင်ငွေပေးချေသော ဒက်ဘစ်ကတ်", "disable": "ပိတ်ပါ။", + "disable_automatic_exchange_status_updates": "အလိုအလျောက်လဲလှယ် status ကို updates များကို disable လုပ်ပါ", "disable_bulletin": "ဝန်ဆောင်မှုအခြေအနေစာစောင်ကိုပိတ်ပါ", "disable_buy": "ဝယ်ယူမှု လုပ်ဆောင်ချက်ကို ပိတ်ပါ။", "disable_cake_2fa": "ကိတ်မုန့် 2FA ကို ပိတ်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 007b2c8f3..41212aa5e 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -250,6 +250,7 @@ "digit_pin": "-cijferige PIN", "digital_and_physical_card": "digitale en fysieke prepaid debetkaart", "disable": "Uitzetten", + "disable_automatic_exchange_status_updates": "Schakel automatische uitwisselingsstatusupdates uit", "disable_bulletin": "Schakel servicestatus Bulletin uit", "disable_buy": "Koopactie uitschakelen", "disable_cake_2fa": "Taart 2FA uitschakelen", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index cdc7a3e65..16480285c 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -250,6 +250,7 @@ "digit_pin": "-znakowy PIN", "digital_and_physical_card": " cyfrowa i fizyczna przedpłacona karta debetowa", "disable": "Wyłącz", + "disable_automatic_exchange_status_updates": "Wyłącz automatyczne aktualizacje statusu wymiany", "disable_bulletin": "Wyłącz biuletyn", "disable_buy": "Wyłącz akcję kupna", "disable_cake_2fa": "Wyłącz Cake 2FA", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index b151578db..0f2213d2b 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -250,6 +250,7 @@ "digit_pin": "dígitos", "digital_and_physical_card": "cartão de débito pré-pago digital e físico", "disable": "Desativar", + "disable_automatic_exchange_status_updates": "Desativar atualizações automáticas de status de troca", "disable_bulletin": "Desativar boletim de status de serviço", "disable_buy": "Desativar ação de compra", "disable_cake_2fa": "Desabilitar o Cake 2FA", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 329a89f79..bbbc03164 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -250,6 +250,7 @@ "digit_pin": "-значный PIN", "digital_and_physical_card": "цифровая и физическая предоплаченная дебетовая карта", "disable": "Запрещать", + "disable_automatic_exchange_status_updates": "Отключить обновления автоматического статуса обмена", "disable_bulletin": "Отключить бюллетень статуса обслуживания", "disable_buy": "Отключить действие покупки", "disable_cake_2fa": "Отключить торт 2FA", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 45a7d3779..c5b540398 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -250,6 +250,7 @@ "digit_pin": "-หลัก PIN", "digital_and_physical_card": "บัตรเดบิตดิจิตอลและบัตรพื้นฐาน", "disable": "ปิดการใช้งาน", + "disable_automatic_exchange_status_updates": "ปิดใช้งานการอัปเดตสถานะการแลกเปลี่ยนอัตโนมัติ", "disable_bulletin": "ปิดการใช้งาน Bulletin สถานะบริการ", "disable_buy": "ปิดการใช้งานการซื้อ", "disable_cake_2fa": "ปิดการใช้งานเค้ก 2FA", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 4f7a1fd4b..23fedcdda 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -250,6 +250,7 @@ "digit_pin": "-digit PIN", "digital_and_physical_card": " digital at pisikal na prepaid debit card", "disable": "Huwag paganahin", + "disable_automatic_exchange_status_updates": "Huwag paganahin ang mga awtomatikong pag -update ng katayuan ng palitan", "disable_bulletin": "Huwag paganahin ang bulletin ng katayuan ng serbisyo", "disable_buy": "Huwag paganahin ang pagkilos ng pagbili", "disable_cake_2fa": "Huwag paganahin ang Cake 2FA", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index eda0a58bb..9396a9108 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -250,6 +250,7 @@ "digit_pin": " haneli PIN", "digital_and_physical_card": " Dijital para birimleri ile para yükleyebileceğiniz ve ek bilgiye gerek olmayan", "disable": "Devre dışı bırakmak", + "disable_automatic_exchange_status_updates": "Otomatik Değişim Durum Güncellemelerini Devre Dışı Bırak", "disable_bulletin": "Hizmet Durumu Bültenini Devre Dışı Bırak", "disable_buy": "Satın alma işlemini devre dışı bırak", "disable_cake_2fa": "Cake 2FA'yı Devre Dışı Bırak", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 6ed48398b..07e38ff3b 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -250,6 +250,7 @@ "digit_pin": "-значний PIN", "digital_and_physical_card": " цифрова та фізична передплачена дебетова картка", "disable": "Вимкнути", + "disable_automatic_exchange_status_updates": "Вимкнути автоматичні оновлення стану обміну", "disable_bulletin": "Вимкнути статус послуги", "disable_buy": "Вимкнути дію покупки", "disable_cake_2fa": "Вимкнути Cake 2FA", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index a62292fd6..9e4676d13 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -250,6 +250,7 @@ "digit_pin": "-ہندسوں کا پن", "digital_and_physical_card": " ڈیجیٹل اور فزیکل پری پیڈ ڈیبٹ کارڈ", "disable": "غیر فعال کریں۔", + "disable_automatic_exchange_status_updates": "خودکار تبادلہ کی حیثیت کی تازہ کاریوں کو غیر فعال کریں", "disable_bulletin": "خدمت کی حیثیت کا بلیٹن کو غیر فعال کریں", "disable_buy": "خرید ایکشن کو غیر فعال کریں۔", "disable_cake_2fa": "کیک 2FA کو غیر فعال کریں۔", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index aa36d4490..157232156 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -249,6 +249,7 @@ "digit_pin": "Mã PIN - số", "digital_and_physical_card": "thẻ ghi nợ trả trước kỹ thuật số và vật lý", "disable": "Vô hiệu hóa", + "disable_automatic_exchange_status_updates": "Tắt các bản cập nhật trạng thái trao đổi tự động", "disable_bulletin": "Vô hiệu hóa bản tin tình trạng dịch vụ", "disable_buy": "Vô hiệu hóa chức năng mua", "disable_cake_2fa": "Vô hiệu hóa 2FA Cake", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 1e660fa3c..2ab9c0a42 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -250,6 +250,7 @@ "digit_pin": "-díjíìtì òǹkà ìdánimọ̀ àdáni", "digital_and_physical_card": " káàdì ìrajà t'ara àti ti ayélujára", "disable": "Ko si", + "disable_automatic_exchange_status_updates": "Mu awọn imudojuiwọn ipo paṣipaarọ aifọwọyi", "disable_bulletin": "Mu blogti ipo ipo ṣiṣẹ", "disable_buy": "Ko iṣọrọ ọja", "disable_cake_2fa": "Ko 2FA Cake sii", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 8b5e7cd0a..e1b71508f 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -250,6 +250,7 @@ "digit_pin": "位 PIN", "digital_and_physical_card": "数字和物理预付借记卡", "disable": "停用", + "disable_automatic_exchange_status_updates": "禁用自动交换状态更新", "disable_bulletin": "禁用服务状态公告", "disable_buy": "禁用购买操作", "disable_cake_2fa": "禁用蛋糕 2FA", From b0edf1fe75e69d886c6b0aaff3baa4c6a53fbbcb Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:15:36 +0100 Subject: [PATCH 03/29] fix(desktop-icon): Desktop Icons Fix (#2303) This change: - Adds svg icons for history and notification - Updates the sidebar and services widget to use new images - Fixes services widget icon looking very tiny on desktop --- assets/images/history.svg | 10 +++++++++ assets/images/notif.svg | 3 +++ .../desktop_sidebar/side_menu_item.dart | 5 +++-- .../desktop_sidebar_wrapper.dart | 7 +------ lib/src/widgets/services_updates_widget.dart | 21 ++++++++++--------- 5 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 assets/images/history.svg create mode 100644 assets/images/notif.svg diff --git a/assets/images/history.svg b/assets/images/history.svg new file mode 100644 index 000000000..f308ab7e3 --- /dev/null +++ b/assets/images/history.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/notif.svg b/assets/images/notif.svg new file mode 100644 index 000000000..b1ff5b4fa --- /dev/null +++ b/assets/images/notif.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart index e8ad78a3d..1f169d202 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:flutter/material.dart'; class SideMenuItem extends StatelessWidget { @@ -46,8 +47,8 @@ class SideMenuItem extends StatelessWidget { color: _setColor(context), size: 30, ) - : Image.asset( - imagePath ?? '', + : CakeImageWidget( + imageUrl: imagePath ?? '', fit: BoxFit.cover, height: 30, width: 30, diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart index a1de585bd..a26f48928 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart @@ -63,9 +63,6 @@ class DesktopSidebarWrapper extends BasePage { final pageController = PageController(); - final selectedIconPath = 'assets/images/desktop_transactions_solid_icon.png'; - final unselectedIconPath = 'assets/images/desktop_transactions_outline_icon.png'; - double get sideMenuWidth => 76.0; @override @@ -103,9 +100,7 @@ class DesktopSidebarWrapper extends BasePage { } }, isSelected: desktopSidebarViewModel.currentPage == SidebarItem.transactions, - imagePath: desktopSidebarViewModel.currentPage == SidebarItem.transactions - ? selectedIconPath - : unselectedIconPath, + imagePath: 'assets/images/history.svg', ), SideMenuItem( widget: ServicesUpdatesWidget( diff --git a/lib/src/widgets/services_updates_widget.dart b/lib/src/widgets/services_updates_widget.dart index d669dba37..eaa50867c 100644 --- a/lib/src/widgets/services_updates_widget.dart +++ b/lib/src/widgets/services_updates_widget.dart @@ -3,11 +3,12 @@ import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/service_status.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/service_status_tile.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -40,16 +41,17 @@ class _ServicesUpdatesWidgetState extends State { ); }); }, - child: SvgPicture.asset( - "assets/images/notification_icon.svg", + child: CakeImageWidget( + imageUrl: "assets/images/notif.svg", color: Theme.of(context).colorScheme.onSurface, - width: 20, - placeholderBuilder: (_) => Icon(Icons.error), + width: DeviceInfo.instance.isDesktop ? 30 : 20, ), ); } return Padding( - padding: const EdgeInsets.only(left: 16, top: 12, right: 8, bottom: 8), + padding: DeviceInfo.instance.isDesktop + ? EdgeInsets.zero + : EdgeInsets.only(left: 16, top: 12, right: 8, bottom: 8), child: FutureBuilder( future: widget.servicesResponse, builder: (context, state) { @@ -135,11 +137,10 @@ class _ServicesUpdatesWidgetState extends State { : null, child: Stack( children: [ - SvgPicture.asset( - "assets/images/notification_icon.svg", + CakeImageWidget( + imageUrl: "assets/images/notif.svg", color: Theme.of(context).colorScheme.onSurface, - width: 20, - placeholderBuilder: (_) => Icon(Icons.error), + width: DeviceInfo.instance.isDesktop ? 30 : 20, ), if (state.hasData && state.data!.hasUpdates && !wasOpened) Container( From 8457a45c2a3599616623d01bc8fe0a4d28b545d5 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 11 Jun 2025 18:44:06 +0200 Subject: [PATCH 04/29] update-payjoin (#2281) * feat: upgrade to flutter_payjoin 0.23.0 * fix: dependency discrepancy freezed_annotation * feat(cw_bitcoin): use latest payjoin deps * ci: update rust toolchain * Update pubspec.yaml * trial: downgrade flutter payjoin ffi * some checks * trial 2 * [skip ci] use correct image for CI * fix: bump flutter sdk to 3.29.0 * partial revert --------- Co-authored-by: Czarek Nakamoto Co-authored-by: Omar Hatem --- .../workflows/automated_integration_test.yml | 2 +- .github/workflows/pr_test_build_android.yml | 7 +- .github/workflows/pr_test_build_linux.yml | 2 +- Dockerfile | 18 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 13 +- cw_bitcoin/lib/payjoin/manager.dart | 36 ++-- cw_bitcoin/lib/payjoin/payjoin_persister.dart | 66 ++++++ .../lib/payjoin/payjoin_receive_worker.dart | 11 +- .../lib/payjoin/payjoin_send_worker.dart | 11 +- cw_bitcoin/pubspec.lock | 189 +++++++++--------- cw_bitcoin/pubspec.yaml | 10 +- cw_bitcoin_cash/pubspec.yaml | 6 +- cw_core/pubspec.lock | 94 ++++----- cw_core/pubspec.yaml | 4 +- cw_decred/pubspec.lock | 14 +- cw_decred/pubspec.yaml | 4 +- cw_ethereum/pubspec.yaml | 2 +- cw_evm/pubspec.yaml | 4 +- cw_monero/pubspec.lock | 8 +- cw_monero/pubspec.yaml | 4 +- cw_nano/pubspec.lock | 137 +++++++------ cw_nano/pubspec.yaml | 5 +- cw_polygon/pubspec.yaml | 2 +- cw_solana/pubspec.yaml | 4 +- cw_tron/pubspec.yaml | 4 +- cw_wownero/pubspec.lock | 55 +++-- cw_wownero/pubspec.yaml | 6 +- cw_zano/pubspec.lock | 55 +++-- cw_zano/pubspec.yaml | 6 +- docs/builds/ANDROID.md | 4 +- docs/builds/IOS.md | 8 +- docs/builds/LINUX.md | 4 +- docs/builds/MACOS.md | 8 +- docs/builds/WINDOWS.md | 6 +- lib/bitcoin/cw_bitcoin.dart | 1 + pubspec_base.yaml | 9 +- scripts/linux/build_cake_release.sh | 2 +- scripts/windows/Dockerfile.windows | 2 +- 38 files changed, 477 insertions(+), 346 deletions(-) create mode 100644 cw_bitcoin/lib/payjoin/payjoin_persister.dart diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 84c680dda..a26e3645d 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -55,7 +55,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.27.4" + flutter-version: "3.27.0" channel: stable - name: Install package dependencies diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 8f6139747..cdbd7ca37 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet @@ -253,6 +253,11 @@ jobs: - name: Build generated code run: | + flutter --version + flutter clean + rm -rf .dart_tool + rm pubspec.lock + flutter pub get ./model_generator.sh async - name: Generate key properties diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 476a033a0..119cd7530 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/Dockerfile b/Dockerfile index 84179d645..151b7af20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 +# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly # Heavily inspired by cirrusci images # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile @@ -15,11 +15,11 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet ENV GOLANG_VERSION=1.24.1 # Pin Flutter version to latest known-working version -ENV FLUTTER_VERSION=3.27.4 +ENV FLUTTER_VERSION=3.27.0 # Pin Android Studio, platform, and build tools versions to latest known-working version # Comes from https://developer.android.com/studio/#command-tools -ENV ANDROID_SDK_TOOLS_VERSION=11076708 +ENV ANDROID_SDK_TOOLS_VERSION=13114758 # Comes from https://developer.android.com/studio/releases/build-tools ENV ANDROID_PLATFORM_VERSION=35 ENV ANDROID_BUILD_TOOLS_VERSION=34.0.0 @@ -164,9 +164,12 @@ RUN (addgroup kvm || true) && \ ENV PATH=${HOME}/.cargo/bin:${PATH} RUN curl https://sh.rustup.rs -sSf | bash -s -- -y && \ cargo install cargo-ndk && \ + for toolchain in stable nightly; \ + do \ for target in aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu aarch64-unknown-linux-gnu; \ do \ - rustup target add --toolchain stable $target; \ + rustup target add --toolchain $toolchain $target; \ + done \ done # Download and install Flutter @@ -175,8 +178,11 @@ ENV FLUTTER_HOME=${HOME}/sdks/flutter/${FLUTTER_VERSION} ENV FLUTTER_ROOT=$FLUTTER_HOME ENV PATH=${PATH}:${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin -RUN git clone --depth 1 --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} \ - && yes | flutter doctor --android-licenses \ +RUN git clone --branch ${FLUTTER_VERSION} https://github.com/flutter/flutter.git ${FLUTTER_HOME} && \ + cd ${FLUTTER_HOME} && \ + git fetch -a + +RUN yes | flutter doctor --android-licenses \ && flutter doctor \ && chown -R root:root ${FLUTTER_HOME} diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 0fefe4e57..a92c4770f 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -31,12 +31,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S final PayjoinManager payjoinManager; - @observable payjoin.Receiver? currentPayjoinReceiver; - @computed - String? get payjoinEndpoint => - currentPayjoinReceiver?.pjUriBuilder().build().pjEndpoint(); + @observable + String? payjoinEndpoint = null; @override String getAddress( @@ -59,14 +57,19 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return generateP2WPKHAddress(hd: hd, index: index, network: network); } + @action Future initPayjoin() async { + await payjoinManager.initPayjoin(); currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); - + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); + payjoinManager.resumeSessions(); } + @action Future newPayjoinReceiver() async { currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); printV("Initializing new Payjoin Receiver"); payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!); diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart index 7ba3ceb9b..ad6fef0d8 100644 --- a/cw_bitcoin/lib/payjoin/manager.dart +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_persister.dart'; import 'package:cw_bitcoin/payjoin/payjoin_receive_worker.dart'; import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; @@ -16,6 +17,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/receive.dart'; import 'package:payjoin_flutter/send.dart'; +import 'package:payjoin_flutter/src/config.dart' as pj_config; import 'package:payjoin_flutter/uri.dart' as PayjoinUri; class PayjoinManager { @@ -31,11 +33,13 @@ class PayjoinManager { 'https://ohttp.cakewallet.com', ]; - static Future randomOhttpRelayUrl() => PayjoinUri.Url.fromStr( - ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]); + static String randomOhttpRelayUrl() => + ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]; static const payjoinDirectoryUrl = 'https://payjo.in'; + Future initPayjoin() => pj_config.PConfig.initializeApp(); + Future resumeSessions() async { final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id); @@ -43,11 +47,11 @@ class PayjoinManager { if (session.isSenderSession) { printV("Resuming Payjoin Sender Session ${session.pjUri!}"); return _spawnSender( - sender: Sender.fromJson(session.sender!), + sender: Sender.fromJson(json: session.sender!), pjUri: session.pjUri!, ); } - final receiver = Receiver.fromJson(session.receiver!); + final receiver = Receiver.fromJson(json: session.receiver!); printV("Resuming Payjoin Receiver Session ${receiver.id()}"); return _spawnReceiver(receiver: receiver); }); @@ -66,7 +70,12 @@ class PayjoinManager { psbtBase64: originalPsbt, pjUri: pjUri, ); - return senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + final persister = PayjoinSenderPersister.impl(); + final newSender = + await senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + final senderToken = await newSender.persist(persister: persister); + + return Sender.load(token: senderToken, persister: persister); } catch (e) { throw Exception('Error initializing Payjoin Sender: $e'); } @@ -143,21 +152,21 @@ class PayjoinManager { Future initReceiver(String address, [bool isTestnet = false]) async { try { - final payjoinDirectory = - await PayjoinUri.Url.fromStr(payjoinDirectoryUrl); - final ohttpKeys = await PayjoinUri.fetchOhttpKeys( ohttpRelay: await randomOhttpRelayUrl(), - payjoinDirectory: payjoinDirectory, + payjoinDirectory: payjoinDirectoryUrl, ); - final receiver = await Receiver.create( + final newReceiver = await NewReceiver.create( address: address, network: isTestnet ? Network.testnet : Network.bitcoin, - directory: payjoinDirectory, + directory: payjoinDirectoryUrl, ohttpKeys: ohttpKeys, - ohttpRelay: await randomOhttpRelayUrl(), ); + final persister = PayjoinReceiverPersister.impl(); + final receiverToken = await newReceiver.persist(persister: persister); + final receiver = + await Receiver.load(persister: persister, token: receiverToken); await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); @@ -195,7 +204,8 @@ class PayjoinManager { rawAmount = getOutputAmountFromTx(tx, _wallet); break; case PayjoinReceiverRequestTypes.checkIsOwned: - (_wallet.walletAddresses as BitcoinWalletAddresses).newPayjoinReceiver(); + (_wallet.walletAddresses as BitcoinWalletAddresses) + .newPayjoinReceiver(); _payjoinStorage.markReceiverSessionInProgress(receiver.id()); final inputScript = message['input_script'] as Uint8List; diff --git a/cw_bitcoin/lib/payjoin/payjoin_persister.dart b/cw_bitcoin/lib/payjoin/payjoin_persister.dart new file mode 100644 index 000000000..4e395e36a --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_persister.dart @@ -0,0 +1,66 @@ +import 'package:payjoin_flutter/src/generated/api/receive.dart'; +import 'package:payjoin_flutter/src/generated/api/send.dart'; + +class PayjoinSenderPersister implements DartSenderPersister { + static DartSenderPersister impl() { + final impl = PayjoinSenderPersister(); + return DartSenderPersister( + save: (sender) => impl.save(sender: sender), + load: (token) => impl.load(token: token), + ); + } + + final Map _store = {}; + + Future save({required FfiSender sender}) async { + final token = sender.key(); + _store[token.toBytes().toString()] = sender; + return token; + } + + Future load({required SenderToken token}) async { + final sender = _store[token.toBytes().toString()]; + if (sender == null) { + throw Exception('Sender not found for the provided token.'); + } + return sender; + } + + @override + void dispose() => _store.clear(); + + @override + bool get isDisposed => _store.isEmpty; +} + +class PayjoinReceiverPersister implements DartReceiverPersister { + static DartReceiverPersister impl() { + final impl = PayjoinReceiverPersister(); + return DartReceiverPersister( + save: (receiver) => impl.save(receiver: receiver), + load: (token) => impl.load(token: token), + ); + } + + final Map _store = {}; + + Future save({required FfiReceiver receiver}) async { + final token = receiver.key(); + _store[token.toBytes().toString()] = receiver; + return token; + } + + Future load({required ReceiverToken token}) async { + final receiver = _store[token.toBytes().toString()]; + if (receiver == null) { + throw Exception('Receiver not found for the provided token.'); + } + return receiver; + } + + @override + void dispose() => _store.clear(); + + @override + bool get isDisposed => _store.isEmpty; +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart index a499660b0..e86624291 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart @@ -4,6 +4,7 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_bitcoin/psbt/signer.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -42,7 +43,7 @@ class PayjoinReceiverWorker { try { final httpClient = http.Client(); - final receiver = Receiver.fromJson(receiverJson); + final receiver = Receiver.fromJson(json: receiverJson); final uncheckedProposal = await worker.receiveUncheckedProposal(httpClient, receiver); @@ -101,7 +102,8 @@ class PayjoinReceiverWorker { http.Client httpClient, Receiver session) async { while (true) { printV("Polling for Proposal (${session.id()})"); - final extractReq = await session.extractReq(); + final extractReq = await session.extractReq( + ohttpRelay: PayjoinManager.randomOhttpRelayUrl()); final request = extractReq.$1; final url = Uri.parse(request.url.asString()); @@ -116,7 +118,8 @@ class PayjoinReceiverWorker { Future sendFinalProposal( http.Client httpClient, PayjoinProposal finalProposal) async { - final req = await finalProposal.extractV2Req(); + final req = await finalProposal.extractReq( + ohttpRelay: PayjoinManager.randomOhttpRelayUrl()); final proposalReq = req.$1; final proposalCtx = req.$2; @@ -214,6 +217,6 @@ class PayjoinReceiverWorker { sequence: 0, ); - return InputPair.newInstance(txin, psbtin); + return InputPair.newInstance(txin: txin, psbtin: psbtin); } } diff --git a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart index f720bac01..f02e59f47 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart @@ -9,6 +9,8 @@ import 'package:http/http.dart' as http; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/send.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; +import 'package:payjoin_flutter/src/generated/api/send/error.dart' as pj_error; +import 'package:payjoin_flutter/uri.dart' as pj_uri; enum PayjoinSenderRequestTypes { requestPosted, @@ -29,7 +31,7 @@ class PayjoinSenderWorker { final senderJson = args[1] as String; final pjUrl = args[2] as String; - final sender = Sender.fromJson(senderJson); + final sender = Sender.fromJson(json: senderJson); final worker = PayjoinSenderWorker._(sendPort, pjUrl); try { @@ -51,9 +53,7 @@ class PayjoinSenderWorker { return await _runSenderV2(sender, httpClient); } catch (e) { printV(e); - if (e is PayjoinException && - // TODO condition on error type instead of message content - e.message?.contains('parse receiver public key') == true) { + if (e is pj_error.FfiCreateRequestError) { return await _runSenderV1(sender, httpClient); } else if (e is HttpException) { printV(e); @@ -68,7 +68,8 @@ class PayjoinSenderWorker { Future _runSenderV2(Sender sender, http.Client httpClient) async { try { final postRequest = await sender.extractV2( - ohttpProxyUrl: await PayjoinManager.randomOhttpRelayUrl(), + ohttpProxyUrl: + await pj_uri.Url.fromStr(PayjoinManager.randomOhttpRelayUrl()), ); final postResult = await _postRequest(httpClient, postRequest.$1); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 77037a979..83af100ae 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -5,34 +5,39 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.11.0" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.5.9" async: dependency: transitive description: @@ -121,10 +126,10 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_cli_annotations: dependency: transitive description: @@ -137,42 +142,42 @@ packages: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "8.0.0" built_collection: dependency: transitive description: @@ -185,10 +190,10 @@ packages: dependency: transitive description: name: built_value - sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.4" + version: "8.10.1" cake_backup: dependency: transitive description: @@ -296,10 +301,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.8" dart_varuint_bitcoin: dependency: transitive description: @@ -389,10 +394,10 @@ packages: dependency: transitive description: name: flutter_rust_bridge - sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00" + sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" url: "https://pub.dev" source: hosted - version: "2.7.1" + version: "2.9.0" flutter_test: dependency: "direct dev" description: flutter @@ -402,10 +407,10 @@ packages: dependency: transitive description: name: flutter_web_bluetooth - sha256: "1363831def5eed1e1064d1eca04e8ccb35446e8f758579c3c519e156b77926da" + sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -415,10 +420,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -439,18 +444,18 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.3+1" googleapis_auth: dependency: transitive description: name: googleapis_auth - sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db url: "https://pub.dev" source: hosted - version: "1.6.0" + version: "2.0.0" graphs: dependency: transitive description: @@ -463,10 +468,10 @@ packages: dependency: "direct main" description: name: grpc - sha256: "5b99b7a420937d4361ece68b798c9af8e04b5bc128a7859f2a4be87427694813" + sha256: "30e1edae6846b163a64f6d8716e3443980fe1f7d2d1f086f011d24ea186f2582" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.4" hex: dependency: transitive description: @@ -487,18 +492,18 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http2: dependency: transitive description: @@ -519,10 +524,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -592,10 +597,10 @@ packages: dependency: "direct main" description: name: ledger_flutter_plus - sha256: "1c03f3c4a9754b5f0170a9eb9552ec54fa86e985f8ee71a255ee2c5629b53d31" + sha256: "531da5daba5731d9eca2732881ef2f039b97bf8aa3564e7098dfa99a9b07a8e6" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.3" ledger_litecoin: dependency: "direct main" description: @@ -621,6 +626,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -665,10 +678,10 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.1" nested: dependency: transitive description: @@ -690,10 +703,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -714,10 +727,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -754,11 +767,11 @@ packages: dependency: "direct main" description: path: "." - ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c" - resolved-ref: "6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c" - url: "https://github.com/konstantinullrich/payjoin-flutter" + ref: da83a23f3a011cb49eb3b6513cd485b3fb8867ff + resolved-ref: da83a23f3a011cb49eb3b6513cd485b3fb8867ff + url: "https://github.com/OmarHatem28/payjoin-flutter" source: git - version: "0.21.0" + version: "0.23.0" petitparser: dependency: transitive description: @@ -811,26 +824,26 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" quiver: dependency: transitive description: @@ -859,18 +872,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -915,18 +928,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -944,18 +957,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.5" source_span: dependency: transitive description: @@ -973,14 +986,6 @@ packages: url: "https://github.com/cake-tech/sp_scanner" source: git version: "0.0.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" stack_trace: dependency: transitive description: @@ -1057,10 +1062,10 @@ packages: dependency: transitive description: name: universal_ble - sha256: "1fad089150a29db82b3b7d60327e18c5ad6b3a5bb509defc1c690b0a76b9c098" + sha256: "35d210e93a5938c6a6d1fd3c710cf4ac90b1bdd1b11c8eb2beeb32600672e6e6" url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.17.0" universal_platform: dependency: transitive description: @@ -1077,14 +1082,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" vector_math: dependency: transitive description: @@ -1121,18 +1118,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -1166,5 +1163,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index f45258f92..09369de8e 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -42,8 +42,8 @@ dependencies: url: https://github.com/cake-tech/bech32.git payjoin_flutter: git: - url: https://github.com/konstantinullrich/payjoin-flutter - ref: 6a3eb32fb9467ac12e7b75d3de47de4ca44fd88c #cake-v1 + url: https://github.com/OmarHatem28/payjoin-flutter + ref: da83a23f3a011cb49eb3b6513cd485b3fb8867ff #cake-v2 ledger_flutter_plus: ^1.4.1 ledger_bitcoin: git: @@ -58,10 +58,10 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 9a5c4f14f..e78261f9a 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -7,7 +7,7 @@ homepage: https://cakewallet.com environment: sdk: '>=2.19.0 <3.0.0' - flutter: ">=1.17.0" + flutter: ">=1.20.0" dependencies: flutter: @@ -33,9 +33,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 36bba72b6..c4e178ccf 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -26,18 +26,18 @@ packages: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.5.9" async: dependency: transitive description: @@ -67,50 +67,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: "direct dev" description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -123,10 +123,10 @@ packages: dependency: transitive description: name: built_value - sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.4" + version: "8.10.1" cake_backup: dependency: "direct main" description: @@ -212,10 +212,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" decimal: dependency: "direct main" description: @@ -326,10 +326,10 @@ packages: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -342,10 +342,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: "direct main" description: @@ -462,10 +462,10 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: "990da80722f7d7c0017dec92040b31545d625b15d40204c36a1e63d167c73cdc" + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" nested: dependency: transitive description: @@ -479,7 +479,7 @@ packages: description: path: "." ref: cake-update-v2 - resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + resolved-ref: "01cbbacbb05d2113aafa8b7c4a2bb766f749d8d8" url: "https://github.com/cake-tech/on_chain.git" source: git version: "3.7.0" @@ -487,10 +487,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -511,10 +511,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -583,26 +583,26 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" rational: dependency: transitive description: @@ -615,18 +615,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -780,18 +780,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -809,5 +809,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 9b38b61a7..03f039d9a 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -39,8 +39,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 hive_generator: ^2.0.1 diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index f9954fe53..f1d4dd57a 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -91,26 +91,26 @@ packages: dependency: "direct dev" description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -848,5 +848,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml index fcb2ac5ec..989831a89 100644 --- a/cw_decred/pubspec.yaml +++ b/cw_decred/pubspec.yaml @@ -19,8 +19,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.1.11 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 hive_generator: ^2.0.1 ffigen: ^16.1.0 diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml index 462e1d77e..c68f78fc9 100644 --- a/cw_ethereum/pubspec.yaml +++ b/cw_ethereum/pubspec.yaml @@ -29,7 +29,7 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 flutter: # assets: diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index 326ff4dc9..2e37a3b6e 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -41,9 +41,9 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 flutter_lints: ^2.0.0 flutter: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 02dcffa34..c449b8c8c 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -131,18 +131,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "8.0.0" built_collection: dependency: transitive description: diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 71d1b0c9b..a11e85b6c 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -34,8 +34,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 mockito: ^5.4.5 hive_generator: ^2.0.1 diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index ca8b61974..20b18e6ed 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -5,34 +5,39 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.11.0" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c" url: "https://pub.dev" source: hosted - version: "1.5.8" + version: "1.5.9" async: dependency: transitive description: @@ -86,50 +91,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: - dependency: "direct overridden" + dependency: transitive description: name: build_runner_core - sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.7+1" + version: "8.0.0" built_collection: dependency: transitive description: @@ -142,10 +147,10 @@ packages: dependency: transitive description: name: built_value - sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61" + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.4" + version: "8.10.1" cake_backup: dependency: transitive description: @@ -238,10 +243,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.8" decimal: dependency: "direct main" description: @@ -373,18 +378,18 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -397,10 +402,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" intl: dependency: transitive description: @@ -473,6 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -517,10 +530,10 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.1" nanodart: dependency: transitive description: @@ -551,7 +564,7 @@ packages: description: path: "." ref: cake-update-v2 - resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + resolved-ref: "01cbbacbb05d2113aafa8b7c4a2bb766f749d8d8" url: "https://github.com/cake-tech/on_chain.git" source: git version: "3.7.0" @@ -559,10 +572,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -583,10 +596,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -663,26 +676,26 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" rational: dependency: transitive description: @@ -695,18 +708,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -751,18 +764,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -780,18 +793,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.5" source_span: dependency: transitive description: @@ -916,18 +929,18 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -945,5 +958,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/cw_nano/pubspec.yaml b/cw_nano/pubspec.yaml index 3ddd9769e..a85a4aee5 100644 --- a/cw_nano/pubspec.yaml +++ b/cw_nano/pubspec.yaml @@ -31,13 +31,12 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 - build_runner_core: 7.2.7+1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_polygon/pubspec.yaml b/cw_polygon/pubspec.yaml index 8421562b4..bcbd80443 100644 --- a/cw_polygon/pubspec.yaml +++ b/cw_polygon/pubspec.yaml @@ -34,7 +34,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.4.7 + build_runner: ^2.4.15 # For information on the generic Dart part of this file, see the diff --git a/cw_solana/pubspec.yaml b/cw_solana/pubspec.yaml index 82b5a2bc0..c91ca6efc 100644 --- a/cw_solana/pubspec.yaml +++ b/cw_solana/pubspec.yaml @@ -33,9 +33,9 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index 80ea7ee51..57c99286c 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -31,9 +31,9 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 - build_runner: ^2.3.3 + build_runner: ^2.4.15 mobx_codegen: ^2.1.1 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 flutter: # assets: # - images/a_dot_burr.jpeg diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index dba8fb36b..dac4bda39 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.11.0" args: dependency: transitive description: @@ -86,26 +91,26 @@ packages: dependency: "direct dev" description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "8.0.0" built_collection: dependency: transitive description: @@ -214,10 +219,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.8" decimal: dependency: transitive description: @@ -336,10 +341,10 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: @@ -428,6 +433,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -472,10 +485,10 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.1" monero: dependency: "direct main" description: @@ -679,18 +692,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.5" source_span: dependency: transitive description: @@ -844,5 +857,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 6c1f37a40..4fda054b5 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -31,10 +31,10 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 - build_resolvers: ^2.0.9 + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 5dc9b5c4b..3c790399c 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "6.11.0" args: dependency: transitive description: @@ -86,26 +91,26 @@ packages: dependency: "direct dev" description: name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "8.0.0" built_collection: dependency: transitive description: @@ -214,10 +219,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.8" decimal: dependency: "direct main" description: @@ -333,10 +338,10 @@ packages: dependency: "direct dev" description: name: hive_generator - sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.1" http: dependency: "direct main" description: @@ -433,6 +438,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -477,10 +490,10 @@ packages: dependency: "direct dev" description: name: mobx_codegen - sha256: d4beb9cea4b7b014321235f8fdc7c2193ee0fe1d1198e9da7403f8bc85c4407c + sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.7.1" monero: dependency: "direct main" description: @@ -676,18 +689,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.5" source_span: dependency: transitive description: @@ -841,5 +854,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml index f69f427dc..529fe04eb 100644 --- a/cw_zano/pubspec.yaml +++ b/cw_zano/pubspec.yaml @@ -30,10 +30,10 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - build_runner: ^2.4.7 + build_runner: ^2.4.15 mobx_codegen: ^2.1.1 - build_resolvers: ^2.0.9 - hive_generator: ^1.1.3 + build_resolvers: ^2.4.4 + hive_generator: ^2.0.1 dependency_overrides: watcher: ^1.1.0 diff --git a/docs/builds/ANDROID.md b/docs/builds/ANDROID.md index 226883679..d7bc584ad 100644 --- a/docs/builds/ANDROID.md +++ b/docs/builds/ANDROID.md @@ -18,8 +18,8 @@ In order to build the latest version of Cake Wallet, simply run the following: git clone --branch main https://github.com/cake-tech/cake_wallet.git # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. cd cake_wallet -# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 . # Uncomment to build the docker image yourself instead of pulling it from the registry -docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 bash -x << EOF +# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF set -x -e pushd scripts/android source ./app_env.sh cakewallet diff --git a/docs/builds/IOS.md b/docs/builds/IOS.md index 44abaa805..1d23b917c 100644 --- a/docs/builds/IOS.md +++ b/docs/builds/IOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your iOS devi ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.27.4 +Flutter 3.27.0 ``` NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team. @@ -43,9 +43,9 @@ To enable iOS build support for Xcode, perform the following: ### 3. Installing Flutter -Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -65,7 +65,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ``` diff --git a/docs/builds/LINUX.md b/docs/builds/LINUX.md index a97a269a5..0f438a1d6 100644 --- a/docs/builds/LINUX.md +++ b/docs/builds/LINUX.md @@ -20,8 +20,8 @@ In order to build the latest version of Cake Wallet, simply run the following: git clone --branch main https://github.com/cake-tech/cake_wallet.git # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. cd cake_wallet -# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 . # Uncomment to build the docker image yourself instead of pulling it from the registry -docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1 bash -x << EOF +# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF set -x -e pushd scripts ./gen_android_manifest.sh diff --git a/docs/builds/MACOS.md b/docs/builds/MACOS.md index 46a9842a4..7e0f39aab 100644 --- a/docs/builds/MACOS.md +++ b/docs/builds/MACOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your macOS de ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.27.4 +Flutter 3.27.0 ``` ### 1. Installing dependencies @@ -34,9 +34,9 @@ sudo xcodebuild -runFirstLaunch ### 3. Installing Flutter -Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -56,7 +56,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) ... [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ... diff --git a/docs/builds/WINDOWS.md b/docs/builds/WINDOWS.md index 4fec78dc0..7a86dac49 100644 --- a/docs/builds/WINDOWS.md +++ b/docs/builds/WINDOWS.md @@ -6,18 +6,18 @@ The following are the system requirements to build Cake Wallet for your Windows ```txt Windows 10 or later (64-bit), x86-64 based -Flutter 3.27.4 +Flutter 3.27.0 ``` ### 1. Installing Flutter -Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). +Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). In order for Flutter to function, you'll also need to enable Developer Mode: Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode. -NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 2. Install Development Tools diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index cbdb343fe..131bc3a02 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -747,5 +747,6 @@ class CWBitcoin extends Bitcoin { final _wallet = wallet as ElectrumWallet; (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinManager.cleanupSessions(); (_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null; + (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint = null; } } diff --git a/pubspec_base.yaml b/pubspec_base.yaml index e43de5ca1..21166bd74 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -116,7 +116,7 @@ dependencies: git: url: https://github.com/cake-tech/on_chain.git ref: cake-update-v2 - reown_walletkit: ^1.1.2 + reown_walletkit: ^1.1.5+1 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils @@ -124,7 +124,7 @@ dependencies: flutter_daemon: git: url: https://github.com/MrCyjaneK/flutter_daemon - ref: 6d5270d64b5dd588fce12fd0a0c7314c37e6cff1 + ref: c24ee99f2f3070ea02d8108bbdd7727d73f7e5f1 flutter_local_notifications: ^19.0.0 dev_dependencies: @@ -133,10 +133,10 @@ dev_dependencies: integration_test: sdk: flutter mocktail: ^1.0.4 - build_runner: ^2.3.3 + build_runner: ^2.4.15 logging: ^1.2.0 mobx_codegen: ^2.1.1 - build_resolvers: ^2.0.9 + build_resolvers: ^2.4.4 hive_generator: ^2.0.1 # flutter_launcher_icons: ^0.11.0 # check flutter_launcher_icons for usage @@ -167,6 +167,7 @@ dependency_overrides: url: https://github.com/vespr-wallet/ledger-flutter-plus ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76 web_socket_channel: ^3.0.2 + freezed_annotation: 2.4.4 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/scripts/linux/build_cake_release.sh b/scripts/linux/build_cake_release.sh index e8ad6fc54..16f228d2e 100755 --- a/scripts/linux/build_cake_release.sh +++ b/scripts/linux/build_cake_release.sh @@ -14,7 +14,7 @@ cd "$SCRIPT_DIR" BUILD_AMD64=false BUILD_ARM64=false APP_TYPE="cakewallet" -DOCKER_IMAGE="ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1" +DOCKER_IMAGE="ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly" # Parse arguments for arg in "$@" diff --git a/scripts/windows/Dockerfile.windows b/scripts/windows/Dockerfile.windows index 893009b8f..a534be5e4 100644 --- a/scripts/windows/Dockerfile.windows +++ b/scripts/windows/Dockerfile.windows @@ -4,7 +4,7 @@ FROM mcr.microsoft.com/windows/servercore:ltsc2022 -ENV FLUTTER_VERSION=3.27.4 +ENV FLUTTER_VERSION=3.27.0 ENV GIT_VERSION=2.47.1 ENV VS_INSTALLED_DIR="C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools" ENV PATH="C:\Users\ContainerAdministrator\.cargo\bin;C:\ProgramData\chocolatey\bin;C:\flutter\flutter\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\ContainerAdministrator\AppData\Local\Microsoft\WindowsApps" From e5d0194f11dde616de8018bbd66d653ccff13f4a Mon Sep 17 00:00:00 2001 From: Luis Miguel Date: Wed, 11 Jun 2025 23:48:10 +0200 Subject: [PATCH 05/29] Improving spanish translation (#2313) Thanks for contibuting --- res/values/strings_es.arb | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 5d43cb1a2..06cda2251 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -78,7 +78,7 @@ "background_sync_on_battery_low": "Sincronizar con batería baja", "background_sync_on_charging": "Sincronizar solo al cargar", "background_sync_on_device_idle": "Sincronizar solo cuando el dispositivo no se usa", - "background_sync_on_unmetered_network": "Requerir una red no metida", + "background_sync_on_unmetered_network": "Requerir una red no medida", "backup": "Apoyo", "backup_file": "Archivo de respaldo", "backup_password": "Contraseña de respaldo", @@ -203,7 +203,7 @@ "contractSymbol": "Símbolo de contrato", "copied_key_to_clipboard": "Copiado ${key} al portapapeles", "copied_to_clipboard": "Copiado al portapapeles", - "copy": "Dupdo", + "copy": "Copiar", "copy_address": "Copiar dirección ", "copy_id": "Copiar ID", "copy_payjoin_address": "Copiar dirección de payjoin", @@ -565,7 +565,7 @@ "payjoin_request_awaiting_tx": "Esperando transacción", "payjoin_request_in_progress": "En curso", "payjoin_unavailable": "Payjoin no disponible", - "payjoin_unavailable_sheet_content": "Recibir una transacción Payjoin requiere que tenga Bitcoin listo para usar. \\ N \\ nas pronto como su billetera tenga fondos, Payjoin estará habilitado automáticamente.", + "payjoin_unavailable_sheet_content": "Recibir una transacción Payjoin requiere que tenga Bitcoin listo para usar.\n\nTan pronto como su billetera tenga fondos, Payjoin se habilitará automáticamente.", "payjoin_unavailable_sheet_title": "¿Por qué no está disponible Payjoin?", "payment_id": "ID de pago: ", "payment_made_easy": "Pagos hechos fáciles", @@ -668,7 +668,7 @@ "restore_from_date_or_blockheight": "Ingrese una fecha unos días antes de crear esta billetera. O si conoce la altura del bloque, ingréselo en su lugar", "restore_from_seed_placeholder": "Ingrese o pegue su frase de código aquí", "restore_new_seed": "Nueva semilla", - "restore_next": "Próximo", + "restore_next": "Seguir", "restore_recover": "Recuperar", "restore_restore_wallet": "Recuperar Cartera", "restore_seed_keys_restore": "Restauración de semillas / llaves", @@ -684,7 +684,7 @@ "restore_wallet_restore_description": "Restaurar billetera", "robinhood_option_description": "Compra y transfiere instantáneamente utilizando su tarjeta de débito, cuenta bancaria o saldo de Robinhood. Solo EE. UU.", "router_no_route": "No hay ruta definida para ${name}", - "save": "Salvar", + "save": "Guardar", "save_backup_password": "Asegúrese de haber guardado su contraseña de respaldo. No podrá importar sus archivos de respaldo sin él.", "save_backup_password_alert": "Guardar contraseña de respaldo", "save_to_downloads": "Guardar en Descargas", @@ -723,7 +723,7 @@ "seed_language_italian": "Italiana/Italiano", "seed_language_japanese": "Japonés", "seed_language_korean": "Coreano", - "seed_language_next": "Próximo", + "seed_language_next": "Seguir", "seed_language_portuguese": "Portugués", "seed_language_russian": "Ruso", "seed_language_spanish": "Español", @@ -806,21 +806,21 @@ "setup_pin": "PIN de configuración", "setup_successful": "Tu PIN se ha configurado correctamente!", "setup_totp_recommended": "Configurar TOTP", - "setup_warning_2fa_text": "Deberá restaurar su billetera a partir de la semilla mnemotécnica.\n\nEl soporte de Cake no podrá ayudarlo si pierde el acceso a su 2FA o a sus semillas mnemotécnicas.\nCake 2FA es una segunda autenticación para ciertas acciones en la billetera. Antes de usar Cake 2FA, recomendamos leer la guía.NO es tan seguro como el almacenamiento en frío.\n\nSi pierde el acceso a su aplicación 2FA o a sus claves TOTP, perderá el acceso a esta billetera. ", + "setup_warning_2fa_text": "Deberá restaurar su billetera a partir de la semilla mnemotécnica.\n\nEl soporte de Cake no podrá ayudarlo si pierde el acceso a su 2FA o a sus semillas mnemotécnicas.\nCake 2FA es una segunda autenticación para ciertas acciones en la billetera. Antes de usar Cake 2FA, recomendamos leer la guía. NO es tan seguro como el almacenamiento en frío.\n\nSi pierde el acceso a su aplicación 2FA o a sus claves TOTP, perderá el acceso a esta billetera. ", "setup_your_debit_card": "Configura tu tarjeta de débito", "share": "Compartir", "share_address": "Compartir dirección", "shared_seed_wallet_groups": "Grupos de billetera de semillas compartidas", - "show": "Espectáculo", + "show": "Mostrar", "show_address_book_popup": "Mostrar la ventana emergente de la libreta de direcciones", - "show_balance": "Prensa larga para mostrar equilibrio", - "show_balance_toast": "Prensa larga para esconder o mostrar equilibrio", + "show_balance": "Pulsación larga para mostrar balance", + "show_balance_toast": "Pulsación larga para ocultar o mostrar balance", "show_details": "Mostrar detalles", "show_keys": "Mostrar semilla/claves", "show_market_place": "Mostrar mercado", "show_seed": "Mostrar semilla", "sign_all": "Firmar todo", - "sign_message": "Mensaje de firma", + "sign_message": "Firmar mensaje", "sign_one": "Firmar", "sign_up": "Registrarse", "sign_verify_message": "Firmar / verificar", @@ -859,9 +859,9 @@ "success": "Éxito", "successful": "Exitoso", "support_description_guides": "Documentación y apoyo para problemas comunes", - "support_description_live_chat": "¡GRATIS y RÁPIDO! Los representantes de apoyo capacitado están disponibles para ayudar", - "support_description_other_links": "Únete a nuestras comunidades o comunícate con nosotros nuestros socios a través de otros métodos", - "support_title_guides": "Documentos de billetera de pastel", + "support_description_live_chat": "¡Gratis y rápido! Los representantes de soporte están disponibles para ayudar", + "support_description_other_links": "Únete a nuestras comunidades o comunícate con nosotros o nuestros socios a través de otros métodos", + "support_title_guides": "Documentación de Cake Wallet", "support_title_live_chat": "Soporte en tiempo real", "support_title_other_links": "Otros enlaces de soporte", "supported": "Compatible", @@ -985,7 +985,7 @@ "unavailable_balance": "Saldo no disponible", "unavailable_balance_description": "Saldo no disponible: este total incluye fondos que están bloqueados en transacciones pendientes y aquellos que usted ha congelado activamente en su configuración de control de monedas. Los saldos bloqueados estarán disponibles una vez que se completen sus respectivas transacciones, mientras que los saldos congelados permanecerán inaccesibles para las transacciones hasta que usted decida descongelarlos.", "unconfirmed": "Saldo no confirmado", - "understand": "Entiendo", + "understand": "Entendido", "unlock": "desbloquear", "unmatched_currencies": "La moneda de tu billetera actual no coincide con la del QR escaneado", "unrestricted_background_service": "Servicio de sincronización sin restricciones", @@ -1011,7 +1011,7 @@ "value_type": "Tipo de valor", "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", "verification": "Verificación", - "verify_message": "Mensaje de verificación", + "verify_message": "Verificar mensaje", "verify_seed": "Verificar semilla", "verify_with_2fa": "Verificar con Cake 2FA", "version": "Versión ${currentVersion}", From fe0c9ecc0eb6480a259152cfbd7d954698427ea5 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:18:46 +0100 Subject: [PATCH 06/29] CW-1084: Solana Issues (#2305) * fix(solana-issues): Fix missing solana transaction history entries * fix(solana-issues): Fixes issues relating to Solana Transaction History This change: - Modifies transaction parsing logic to handle more scenarios and better parse Solana transaction data. - Adds partial filter for spam transactions * fix(solana-issues): Enhance transaction parsing for Associated Token Account (ATA) programs This change: - Adds logic to differentiate between create account and token transfer transactions for the ATA program. - Introduces a check to skip transactions that only create accounts without associated token transfers. * fix(solana-issues): Improve transaction update logic and enhance error handling This change: - Updates the transaction update callback to only trigger when new valid transactions are present. - Enhances error handling for insufficient funds by distinguishing between errors for sender and receiver. --- cw_solana/lib/solana_client.dart | 379 ++++++++++++++--------- lib/view_model/send/send_view_model.dart | 11 +- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 3 +- res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + 30 files changed, 263 insertions(+), 157 deletions(-) diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 05b0cec82..128366c73 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -13,13 +13,17 @@ import 'package:cw_solana/solana_transaction_model.dart'; import 'package:cw_solana/spl_token.dart'; import 'package:http/http.dart' as http; import 'package:on_chain/solana/solana.dart'; +import 'package:on_chain/solana/src/instructions/associated_token_account/constant.dart'; import 'package:on_chain/solana/src/models/pda/pda.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:on_chain/solana/src/rpc/models/models/confirmed_transaction_meta.dart'; import '.secrets.g.dart' as secrets; class SolanaWalletClient { final httpClient = http.Client(); SolanaRPC? _provider; + // Minimum amount in SOL to consider a transaction valid (to filter spam) + static const double minValidAmount = 0.00000003; bool connect(Node node) { try { @@ -155,170 +159,88 @@ class SolanaWalletClient { if (meta == null || transaction == null) return null; final int fee = meta.fee; + final feeInSol = fee / SolanaUtils.lamportsPerSol; final message = transaction.message; final instructions = message.compiledInstructions; - String sender = ""; - String receiver = ""; - String signature = (txResponse.transaction?.signatures.isEmpty ?? true) ? "" : Base58Encoder.encode(txResponse.transaction!.signatures.first); + for (final instruction in instructions) { final programId = message.accountKeys[instruction.programIdIndex]; - if (programId == SystemProgramConst.programId) { + if (programId == SystemProgramConst.programId || + programId == ComputeBudgetConst.programId) { // For native solana transactions + if (instruction.accounts.length < 2) continue; - if (txResponse.version == TransactionType.legacy) { - // For legacy transfers, the fee payer (index 0) is the sender. - sender = message.accountKeys[0].address; + // Get the fee payer index based on transaction type + // For legacy transfers, the first account is usually the fee payer + // For versioned, the first account in instruction is usually the fee payer + final feePayerIndex = + txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0]; - final senderPreBalance = meta.preBalances[0]; - final senderPostBalance = meta.postBalances[0]; - final feeForTx = fee / SolanaUtils.lamportsPerSol; + final transactionModel = await _parseNativeTransaction( + message: message, + meta: meta, + fee: fee, + feeInSol: feeInSol, + feePayerIndex: feePayerIndex, + walletAddress: walletAddress, + signature: signature, + blockTime: blockTime, + ); - // The loss on the sender's account would include both the transfer amount and the fee. - // So we would subtract the fee to calculate the actual amount that was transferred (in lamports). - final transferLamports = (senderPreBalance - senderPostBalance) - BigInt.from(fee); - - // Next, we attempt to find the receiver by comparing the balance changes. - // (The index 0 is for the sender so we skip it.) - bool foundReceiver = false; - for (int i = 1; i < meta.preBalances.length; i++) { - // The increase in balance on the receiver account should correspond to the transfer amount we calculated earlieer. - final pre = meta.preBalances[i]; - final post = meta.postBalances[i]; - if ((post - pre) == transferLamports) { - receiver = message.accountKeys[i].address; - foundReceiver = true; - break; - } - } - - if (!foundReceiver) { - // Optionally (and rarely), if no account shows the exact expected change, - // we set the receiver address to unknown. - receiver = "unknown"; - } - - final amount = transferLamports / BigInt.from(1e9); - - return SolanaTransactionModel( - isOutgoingTx: sender == walletAddress, - from: sender, - to: receiver, - id: signature, - amount: amount.abs(), - programId: SystemProgramConst.programId.address, - tokenSymbol: 'SOL', - blockTimeInInt: blockTime?.toInt() ?? 0, - fee: feeForTx, - ); - } else { - if (instruction.accounts.length < 2) continue; - final senderIndex = instruction.accounts[0]; - final receiverIndex = instruction.accounts[1]; - - sender = message.accountKeys[senderIndex].address; - receiver = message.accountKeys[receiverIndex].address; - - final feeForTx = fee / SolanaUtils.lamportsPerSol; - - final preBalances = meta.preBalances; - final postBalances = meta.postBalances; - - final amountInString = - (((preBalances[senderIndex] - postBalances[senderIndex]) / BigInt.from(1e9)) - .toDouble() - - feeForTx) - .toStringAsFixed(6); - - final amount = double.parse(amountInString); - - return SolanaTransactionModel( - isOutgoingTx: sender == walletAddress, - from: sender, - to: receiver, - id: signature, - amount: amount.abs(), - programId: SystemProgramConst.programId.address, - tokenSymbol: 'SOL', - blockTimeInInt: blockTime?.toInt() ?? 0, - fee: feeForTx, - ); + if (transactionModel != null) { + return transactionModel; } } else if (programId == SPLTokenProgramConst.tokenProgramId) { // For SPL Token transactions if (instruction.accounts.length < 2) continue; - final preBalances = meta.preTokenBalances; - final postBalances = meta.postTokenBalances; - - double amount = 0.0; - bool isOutgoing = false; - String? mintAddress; - - double userPreAmount = 0.0; - if (preBalances != null && preBalances.isNotEmpty) { - for (final preBal in preBalances) { - if (preBal.owner?.address == walletAddress) { - userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; - - mintAddress = preBal.mint.address; - break; - } - } - } - - double userPostAmount = 0.0; - if (postBalances != null && postBalances.isNotEmpty) { - for (final postBal in postBalances) { - if (postBal.owner?.address == walletAddress) { - userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; - - mintAddress ??= postBal.mint.address; - break; - } - } - } - - final diff = userPreAmount - userPostAmount; - final rawAmount = diff.abs(); - - final amountInString = rawAmount.toStringAsFixed(6); - amount = double.parse(amountInString); - - isOutgoing = diff > 0; - - if (mintAddress == null && instruction.accounts.length >= 4) { - final mintIndex = instruction.accounts[3]; - mintAddress = message.accountKeys[mintIndex].address; - } - - final sender = message.accountKeys[instruction.accounts[0]].address; - final receiver = message.accountKeys[instruction.accounts[1]].address; - - String? tokenSymbol = splTokenSymbol; - - if (tokenSymbol == null && mintAddress != null) { - final token = await getTokenInfo(mintAddress); - tokenSymbol = token?.symbol; - } - - return SolanaTransactionModel( - isOutgoingTx: isOutgoing, - from: sender, - to: receiver, - id: signature, - amount: amount, - programId: SPLTokenProgramConst.tokenProgramId.address, - blockTimeInInt: blockTime?.toInt() ?? 0, - tokenSymbol: tokenSymbol ?? '', - fee: fee / SolanaUtils.lamportsPerSol, + final transactionModel = await _parseSPLTokenTransaction( + message: message, + meta: meta, + fee: fee, + feeInSol: feeInSol, + instruction: instruction, + walletAddress: walletAddress, + signature: signature, + blockTime: blockTime, + splTokenSymbol: splTokenSymbol, ); + + if (transactionModel != null) { + return transactionModel; + } + } else if (programId == AssociatedTokenAccountProgramConst.associatedTokenProgramId) { + // For ATA program, we need to check if this is a create account transaction + // or if it's part of a normal token transfer + + // We skip this transaction if this is the only instruction (this means that it's a create account transaction) + if (instructions.length == 1) { + return null; + } + + // We look for a token transfer instruction in the same transaction + bool hasTokenTransfer = false; + for (final otherInstruction in instructions) { + final otherProgramId = message.accountKeys[otherInstruction.programIdIndex]; + if (otherProgramId == SPLTokenProgramConst.tokenProgramId) { + hasTokenTransfer = true; + break; + } + } + + // If there's no token transfer instruction, it means this is just an ATA creation transaction + if (!hasTokenTransfer) { + return null; + } + + continue; } else { return null; } @@ -330,6 +252,144 @@ class SolanaWalletClient { return null; } + Future _parseNativeTransaction({ + required VersionedMessage message, + required ConfirmedTransactionMeta meta, + required int fee, + required double feeInSol, + required int feePayerIndex, + required String walletAddress, + required String signature, + required BigInt? blockTime, + }) async { + // Calculate total balance changes across all accounts + BigInt totalBalanceChange = BigInt.zero; + String? sender; + String? receiver; + + for (int i = 0; i < meta.preBalances.length; i++) { + final preBalance = meta.preBalances[i]; + final postBalance = meta.postBalances[i]; + final balanceChange = preBalance - postBalance; + + if (balanceChange > BigInt.zero) { + // This account sent funds + sender = message.accountKeys[i].address; + totalBalanceChange += balanceChange; + } else if (balanceChange < BigInt.zero) { + // This account received funds + receiver = message.accountKeys[i].address; + } + } + + // We subtract the fee from total balance change if the fee payer is the sender + if (sender == message.accountKeys[feePayerIndex].address) { + totalBalanceChange -= BigInt.from(fee); + } + + if (sender == null || receiver == null) { + return null; + } + + final amount = totalBalanceChange / BigInt.from(1e9); + final amountInSol = amount.abs().toDouble(); + + // Skip transactions with very small amounts (likely spam) + if (amountInSol < minValidAmount) { + return null; + } + + return SolanaTransactionModel( + isOutgoingTx: sender == walletAddress, + from: sender, + to: receiver, + id: signature, + amount: amountInSol, + programId: SystemProgramConst.programId.address, + tokenSymbol: 'SOL', + blockTimeInInt: blockTime?.toInt() ?? 0, + fee: feeInSol, + ); + } + + Future _parseSPLTokenTransaction({ + required VersionedMessage message, + required ConfirmedTransactionMeta meta, + required int fee, + required double feeInSol, + required CompiledInstruction instruction, + required String walletAddress, + required String signature, + required BigInt? blockTime, + String? splTokenSymbol, + }) async { + final preBalances = meta.preTokenBalances; + final postBalances = meta.postTokenBalances; + + double amount = 0.0; + bool isOutgoing = false; + String? mintAddress; + + double userPreAmount = 0.0; + if (preBalances != null && preBalances.isNotEmpty) { + for (final preBal in preBalances) { + if (preBal.owner?.address == walletAddress) { + userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; + + mintAddress = preBal.mint.address; + break; + } + } + } + + double userPostAmount = 0.0; + if (postBalances != null && postBalances.isNotEmpty) { + for (final postBal in postBalances) { + if (postBal.owner?.address == walletAddress) { + userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; + + mintAddress ??= postBal.mint.address; + break; + } + } + } + + final diff = userPreAmount - userPostAmount; + final rawAmount = diff.abs(); + + final amountInString = rawAmount.toStringAsFixed(6); + amount = double.parse(amountInString); + + isOutgoing = diff > 0; + + if (mintAddress == null && instruction.accounts.length >= 4) { + final mintIndex = instruction.accounts[3]; + mintAddress = message.accountKeys[mintIndex].address; + } + + final sender = message.accountKeys[instruction.accounts[0]].address; + final receiver = message.accountKeys[instruction.accounts[1]].address; + + String? tokenSymbol = splTokenSymbol; + + if (tokenSymbol == null && mintAddress != null) { + final token = await getTokenInfo(mintAddress); + tokenSymbol = token?.symbol; + } + + return SolanaTransactionModel( + isOutgoingTx: isOutgoing, + from: sender, + to: receiver, + id: signature, + amount: amount, + programId: SPLTokenProgramConst.tokenProgramId.address, + blockTimeInInt: blockTime?.toInt() ?? 0, + tokenSymbol: tokenSymbol ?? '', + fee: feeInSol, + ); + } + /// Load the Address's transactions into the account Future> fetchTransactions( SolAddress address, { @@ -381,11 +441,13 @@ class SolanaWalletClient { transactions.addAll(parsedTransactions.whereType().toList()); - // Calling the callback after each batch is processed, therefore passing the current list of transactions. - onUpdate(List.from(transactions)); + // Only update UI if we have new valid transactions + if (parsedTransactions.isNotEmpty) { + onUpdate(List.from(transactions)); + } if (i + batchSize < signatures.length) { - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 300)); } } @@ -732,19 +794,24 @@ class SolanaWalletClient { SolanaAccountInfo? accountInfo; try { accountInfo = await _provider!.request( - SolanaRPCGetAccountInfo(account: associatedTokenAccount.address), + SolanaRPCGetAccountInfo( + account: associatedTokenAccount.address, + commitment: Commitment.confirmed, + ), ); } catch (e) { accountInfo = null; } - // If aacountInfo is null, signifies that the associatedTokenAccount has only been created locally and not been broadcasted to the blockchain. + // If account exists, we return the associated token account if (accountInfo != null) return associatedTokenAccount; if (!shouldCreateATA) return null; + final payerAddress = payerPrivateKey.publicKey().toAddress(); + final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount( - payer: payerPrivateKey.publicKey().toAddress(), + payer: payerAddress, associatedToken: associatedTokenAccount.address, owner: ownerAddress, mint: mintAddress, @@ -753,19 +820,23 @@ class SolanaWalletClient { final blockhash = await _getLatestBlockhash(Commitment.confirmed); final transaction = SolanaTransaction( - payerKey: payerPrivateKey.publicKey().toAddress(), + payerKey: payerAddress, instructions: [createAssociatedTokenAccount], recentBlockhash: blockhash, + type: TransactionType.v0, ); - transaction.sign([payerPrivateKey]); + final serializedTransaction = await _signTransactionInternal( + ownerPrivateKey: payerPrivateKey, + transaction: transaction, + ); await sendTransaction( - serializedTransaction: transaction.serializeString(), + serializedTransaction: serializedTransaction, commitment: Commitment.confirmed, ); - // Delay for propagation on the blockchain for newly created associated token addresses + // Wait for confirmation await Future.delayed(const Duration(seconds: 2)); return associatedTokenAccount; @@ -890,7 +961,7 @@ class SolanaWalletClient { }) async { /// Sign the transaction with the owner's private key. final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage()); - + transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature); /// Serialize the transaction. diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index be354d976..0a4b7e841 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -767,8 +767,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.solana_no_associated_token_account_exception; } - if (errorMessage.contains('insufficient funds for rent')) { - return S.current.insufficientFundsForRentError; + if (errorMessage.contains('insufficient funds for rent') && + errorMessage.contains('Transaction simulation failed') && + errorMessage.contains('account_index')) { + final accountIndexMatch = RegExp(r'account_index: (\d+)').firstMatch(errorMessage); + if (accountIndexMatch != null) { + return int.parse(accountIndexMatch.group(1)!) == 0 + ? S.current.insufficientFundsForRentError + : S.current.insufficientFundsForRentErrorReceiver; + } } return errorMessage; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 3c0d4f48e..81f7fcb33 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "ليس لديك ما يكفي من SOL لتغطية المعاملة ورسوم المعاملات الخاصة بها. يرجى إضافة المزيد من SOL إلى محفظتك أو تقليل كمية SOL التي ترسلها.", "insufficient_lamports": "ليس لديك ما يكفي من SOL لتغطية المعاملة ورسوم المعاملات الخاصة بها. تحتاج على الأقل ${solValueNeeded} sol. يرجى إضافة المزيد من sol إلى محفظتك أو تقليل مبلغ sol الذي ترسله", "insufficientFundsForRentError": "ليس لديك ما يكفي من SOL لتغطية رسوم المعاملة والإيجار للحساب. يرجى إضافة المزيد من sol إلى محفظتك أو تقليل مبلغ sol الذي ترسله", + "insufficientFundsForRentErrorReceiver": "لا يحتوي حساب المتلقي على ما يكفي من SOL لتغطية الإيجار. يرجى اطلب من المتلقي إضافة المزيد من SOL إلى حسابه.", "introducing_cake_pay": "نقدم لكم Cake Pay!", "invalid_input": "مدخل غير صالح", "invalid_password": "رمز مرور خاطئ", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index e86a17f95..e952d3728 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Нямате достатъчно SOL, за да покриете транзакцията и таксата му за транзакция. Моля, добавете повече SOL към портфейла си или намалете сумата на SOL, която изпращате.", "insufficient_lamports": "Нямате достатъчно SOL, за да покриете транзакцията и таксата му за транзакция. Имате нужда от поне ${solValueNeeded} sol. Моля, добавете повече SOL към портфейла си или намалете сумата на SOL, която изпращате", "insufficientFundsForRentError": "Нямате достатъчно SOL, за да покриете таксата за транзакцията и наемането на сметката. Моля, добавете повече SOL към портфейла си или намалете сумата на SOL, която изпращате", + "insufficientFundsForRentErrorReceiver": "Сметката на приемника няма достатъчно SOL, за да покрие наема. Моля, помолете приемника да добави още SOL към техния акаунт.", "introducing_cake_pay": "Запознайте се с Cake Pay!", "invalid_input": "Невалиден вход", "invalid_password": "Невалидна парола", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 21a8b2cd4..1b17c4362 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Nemáte dostatek SOL na pokrytí transakce a jejího transakčního poplatku. Laskavě přidejte do své peněženky více solu nebo snižte množství Sol, kterou odesíláte.", "insufficient_lamports": "Nemáte dostatek SOL na pokrytí transakce a jejího transakčního poplatku. Potřebujete alespoň ${solValueNeeded} sol. Laskavě přidejte do své peněženky více SOL nebo snižte množství Sol, kterou odesíláte", "insufficientFundsForRentError": "Nemáte dostatek SOL na pokrytí transakčního poplatku a nájemného za účet. Laskavě přidejte do své peněženky více SOL nebo snižte množství Sol, kterou odesíláte", + "insufficientFundsForRentErrorReceiver": "Účet přijímače nemá dostatek SOL na pokrytí nájemného. Požádejte přijímač, aby na jejich účet přidal další SOL.", "introducing_cake_pay": "Představujeme Cake Pay!", "invalid_input": "Neplatný vstup", "invalid_password": "Neplatné heslo", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 17e75ded0..27a312b5c 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Bitte fügen Sie Ihrer Wallet mehr Sol hinzu oder reduzieren Sie die SOL-Menge, die Sie senden.", "insufficient_lamports": "Sie haben nicht genug SOL, um die Transaktion und ihre Transaktionsgebühr abzudecken. Sie brauchen mindestens ${solValueNeeded} Sol. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol-Betrag", "insufficientFundsForRentError": "Sie haben nicht genug SOL, um die Transaktionsgebühr und die Miete für das Konto zu decken. Bitte fügen Sie mehr Sol zu Ihrer Wallet hinzu oder reduzieren Sie den von Ihnen gesendeten Sol-Betrag", + "insufficientFundsForRentErrorReceiver": "Das Konto des Empfängers hat nicht genug SOL, um die Miete zu decken. Bitte bitten Sie den Empfänger, ihr Konto mehr Sol hinzuzufügen.", "introducing_cake_pay": "Einführung von Cake Pay!", "invalid_input": "Ungültige Eingabe", "invalid_password": "Ungültiges Passwort", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 59533730e..725ab95a3 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "You do not have enough SOL to cover the transaction and its transaction fee. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending.", "insufficient_lamports": "You do not have enough SOL to cover the transaction and its transaction fee. You need at least ${solValueNeeded} SOL. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending", "insufficientFundsForRentError": "You do not have enough SOL to cover the transaction fee and rent for the account. Kindly add more SOL to your wallet or reduce the SOL amount you\\'re sending", + "insufficientFundsForRentErrorReceiver": "The receiver's account does not have enough SOL to cover the rent. Please ask the receiver to add more SOL to their account.", "introducing_cake_pay": "Introducing Cake Pay!", "invalid_input": "Invalid input", "invalid_password": "Invalid password", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 06cda2251..7c1bc280c 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "No tienes suficiente SOL para cubrir la transacción y su tarifa de transacción. Por favor, agrega más SOL a su billetera o reduce la cantidad de sol que está enviando.", "insufficient_lamports": "No tienes suficiente SOL para cubrir la transacción y su tarifa de transacción. Necesita al menos ${solValueNeeded} sol. Por favor, agrega más sol a su billetera o reduzca la cantidad de sol que está enviando", "insufficientFundsForRentError": "No tienes suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agrega más sol a su billetera o reduce la cantidad de sol que está enviando", + "insufficientFundsForRentErrorReceiver": "La cuenta del receptor no tiene suficiente SOL para cubrir el alquiler. Pida al receptor que agregue más SOL a su cuenta.", "introducing_cake_pay": "¡Presentamos Cake Pay!", "invalid_input": "Entrada inválida", "invalid_password": "Contraseña invalida", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 302435dcb..d762c558d 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Vous n'avez pas assez de sol pour couvrir la transaction et ses frais de transaction. Veuillez ajouter plus de Sol à votre portefeuille ou réduire la quantité de Sol que vous envoyez.", "insufficient_lamports": "Vous n'avez pas assez de sol pour couvrir la transaction et ses frais de transaction. Vous avez besoin d'au moins ${solValueNeeded} sol. Veuillez ajouter plus de Sol à votre portefeuille ou réduire la quantité de sol que vous envoyez", "insufficientFundsForRentError": "Vous n'avez pas assez de SOL pour couvrir les frais de transaction et le loyer pour le compte. Veuillez ajouter plus de Sol à votre portefeuille ou réduire la quantité de sol que vous envoyez", + "insufficientFundsForRentErrorReceiver": "Le compte du récepteur n'a pas assez de sol pour couvrir le loyer. Veuillez demander au récepteur d'ajouter plus de Sol à son compte.", "introducing_cake_pay": "Présentation de Cake Pay !", "invalid_input": "Entrée invalide", "invalid_password": "Mot de passe incorrect", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 6a3d2f2f0..ad4db22e5 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Ba ku da isasshen sool don rufe ma'amala da kuɗin ma'amala. Da unara ƙara ƙarin sool a cikin walat ɗinku ko rage adadin Sol ɗin da kuke aikawa.", "insufficient_lamports": "Ba ku da isasshen sool don rufe ma'amala da kuɗin ma'amala. Kuna buƙatar aƙalla ${solValueNeeded} Sol. Da kyau ƙara ƙarin sool zuwa walat ɗinku ko rage adadin Sol ɗin da kuke aikawa", "insufficientFundsForRentError": "Ba ku da isasshen Sol don rufe kuɗin ma'amala da haya don asusun. Da kyau ƙara ƙarin sool zuwa walat ɗinku ko rage adadin Sol ɗin da kuke aikawa", + "insufficientFundsForRentErrorReceiver": "Asusun mai karba bashi da isasshen soya don rufe haya. Da fatan za a nemi mai karba don ƙara ƙarin sol zuwa asusun su.", "introducing_cake_pay": "Gabatar da Cake Pay!", "invalid_input": "Shigar da ba daidai ba", "invalid_password": "Kalmar sirri mara inganci", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index bad13b469..819486206 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "आपके पास लेनदेन और इसके लेनदेन शुल्क को कवर करने के लिए पर्याप्त सोल नहीं है। कृपया अपने बटुए में अधिक सोल जोड़ें या आपके द्वारा भेजे जा रहे सोल राशि को कम करें।", "insufficient_lamports": "आपके पास लेनदेन और इसके लेनदेन शुल्क को कवर करने के लिए पर्याप्त सोल नहीं है। आपको कम से कम ${solValueNeeded} सोल की आवश्यकता है। कृपया अपने बटुए में अधिक सोल जोड़ें या सोल राशि को कम करें जिसे आप भेज रहे हैं", "insufficientFundsForRentError": "आपके पास लेन -देन शुल्क और खाते के लिए किराए को कवर करने के लिए पर्याप्त सोल नहीं है। कृपया अपने बटुए में अधिक सोल जोड़ें या सोल राशि को कम करें जिसे आप भेज रहे हैं", + "insufficientFundsForRentErrorReceiver": "रिसीवर के खाते में किराए को कवर करने के लिए पर्याप्त सोल नहीं है। कृपया रिसीवर को उनके खाते में अधिक सोल जोड़ने के लिए कहें।", "introducing_cake_pay": "परिचय Cake Pay!", "invalid_input": "अमान्य निवेश", "invalid_password": "अवैध पासवर्ड", @@ -569,8 +570,8 @@ "payjoin_unavailable_sheet_title": "Payjoin अनुपलब्ध क्यों है?", "payment_id": "भुगतान ID: ", "payment_made_easy": "भुगतान आसान किया गया", - "payment_was_received": "आपका भुगतान प्राप्त हुआ था।", "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", + "payment_was_received": "आपका भुगतान प्राप्त हुआ था।", "payments": "भुगतान", "pending": " (अपूर्ण)", "percentageOf": "${amount} का", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 286ced787..43025f45c 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Nemate dovoljno SOL -a da pokriva transakciju i njegovu transakcijsku naknadu. Ljubazno dodajte više sol u svoj novčanik ili smanjite količinu SOL -a koju šaljete.", "insufficient_lamports": "Nemate dovoljno SOL -a da pokriva transakciju i njegovu transakcijsku naknadu. Trebate najmanje ${solValueNeeded} sol. Ljubazno dodajte više sol u svoj novčanik ili smanjite količinu SOL -a koju šaljete", "insufficientFundsForRentError": "Nemate dovoljno SOL -a za pokrivanje naknade za transakciju i najamninu za račun. Ljubazno dodajte više sol u svoj novčanik ili smanjite količinu SOL -a koju šaljete", + "insufficientFundsForRentErrorReceiver": "Račun prijemnika nema dovoljno SOL -a da pokriva najamninu. Molimo zamolite prijemnika da doda više SOL -a na svoj račun.", "introducing_cake_pay": "Predstavljamo Cake Pay!", "invalid_input": "Pogrešan unos", "invalid_password": "Netočna zaporka", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index b4d095027..57203b934 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Դուք չունեք բավարար SOL՝ գործարքն և գործարքի վարձը ծածկելու համար։ Խնդրում ենք ավելացնել ավելի շատ SOL ձեր դրամապանակում կամ նվազեցնել ուղարկվող SOL-ի քանակը։", "insufficient_lamports": "Դուք չունեք բավարար SOL՝ գործարքն և գործարքի վարձը ծածկելու համար։ Ձեզ անհրաժեշտ է առնվազն ${solValueNeeded} SOL։ Խնդրում ենք ավելացնել ավելի շատ SOL ձեր դրամապանակում կամ նվազեցնել ուղարկվող SOL-ի քանակը։", "insufficientFundsForRentError": "Ձեր մնացորդը բավարար չէ վարձակալության համար: Խնդրում ենք ավելացնել մնացորդը կամ նվազեցնել ուղարկվող գումարը", + "insufficientFundsForRentErrorReceiver": "Ստացողի հաշիվը չունի բավարար SOL, վարձավճարը ծածկելու համար: Խնդրում ենք ստանալ ստացողին ավելի շատ սոլ ավելացնել իրենց հաշվին:", "introducing_cake_pay": "Ներկայացնում ենք Cake Pay!", "invalid_input": "Սխալ մուտք", "invalid_password": "Սխալ գաղտնաբառ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index d1d699150..b7cf3e84e 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Anda tidak memiliki cukup SOL untuk menutupi transaksi dan biaya transaksinya. Mohon tambahkan lebih banyak sol ke dompet Anda atau kurangi jumlah sol yang Anda kirim.", "insufficient_lamports": "Anda tidak memiliki cukup SOL untuk menutupi transaksi dan biaya transaksinya. Anda membutuhkan setidaknya ${solValueNeeded} sol. Mohon tambahkan lebih banyak sol ke dompet Anda atau kurangi jumlah sol yang Anda kirim", "insufficientFundsForRentError": "Anda tidak memiliki cukup SOL untuk menutupi biaya transaksi dan menyewa untuk akun tersebut. Mohon tambahkan lebih banyak sol ke dompet Anda atau kurangi jumlah sol yang Anda kirim", + "insufficientFundsForRentErrorReceiver": "Akun penerima tidak memiliki cukup SOL untuk menutupi sewa. Silakan minta penerima untuk menambahkan lebih banyak SOL ke akun mereka.", "introducing_cake_pay": "Perkenalkan Cake Pay!", "invalid_input": "Masukan tidak valid", "invalid_password": "Kata sandi salah", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 019885f42..6edfc837a 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Non hai abbastanza SOL per coprire la transazione e la sua quota di transazione. Aggiungi più SOL al tuo portafoglio, o riduci l'importo di SOL che stai inviando.", "insufficient_lamports": "Non hai abbastanza SOL per coprire la transazione e la sua quota di transazione. Hai bisogno di almeno ${solValueNeeded} SOL. Aggiungi più SOL al tuo portafoglio, o riduci l'importo di SOL che stai inviando", "insufficientFundsForRentError": "Non hai abbastanza SOL per coprire la tassa di transazione e l'affitto per il conto. Aggiungi più SOL al tuo portafoglio, o riduci l'importo di SOL che stai inviando", + "insufficientFundsForRentErrorReceiver": "L'account del destinatario non ha abbastanza SOL per coprire l'affitto. Si prega di chiedere al destinatario di aggiungere più SOL al loro account.", "introducing_cake_pay": "Vi presentiamo Cake Pay!", "invalid_input": "Inserimento non valido", "invalid_password": "Password non valida", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index c9021d7da..60b964f6d 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -419,6 +419,7 @@ "insufficient_lamport_for_tx": "トランザクションとその取引手数料をカバーするのに十分なSOLがありません。財布にソルを追加するか、送信するソル量を減らしてください。", "insufficient_lamports": "トランザクションとその取引手数料をカバーするのに十分なSOLがありません。少なくとも${solValueNeeded} solが必要です。財布にソルを追加するか、送信するソル量を減らしてください", "insufficientFundsForRentError": "アカウントの取引料金とレンタルをカバーするのに十分なソルがありません。財布にソルを追加するか、送信するソル量を減らしてください", + "insufficientFundsForRentErrorReceiver": "受信者のアカウントには、家賃をカバーするのに十分なソルがありません。レシーバーにアカウントにソルを追加するように依頼してください。", "introducing_cake_pay": "序章Cake Pay!", "invalid_input": "無効入力", "invalid_password": "無効なパスワード", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index a7ada45e7..31a9c5d0b 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "트랜잭션 및 트랜잭션 수수료를 충당하기에 SOL이 부족합니다. 지갑에 SOL을 더 추가하거나 보내는 SOL 금액을 줄이세요.", "insufficient_lamports": "트랜잭션 및 트랜잭션 수수료를 충당하기에 SOL이 부족합니다. 최소 ${solValueNeeded} SOL이 필요합니다. 지갑에 SOL을 더 추가하거나 보내는 SOL 금액을 줄이세요.", "insufficientFundsForRentError": "계정의 트랜잭션 수수료 및 렌트를 충당하기에 SOL이 부족합니다. 지갑에 SOL을 더 추가하거나 보내는 SOL 금액을 줄이세요.", + "insufficientFundsForRentErrorReceiver": "수신기의 계정에는 임대료를 충당하기에 충분한 SOL이 없습니다. 수신기에게 계정에 더 많은 솔을 추가하도록 요청하십시오.", "introducing_cake_pay": "Cake Pay를 소개합니다!", "invalid_input": "잘못된 입력", "invalid_password": "잘못된 비밀번호", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 47f7acd20..9b0bb0025 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "သငျသညျငွေပေးငွေယူနှင့်၎င်း၏ငွေပေးငွေယူကြေးကိုဖုံးလွှမ်းရန် sol ရှိသည်မဟုတ်ကြဘူး။ ကြင်နာစွာသင်၏ပိုက်ဆံအိတ်သို့ပိုမို sol ကိုထပ်ထည့်ပါသို့မဟုတ်သင်ပို့လွှတ်ခြင်း sol ပမာဏကိုလျှော့ချပါ။", "insufficient_lamports": "သငျသညျငွေပေးငွေယူနှင့်၎င်း၏ငွေပေးငွေယူကြေးကိုဖုံးလွှမ်းရန် sol ရှိသည်မဟုတ်ကြဘူး။ သင်အနည်းဆုံး ${solValueNeeded} s ကိုလိုအပ်ပါတယ်။ ကြင်နာစွာသင်၏ပိုက်ဆံအိတ်သို့ပိုမို sol ကိုထပ်ထည့်ပါသို့မဟုတ်သင်ပို့နေသော sol ပမာဏကိုလျှော့ချပါ", "insufficientFundsForRentError": "သင်ငွေပေးချေမှုအခကြေးငွေကိုဖုံးအုပ်ရန်နှင့်အကောင့်ငှားရန်လုံလောက်သော sol ရှိသည်မဟုတ်ကြဘူး။ ကြင်နာစွာသင်၏ပိုက်ဆံအိတ်သို့ပိုမို sol ကိုပိုမိုထည့်ပါသို့မဟုတ်သင်ပို့ခြင်း sol ပမာဏကိုလျှော့ချပါ", + "insufficientFundsForRentErrorReceiver": "လက်ခံသူ၏အကောင့်တွင်အိမ်ငှားခကိုဖုံးအုပ်ရန်အစွမ်းမရှိနိုင်ပါ။ ကျေးဇူးပြု. လက်ခံသူအားသူတို့၏အကောင့်သို့ထပ်မံထည့်သွင်းရန်တောင်းဆိုပါ။", "introducing_cake_pay": "Cake Pay ကို မိတ်ဆက်ခြင်း။", "invalid_input": "ထည့်သွင်းမှု မမှန်ကန်ပါ။", "invalid_password": "မမှန်ကန်သောစကားဝှက်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 41212aa5e..a58748b4e 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "U hebt niet genoeg SOL om de transactie en de transactiekosten te dekken. Voeg vriendelijk meer SOL toe aan uw portemonnee of verminder de SOL -hoeveelheid die u verzendt.", "insufficient_lamports": "U hebt niet genoeg SOL om de transactie en de transactiekosten te dekken. Je hebt minstens ${solValueNeeded} sol nodig. Voeg vriendelijk meer Sol toe aan uw portemonnee of verminder de SOL -hoeveelheid die u verzendt", "insufficientFundsForRentError": "U hebt niet genoeg SOL om de transactiekosten en huur voor de rekening te dekken. Voeg vriendelijk meer SOL toe aan uw portemonnee of verminder de SOL -hoeveelheid die u verzendt", + "insufficientFundsForRentErrorReceiver": "De account van de ontvanger heeft niet genoeg SOL om de huur te dekken. Vraag de ontvanger om meer SOL aan hun account toe te voegen.", "introducing_cake_pay": "Introductie van Cake Pay!", "invalid_input": "Ongeldige invoer", "invalid_password": "Ongeldig wachtwoord", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 16480285c..9dd895716 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Nie masz wystarczającej ilości SOL, aby pokryć transakcję i opłatę za transakcję. Dodaj więcej SOL do portfela lub zmniejsz wysyłaną kwotę SOL.", "insufficient_lamports": "Nie masz wystarczającej ilości SOL, aby pokryć transakcję i opłatę za transakcję. Potrzebujesz przynajmniej ${solValueNeeded} SOL. Uprzejmie dodaj więcej SOL do portfela lub zmniejsz wysyłaną kwotę SOL, którą wysyłasz", "insufficientFundsForRentError": "Nie masz wystarczającej ilości SOL, aby pokryć opłatę za transakcję i czynsz za konto. Dodaj więcej SOL do portfela lub zmniejsz kwotę, którą wysyłasz", + "insufficientFundsForRentErrorReceiver": "Konto odbiorcy nie ma wystarczającej ilości SOL, aby pokryć czynsz. Poproś odbiorcę o dodanie więcej SOL do ich konta.", "introducing_cake_pay": "Przedstawiamy Cake Pay!", "invalid_input": "Nieprawidłowe dane wejściowe", "invalid_password": "Nieprawidłowe hasło", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 0f2213d2b..fe54b1fd9 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Você não tem Sol suficiente para cobrir a transação e sua taxa de transação. Por favor, adicione mais sol à sua carteira ou reduza a quantidade de sol que você envia.", "insufficient_lamports": "Você não tem Sol suficiente para cobrir a transação e sua taxa de transação. Você precisa de pelo menos ${solValueNeeded} sol. Por favor, adicione mais sol à sua carteira ou reduza a quantidade de sol que você está enviando", "insufficientFundsForRentError": "Você não tem Sol suficiente para cobrir a taxa de transação e o aluguel da conta. Por favor, adicione mais sol à sua carteira ou reduza a quantidade de sol que você envia", + "insufficientFundsForRentErrorReceiver": "A conta do receptor não possui SOL suficiente para cobrir o aluguel. Por favor, peça ao destinatário que adicione mais sol à sua conta.", "introducing_cake_pay": "Apresentando o Cake Pay!", "invalid_input": "Entrada inválida", "invalid_password": "Senha inválida", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index bbbc03164..b1e49867a 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "У вас недостаточно Sol, чтобы покрыть транзакцию и плату за транзакцию. Пожалуйста, добавьте больше Sol в свой кошелек или уменьшите сумму Sol, которую вы отправляете.", "insufficient_lamports": "У вас недостаточно Sol, чтобы покрыть транзакцию и плату за транзакцию. Вам нужен как минимум ${solValueNeeded} sol. Пожалуйста, добавьте больше Sol в свой кошелек или уменьшите сумму Sol, которую вы отправляете", "insufficientFundsForRentError": "У вас недостаточно Sol, чтобы покрыть плату за транзакцию и аренду для счета. Пожалуйста, добавьте больше Sol в свой кошелек или уменьшите сумму Sol, которую вы отправляете", + "insufficientFundsForRentErrorReceiver": "У счета приемника не хватает Sol, чтобы покрыть арендную плату. Пожалуйста, попросите приемника добавить больше SOL в свою учетную запись.", "introducing_cake_pay": "Представляем Cake Pay!", "invalid_input": "Неверный Ввод", "invalid_password": "Неверный пароль", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index c5b540398..b1913d990 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "คุณไม่มีโซลเพียงพอที่จะครอบคลุมการทำธุรกรรมและค่าธรรมเนียมการทำธุรกรรม กรุณาเพิ่มโซลให้มากขึ้นลงในกระเป๋าเงินของคุณหรือลดจำนวนโซลที่คุณส่งมา", "insufficient_lamports": "คุณไม่มีโซลเพียงพอที่จะครอบคลุมการทำธุรกรรมและค่าธรรมเนียมการทำธุรกรรม คุณต้องการอย่างน้อย ${solValueNeeded} SOL กรุณาเพิ่มโซลให้มากขึ้นลงในกระเป๋าเงินของคุณหรือลดจำนวนโซลที่คุณกำลังส่ง", "insufficientFundsForRentError": "คุณไม่มีโซลเพียงพอที่จะครอบคลุมค่าธรรมเนียมการทำธุรกรรมและค่าเช่าสำหรับบัญชี กรุณาเพิ่มโซลให้มากขึ้นลงในกระเป๋าเงินของคุณหรือลดจำนวนโซลที่คุณส่งมา", + "insufficientFundsForRentErrorReceiver": "บัญชีของผู้รับไม่เพียงพอที่จะครอบคลุมค่าเช่า โปรดขอให้ผู้รับเพิ่ม SOL เพิ่มเติมในบัญชีของพวกเขา", "introducing_cake_pay": "ยินดีต้อนรับสู่ Cake Pay!", "invalid_input": "อินพุตไม่ถูกต้อง", "invalid_password": "รหัสผ่านไม่ถูกต้อง", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 23fedcdda..34adc4ad7 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "Wala kang sapat na SOL upang masakop ang transaksyon at ang bayad sa transaksyon nito. Mabuting magdagdag ng higit pa sa iyong pitaka o bawasan ang sol na halaga na iyong ipinapadala.", "insufficient_lamports": "Wala kang sapat na SOL upang masakop ang transaksyon at ang bayad sa transaksyon nito. Kailangan mo ng hindi bababa sa ${solValueNeeded} sol. Mabait na magdagdag ng higit pang sol sa iyong pitaka o bawasan ang dami ng iyong ipinapadala", "insufficientFundsForRentError": "Wala kang sapat na SOL upang masakop ang fee sa transaksyon at upa para sa account. Mabait na magdagdag ng higit pa sa iyong wallet o bawasan ang halaga ng SOL na iyong ipinapadala", + "insufficientFundsForRentErrorReceiver": "Ang account ng tatanggap ay walang sapat na sol upang masakop ang upa. Mangyaring hilingin sa tatanggap na magdagdag ng higit pang SOL sa kanilang account.", "introducing_cake_pay": "Pagpapakilala ng Cake Pay!", "invalid_input": "Di-wastong input", "invalid_password": "Di-wastong password", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 9396a9108..276f0c174 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "İşlemi ve işlem ücretini karşılamak için yeterli SOL'unuz yok. Lütfen cüzdanınıza daha fazla SOL ekleyin veya gönderdiğiniz sol miktarını azaltın.", "insufficient_lamports": "İşlemi ve işlem ücretini karşılamak için yeterli SOL'unuz yok. En az ${solValueNeeded} Sol'a ihtiyacınız var. Lütfen cüzdanınıza daha fazla sol ekleyin veya gönderdiğiniz sol miktarını azaltın", "insufficientFundsForRentError": "İşlem ücretini karşılamak ve hesap için kiralamak için yeterli SOL'nuz yok. Lütfen cüzdanınıza daha fazla sol ekleyin veya gönderdiğiniz sol miktarını azaltın", + "insufficientFundsForRentErrorReceiver": "Alıcının hesabının kirayı karşılamak için yeterli SOL yoktur. Lütfen alıcıdan hesaplarına daha fazla SOL eklemesini isteyin.", "introducing_cake_pay": "Cake Pay ile tanışın!", "invalid_input": "Geçersiz Giriş", "invalid_password": "Geçersiz şifre", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 07e38ff3b..ac9e34fac 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "У вас недостатньо SOL, щоб покрити транзакцію та її плату за трансакцію. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму, яку ви надсилаєте.", "insufficient_lamports": "У вас недостатньо SOL, щоб покрити транзакцію та її плату за трансакцію. Вам потрібно щонайменше ${solValueNeeded} sol. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму Sol, яку ви надсилаєте", "insufficientFundsForRentError": "У вас недостатньо SOL, щоб покрити плату за транзакцію та оренду на рахунок. Будь ласка, додайте до свого гаманця більше SOL або зменшіть суму, яку ви надсилаєте", + "insufficientFundsForRentErrorReceiver": "На рахунку одержувача не вистачає SOL, щоб покрити оренду. Будь ласка, попросіть одержувача додати більше SOL до свого рахунку.", "introducing_cake_pay": "Представляємо Cake Pay!", "invalid_input": "Неправильні дані", "invalid_password": "Недійсний пароль", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 9e4676d13..2a511adf9 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "آپ کے پاس ٹرانزیکشن اور اس کے لین دین کی فیس کا احاطہ کرنے کے لئے کافی SOL نہیں ہے۔ برائے مہربانی اپنے بٹوے میں مزید سول شامل کریں یا آپ کو بھیجنے والی سول رقم کو کم کریں۔", "insufficient_lamports": "آپ کے پاس ٹرانزیکشن اور اس کے لین دین کی فیس کا احاطہ کرنے کے لئے کافی SOL نہیں ہے۔ آپ کو کم از کم ${solValueNeeded} sol کی ضرورت ہے۔ برائے مہربانی اپنے بٹوے میں مزید SOL شامل کریں یا آپ جس SOL رقم کو بھیج رہے ہو اسے کم کریں", "insufficientFundsForRentError": "آپ کے پاس ٹرانزیکشن فیس اور اکاؤنٹ کے لئے کرایہ لینے کے ل enough اتنا SOL نہیں ہے۔ برائے مہربانی اپنے بٹوے میں مزید سول شامل کریں یا آپ کو بھیجنے والی سول رقم کو کم کریں", + "insufficientFundsForRentErrorReceiver": "وصول کنندہ کے اکاؤنٹ میں کرایہ کا احاطہ کرنے کے لئے کافی SOL نہیں ہے۔ براہ کرم وصول کنندہ سے ان کے اکاؤنٹ میں مزید SOL شامل کرنے کو کہیں۔", "introducing_cake_pay": "Cake پے کا تعارف!", "invalid_input": "غلط ان پٹ", "invalid_password": "غلط پاسورڈ", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 157232156..133ddbe30 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -417,6 +417,7 @@ "insufficient_lamport_for_tx": "Bạn không có đủ SOL để thanh toán giao dịch và phí giao dịch. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi.", "insufficient_lamports": "Bạn không có đủ SOL để thanh toán giao dịch và phí giao dịch. Bạn cần ít nhất ${solValueNeeded} SOL. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi", "insufficientFundsForRentError": "Bạn không có đủ SOL để thanh toán phí giao dịch và phí thuê cho tài khoản. Vui lòng thêm SOL vào ví của bạn hoặc giảm số lượng SOL bạn đang gửi", + "insufficientFundsForRentErrorReceiver": "Tài khoản của người nhận không có đủ SOL để trang trải tiền thuê nhà. Vui lòng yêu cầu người nhận thêm SOL vào tài khoản của họ.", "introducing_cake_pay": "Giới thiệu Cake Pay!", "invalid_input": "Nhập không hợp lệ", "invalid_password": "Mật khẩu không hợp lệ", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 2ab9c0a42..1d9f8ef88 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -419,6 +419,7 @@ "insufficient_lamport_for_tx": "O ko ni sosi to lati bo idunadura ati idiyele iṣowo rẹ. Fi agbara kun Sol diẹ sii si apamọwọ rẹ tabi dinku sodo naa ti o \\ 'tun n firanṣẹ.", "insufficient_lamports": "O ko ni sosi to lati bo idunadura ati idiyele iṣowo rẹ. O nilo o kere ju ${solValueNeeded}. Fi agbara kun Sol diẹ sii si apamọwọ rẹ tabi dinku soso ti o n firanṣẹ", "insufficientFundsForRentError": "O ko ni Sol kan lati bo owo isanwo naa ki o yalo fun iroyin naa. Fi agbara kun Sol diẹ sii si apamọwọ rẹ tabi dinku soso naa ti o \\ 'tun n firanṣẹ", + "insufficientFundsForRentErrorReceiver": "Akọọlẹ olugba ko ni Sol lati bo iyalo naa. Jọwọ beere olugba lati ṣafikun Sol diẹ sii si akọọlẹ wọn.", "introducing_cake_pay": "Ẹ bá Cake Pay!", "invalid_input": "Iṣawọle ti ko tọ", "invalid_password": "Ọrọ igbaniwọle ti ko wulo", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index e1b71508f..fd3526e13 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -418,6 +418,7 @@ "insufficient_lamport_for_tx": "您没有足够的溶胶来支付交易及其交易费用。请在您的钱包中添加更多溶胶或减少您发送的溶胶量。", "insufficient_lamports": "您没有足够的溶胶来支付交易及其交易费用。您至少需要${solValueNeeded} sol。请在您的钱包中添加更多溶胶或减少您发送的溶胶量", "insufficientFundsForRentError": "您没有足够的溶胶来支付该帐户的交易费和租金。请在钱包中添加更多溶胶或减少您发送的溶胶量", + "insufficientFundsForRentErrorReceiver": "接收器的帐户没有足够的溶胶来支付租金。请要求接收器向其帐户添加更多SOL。", "introducing_cake_pay": "介绍 Cake Pay!", "invalid_input": "输入无效", "invalid_password": "无效的密码", From a96b493b60b10837903d1bc59699f8db5dd3bcd6 Mon Sep 17 00:00:00 2001 From: cyan Date: Mon, 16 Jun 2025 16:49:43 +0200 Subject: [PATCH 07/29] CW 1080: fix(cw_monero): call store() directly after commiting tx (#2312) * fix(cw_monero): call store() directly after commiting tx to make sure that tx key is written to cache also, store it in TransactionDescription hive box * Update lib/view_model/send/send_view_model.dart --------- Co-authored-by: Omar Hatem --- .../lib/api/structs/pending_transaction.dart | 2 -- cw_monero/lib/api/transaction_history.dart | 16 ++++++++-------- cw_monero/lib/pending_monero_transaction.dart | 2 -- lib/entities/transaction_description.dart | 7 ++++++- lib/monero/cw_monero.dart | 2 +- lib/view_model/send/send_view_model.dart | 9 +++++++++ .../transaction_details_view_model.dart | 8 +++++++- 7 files changed, 31 insertions(+), 15 deletions(-) diff --git a/cw_monero/lib/api/structs/pending_transaction.dart b/cw_monero/lib/api/structs/pending_transaction.dart index dc5fbddd0..22f974f4b 100644 --- a/cw_monero/lib/api/structs/pending_transaction.dart +++ b/cw_monero/lib/api/structs/pending_transaction.dart @@ -5,13 +5,11 @@ class PendingTransactionDescription { required this.fee, required this.hash, required this.hex, - required this.txKey, required this.pointerAddress}); final int amount; final int fee; final String hash; final String hex; - final String txKey; final int pointerAddress; } \ No newline at end of file diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 2fe0888f6..fd6004140 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; @@ -194,14 +195,12 @@ Future createTransactionSync( final rFee = pendingTx.fee(); final rHash = pendingTx.txid(''); final rHex = pendingTx.hex(''); - final rTxKey = rHash; return PendingTransactionDescription( amount: rAmt, fee: rFee, hash: rHash, hex: rHex, - txKey: rTxKey, pointerAddress: pendingTx.ffiAddress(), ); } @@ -246,7 +245,6 @@ Future createTransactionMultDest( fee: tx.fee(), hash: tx.txid(''), hex: tx.hex(''), - txKey: tx.txid(''), pointerAddress: tx.ffiAddress(), ); } @@ -263,6 +261,7 @@ Future commitTransaction({required Wallet2PendingTransaction tx, requir filename: '', overwrite: false, ); + return null; }); String? error = (() { @@ -285,11 +284,12 @@ Future commitTransaction({required Wallet2PendingTransaction tx, requir if (error != null && error != "no tx keys found for this txid") { throw CreationTransactionException(message: error); } - if (useUR) { - return Future.value(txCommit as String?); - } else { - return Future.value(null); - } + unawaited(() async { + storeSync(force: true); + await Future.delayed(Duration(seconds: 5)); + storeSync(force: true); + }()); + return Future.value(txCommit); } class Transaction { diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index f29d1ccd2..9909a3021 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -31,8 +31,6 @@ class PendingMoneroTransaction with PendingTransaction { @override String get hex => pendingTransactionDescription.hex; - String get txKey => pendingTransactionDescription.txKey; - @override String get amountFormatted => AmountConverter.amountIntToString( CryptoCurrency.xmr, pendingTransactionDescription.amount); diff --git a/lib/entities/transaction_description.dart b/lib/entities/transaction_description.dart index 2ac573652..05f64820e 100644 --- a/lib/entities/transaction_description.dart +++ b/lib/entities/transaction_description.dart @@ -5,7 +5,7 @@ part 'transaction_description.g.dart'; @HiveType(typeId: TransactionDescription.typeId) class TransactionDescription extends HiveObject { - TransactionDescription({required this.id, this.recipientAddress, this.transactionNote}); + TransactionDescription({required this.id, this.recipientAddress, this.transactionNote, this.transactionKey}); static const typeId = TRANSACTION_TYPE_ID; static const boxName = 'TransactionDescriptions'; @@ -20,12 +20,16 @@ class TransactionDescription extends HiveObject { @HiveField(2) String? transactionNote; + @HiveField(3) + String? transactionKey; + String get note => transactionNote ?? ''; Map toJson() => { 'id': id, 'recipientAddress': recipientAddress, 'transactionNote': transactionNote, + 'transactionKey': transactionKey, }; factory TransactionDescription.fromJson(Map json) { @@ -33,6 +37,7 @@ class TransactionDescription extends HiveObject { id: json['id'] as String, recipientAddress: json['recipientAddress'] as String?, transactionNote: json['transactionNote'] as String?, + transactionKey: json['transactionKey'] as String?, ); } } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 6ae48b863..257ff895e 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -365,7 +365,7 @@ class CWMonero extends Monero { @override Map pendingTransactionInfo(Object transaction) { final ptx = transaction as PendingMoneroTransaction; - return {'id': ptx.id, 'hex': ptx.hex, 'key': ptx.txKey}; + return {'id': ptx.id, 'hex': ptx.hex}; } @override diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 0a4b7e841..edca5636c 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -590,16 +590,25 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } if (pendingTransaction!.id.isNotEmpty) { + TransactionInfo? tx; + if (walletType == WalletType.monero) { + await Future.delayed(Duration(milliseconds: 450)); + await wallet.fetchTransactions(); + final txhistory = monero!.getTransactionHistory(wallet); + tx = txhistory.transactions.values.last; + } final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}'; _settingsStore.shouldSaveRecipientAddress ? await transactionDescriptionBox.add(TransactionDescription( id: descriptionKey, recipientAddress: address, transactionNote: note, + transactionKey: tx?.additionalInfo["key"] as String?, )) : await transactionDescriptionBox.add(TransactionDescription( id: descriptionKey, transactionNote: note, + transactionKey: tx?.additionalInfo["key"] as String?, )); } final sharedPreferences = await SharedPreferences.getInstance(); diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 067ca73f9..691c94f56 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -233,7 +233,13 @@ abstract class TransactionDetailsViewModelBase with Store { } void _addMoneroListItems(TransactionInfo tx, DateFormat dateFormat) { - final key = tx.additionalInfo['key'] as String?; + final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final description = transactionDescriptionBox.values.firstWhere( + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey)); + + + final key = tx.additionalInfo['key'] as String? ?? description.transactionKey; final accountIndex = tx.additionalInfo['accountIndex'] as int; final addressIndex = tx.additionalInfo['addressIndex'] as int; final feeFormatted = tx.feeFormatted(); From 4fb2fc47ad26f08d2eaeffebd486f9cf4e3ffd41 Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 16 Jun 2025 17:52:15 +0300 Subject: [PATCH 08/29] fix: extra ID for Trocador swap (#2307) --- .../provider/trocador_exchange_provider.dart | 2 ++ .../exchange/exchange_trade_view_model.dart | 24 ++++++++----------- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + 30 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 26a9b2e35..170c53627 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -230,6 +230,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { final providerName = responseJSON['provider'] as String; final amount = responseJSON['amount_from']?.toString(); final receiveAmount = responseJSON['amount_to']?.toString(); + final addressProviderMemo = responseJSON['address_provider_memo'] as String?; return Trade( id: id, @@ -247,6 +248,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { receiveAmount: receiveAmount ?? request.toAmount, payoutAddress: payoutAddress, isSendAll: isSendAll, + extraId: addressProviderMemo, ); } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index fc7a5429b..76a49e1d5 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -96,11 +96,12 @@ abstract class ExchangeTradeViewModelBase with Store { bool isSendable; @computed - String get extraInfo => trade.from == CryptoCurrency.xlm - ? '\n\n' + S.current.xlm_extra_info - : trade.from == CryptoCurrency.xrp - ? '\n\n' + S.current.xrp_extra_info - : ''; + String get extraInfo => switch (trade.from) { + CryptoCurrency.xlm => '\n\n' + S.current.xlm_extra_info, + CryptoCurrency.xrp => '\n\n' + S.current.xrp_extra_info, + CryptoCurrency.ton => '\n\n' + S.current.ton_extra_info, + _ => '' + }; @computed String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled @@ -203,12 +204,12 @@ abstract class ExchangeTradeViewModelBase with Store { ]); if (trade.extraId != null) { - final shouldAddExtraId = trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm; + final shouldAddExtraId = trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm || trade.from == CryptoCurrency.ton; if (shouldAddExtraId) { final title = trade.from == CryptoCurrency.xrp ? S.current.destination_tag - : trade.from == CryptoCurrency.xlm + : trade.from == CryptoCurrency.xlm || trade.from == CryptoCurrency.ton ? S.current.memo : S.current.extra_id; @@ -217,13 +218,8 @@ abstract class ExchangeTradeViewModelBase with Store { title: title, data: '${trade.extraId}', isCopied: true, - isReceiveDetail: (trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm) - ? false - : true, - isExternalSendDetail: - (trade.from == CryptoCurrency.xrp || trade.from == CryptoCurrency.xlm) - ? true - : false, + isReceiveDetail: !shouldAddExtraId, + isExternalSendDetail: shouldAddExtraId ), ); } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 81f7fcb33..0c63a33b4 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -904,6 +904,7 @@ "token_name": "اسم الرمز ، على سبيل المثال: Tether", "token_symbol": "رمز العملة ، على سبيل المثال: USDT", "tokenID": "ﻒﻳﺮﻌﺗ ﺔﻗﺎﻄﺑ", + "ton_extra_info": "يرجى عدم نسيان تحديد معرف المذكرة أثناء إرسال معاملة TON للتبادل", "tor_connection": "ﺭﻮﺗ ﻝﺎﺼﺗﺍ", "tor_only": "Tor فقط", "total": "المجموع", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index e952d3728..8163da646 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -904,6 +904,7 @@ "token_name": "Име на токена, напр.: Tether", "token_symbol": "Символ на токена, напр.: USDT", "tokenID": "документ за самоличност", + "ton_extra_info": "Моля, не забравяйте да посочите идентификационния номер на бележката, докато изпращате транзакцията TON за борсата", "tor_connection": "Tor връзка", "tor_only": "Само чрез Tor", "total": "Обща сума", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 1b17c4362..80b386859 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -904,6 +904,7 @@ "token_name": "Název tokenu např.: Tether", "token_symbol": "Symbol tokenu, např.: USDT", "tokenID": "ID", + "ton_extra_info": "Při odeslání TON transakce pro výměnu nezapomeňte zadat ID Memo ID", "tor_connection": "Připojení Tor", "tor_only": "Pouze Tor", "total": "Celkový", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 27a312b5c..479954b23 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -905,6 +905,7 @@ "token_name": "Token-Name, z. B.: Tether", "token_symbol": "Token-Symbol, z. B.: USDT", "tokenID": "AUSWEIS", + "ton_extra_info": "Bitte vergessen Sie nicht, die Memo -ID anzugeben, während Sie die TON -Transaktion für den Austausch senden", "tor_connection": "Tor-Verbindung", "tor_only": "Nur Tor", "total": "Gesamt", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 725ab95a3..971a68455 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -905,6 +905,7 @@ "token_name": "Token name eg: Tether", "token_symbol": "Token symbol eg: USDT", "tokenID": "ID", + "ton_extra_info": "Please don’t forget to specify the Memo ID while sending the TON transaction for the exchange", "tor_connection": "Tor connection", "tor_only": "Tor only", "total": "Total", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 7c1bc280c..fe802237a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -905,6 +905,7 @@ "token_name": "Nombre del token, por ejemplo: Tether", "token_symbol": "Símbolo de token, por ejemplo: USDT", "tokenID": "IDENTIFICACIÓN", + "ton_extra_info": "No olvide especificar el ID de memo mientras envía la transacción TON para el intercambio", "tor_connection": "conexión tor", "tor_only": "solo Tor", "total": "Total", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d762c558d..b78ef8bc5 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -904,6 +904,7 @@ "token_name": "Nom du token, par exemple : Tether", "token_symbol": "Symbole de token, par exemple : USDT", "tokenID": "IDENTIFIANT", + "ton_extra_info": "N'oubliez pas de spécifier l'identification de la note lors de l'envoi de la transaction TON pour l'échange", "tor_connection": "Connexion Tor", "tor_only": "Tor uniquement", "total": "Total", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index ad4db22e5..710960b7d 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -906,6 +906,7 @@ "token_name": "Alamar sunan misali: Tether", "token_symbol": "Alamar alama misali: USDT", "tokenID": "ID", + "ton_extra_info": "Don Allah kar a manta su saka ID na Memo yayin aikawa da ma'amala don musayar", "tor_connection": "Tor haɗin gwiwa", "tor_only": "Tor kawai", "total": "Duka", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 819486206..f0a7efaaf 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -906,6 +906,7 @@ "token_name": "टोकन नाम जैसे: टीथर", "token_symbol": "टोकन प्रतीक जैसे: यूएसडीटी", "tokenID": "पहचान", + "ton_extra_info": "कृपया एक्सचेंज के लिए टन लेनदेन भेजते समय मेमो आईडी निर्दिष्ट करना न भूलें", "tor_connection": "टोर कनेक्शन", "tor_only": "Tor केवल", "total": "कुल", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 43025f45c..5ecfb5b21 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -904,6 +904,7 @@ "token_name": "Naziv tokena npr.: Tether", "token_symbol": "Simbol tokena npr.: USDT", "tokenID": "iskaznica", + "ton_extra_info": "Ne zaboravite navesti ID memorandu", "tor_connection": "Tor veza", "tor_only": "Samo Tor", "total": "Ukupno", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 57203b934..5d274c632 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -902,6 +902,7 @@ "token_name": "Token-ի անուն, օրինակ՝ Tether", "token_symbol": "Token-ի նշան, օրինակ՝ USDT", "tokenID": "ID", + "ton_extra_info": "Խնդրում ենք մի մոռացեք նշել MEMO ID- ն, երբ փոխանակման համար տոննա գործարքը ուղարկեք", "tor_connection": "Tor կապ", "tor_only": "Միայն Tor", "total": "Ընդհանուր", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index b7cf3e84e..b7847badf 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -907,6 +907,7 @@ "token_name": "Nama token misalnya: Tether", "token_symbol": "Simbol token misalnya: USDT", "tokenID": "PENGENAL", + "ton_extra_info": "Harap jangan lupa untuk menentukan ID memo saat mengirim transaksi ton untuk pertukaran", "tor_connection": "koneksi Tor", "tor_only": "Hanya Tor", "total": "Total", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 6edfc837a..a0ff46c4f 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -905,6 +905,7 @@ "token_name": "Nome del token, ad esempio: Tether", "token_symbol": "Simbolo del token, ad esempio: USDT", "tokenID": "ID", + "ton_extra_info": "Non dimenticare di specificare l'ID memo durante l'invio della transazione Ton per lo scambio", "tor_connection": "Connessione Tor", "tor_only": "Solo Tor", "total": "Totale", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 60b964f6d..ac8dafbac 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -905,6 +905,7 @@ "token_name": "トークン名 例: Tether", "token_symbol": "トークンシンボル 例: USDT", "tokenID": "ID", + "ton_extra_info": "Exchangeのトントランザクションを送信しながら、メモIDを指定することを忘れないでください", "tor_connection": "Tor接続", "tor_only": "Torのみ", "total": "合計", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 31a9c5d0b..e2b21aa04 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -905,6 +905,7 @@ "token_name": "토큰 이름 (예: Tether)", "token_symbol": "토큰 심볼 (예: USDT)", "tokenID": "ID", + "ton_extra_info": "교환을 위해 TON 트랜잭션을 보내는 동안 메모 ID를 지정하는 것을 잊지 마십시오.", "tor_connection": "Tor 연결", "tor_only": "Tor 전용", "total": "합계", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 9b0bb0025..edafed5d2 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -904,6 +904,7 @@ "token_name": "တိုကင်အမည် ဥပမာ- Tether", "token_symbol": "တိုကင်သင်္ကေတ ဥပမာ- USDT", "tokenID": "အမှတ်သညာ", + "ton_extra_info": "ငွေလဲလှယ်မှုအတွက်တန်ပြန်ငွေပေးငွေယူကိုပို့နေစဉ် Memo ID ကိုသတ်မှတ်ရန်မမေ့ပါနှင့်", "tor_connection": "Tor ချိတ်ဆက်မှု", "tor_only": "Tor သာ", "total": "လုံးဝသော", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index a58748b4e..c47160a5f 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -904,6 +904,7 @@ "token_name": "Tokennaam bijv.: Tether", "token_symbol": "Tokensymbool bijv.: USDT", "tokenID": "ID kaart", + "ton_extra_info": "Vergeet niet om de memo -ID op te geven tijdens het verzenden van de ton -transactie voor de uitwisseling", "tor_connection": "Tor-verbinding", "tor_only": "Alleen Tor", "total": "Totaal", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 9dd895716..b3b99e28c 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -904,6 +904,7 @@ "token_name": "Nazwa tokena, np.: Tether", "token_symbol": "Symbol tokena np.: USDT", "tokenID": "ID", + "ton_extra_info": "Nie zapomnij określić identyfikatora notatki podczas wysyłania transakcji TON dla wymiany", "tor_connection": "Połączenie przez Tor", "tor_only": "Tylko sieć Tor", "total": "Całkowity", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index fe54b1fd9..9b55fc7b6 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -906,6 +906,7 @@ "token_name": "Nome do token, por exemplo: Tether", "token_symbol": "Símbolo de token, por exemplo: USDT", "tokenID": "EU IA", + "ton_extra_info": "Não se esqueça de especificar o ID do memorando ao enviar a transação TON para a troca", "tor_connection": "Conexão Tor", "tor_only": "Tor apenas", "total": "Total", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index b1e49867a..3e61f4277 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -905,6 +905,7 @@ "token_name": "Имя токена, например: Tether", "token_symbol": "Символ токена, например: USDT", "tokenID": "ИДЕНТИФИКАТОР", + "ton_extra_info": "Пожалуйста, не забудьте указать идентификатор записки при отправке TON транзакции для обмена", "tor_connection": "Тор соединение", "tor_only": "Только Tor", "total": "Общий", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index b1913d990..a60f1b332 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -904,6 +904,7 @@ "token_name": "ชื่อโทเค็น เช่น Tether", "token_symbol": "สัญลักษณ์โทเค็น เช่น USDT", "tokenID": "บัตรประจำตัวประชาชน", + "ton_extra_info": "โปรดอย่าลืมระบุรหัสบันทึกในขณะที่ส่งธุรกรรม TON สำหรับการแลกเปลี่ยน", "tor_connection": "การเชื่อมต่อทอร์", "tor_only": "Tor เท่านั้น", "total": "ทั้งหมด", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 34adc4ad7..9ed4e13a8 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -904,6 +904,7 @@ "token_name": "Pangalan ng token, halimbawa: Tether", "token_symbol": "Simbolo ng token, halimbawa: USDT", "tokenID": "ID", + "ton_extra_info": "Mangyaring huwag kalimutan na tukuyin ang memo ID habang nagpapadala ng toneladang transaksyon para sa palitan", "tor_connection": "Koneksyon ng Tor", "tor_only": "Tor lamang", "total": "Kabuuan", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 276f0c174..4d790d1c6 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -904,6 +904,7 @@ "token_name": "Belirteç adı, örneğin: Tether", "token_symbol": "Jeton sembolü, örneğin: USDT", "tokenID": "İD", + "ton_extra_info": "Lütfen değişim için ton işlemini gönderirken not kimliğini belirtmeyi unutmayın", "tor_connection": "Tor bağlantısı", "tor_only": "Yalnızca Tor", "total": "Toplam", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ac9e34fac..06b6d09e7 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -905,6 +905,7 @@ "token_name": "Назва токена, наприклад: Tether", "token_symbol": "Символ маркера, наприклад: USDT", "tokenID": "ID", + "ton_extra_info": "Не забудьте вказати ідентифікатор пам’яті під час надсилання транзакції TON для обміну", "tor_connection": "Підключення Tor", "tor_only": "Тільки Tor", "total": "Загальний", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2a511adf9..c83de672b 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -906,6 +906,7 @@ "token_name": "ٹوکن کا نام جیسے: Tether", "token_symbol": "ٹوکن کی علامت جیسے: USDT", "tokenID": "ID", + "ton_extra_info": "ایکسچینج کے لئے ٹن ٹرانزیکشن بھیجتے وقت براہ کرم میمو آئی ڈی کی وضاحت کرنا نہ بھولیں", "tor_connection": "ﻦﺸﮑﻨﮐ ﺭﻮﭨ", "tor_only": "صرف Tor", "total": "کل", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 133ddbe30..14eeee189 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -901,6 +901,7 @@ "token_name": "Tên token ví dụ: Tether", "token_symbol": "Ký hiệu token ví dụ: USDT", "tokenID": "ID", + "ton_extra_info": "Xin đừng quên chỉ định ID ghi nhớ trong khi gửi giao dịch tấn cho trao đổi", "tor_connection": "Kết nối Tor", "tor_only": "Chỉ Tor", "total": "Tổng cộng", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 1d9f8ef88..fee486da4 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -905,6 +905,7 @@ "token_name": "Orukọ àmi fun apẹẹrẹ: Tether", "token_symbol": "Aami aami fun apẹẹrẹ: USDT", "tokenID": "ID", + "ton_extra_info": "Jọwọ maṣe gbagbe lati tokasi ID akọsilẹ lakoko fifiranṣẹ idunadura pupọ fun paṣipaarọ naa", "tor_connection": "Tor asopọ", "tor_only": "Tor nìkan", "total": "Apapọ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index fd3526e13..ffa8509c4 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -904,6 +904,7 @@ "token_name": "代币名称例如:Tether", "token_symbol": "代币符号例如:USDT", "tokenID": "ID", + "ton_extra_info": "请不要忘记在发送TON交易时指定备忘录", "tor_connection": "Tor连接", "tor_only": "仅限 Tor", "total": "全部的", From 85d3e727e20203b411e3225396f6ca00d280f18c Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 17 Jun 2025 00:31:49 +0200 Subject: [PATCH 09/29] CW-1092-restoring-from-backup-doesnt-maintain-hardware-wallets (#2319) * feat: add hardware wallet verification during backup restoration * style: improve readability of verifyHardwareWallets in backup_service_v3.dart --- lib/core/backup_service.dart | 10 +++++---- lib/core/backup_service_v3.dart | 37 ++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 2e8523024..87bb71ce9 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -110,7 +110,7 @@ class $BackupService { } Future verifyWallets() async { - final walletInfoSource = await _reloadHiveWalletInfoBox(); + final walletInfoSource = await reloadHiveWalletInfoBox(); correctWallets = walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList(); @@ -119,7 +119,7 @@ class $BackupService { } } - Future> _reloadHiveWalletInfoBox() async { + Future> reloadHiveWalletInfoBox() async { final appDir = await getAppDir(); await CakeHive.close(); CakeHive.init(appDir.path); @@ -288,13 +288,15 @@ class $BackupService { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': await keyService.getWalletPassword(walletName: walletInfo.name) + 'password': await keyService.getWalletPassword(walletName: walletInfo.name), + 'hardwareWalletType': walletInfo.hardwareWalletType?.index, }; } catch (e) { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': '' + 'password': '', + 'hardwareWalletType': walletInfo.hardwareWalletType?.index, }; } })); diff --git a/lib/core/backup_service_v3.dart b/lib/core/backup_service_v3.dart index be678b679..a0640dfd3 100644 --- a/lib/core/backup_service_v3.dart +++ b/lib/core/backup_service_v3.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/utils/package_info.dart'; import 'package:crypto/crypto.dart'; import 'package:cw_core/root_dir.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info.dart'; import 'package:flutter/foundation.dart'; enum BackupVersion { @@ -305,6 +306,7 @@ class BackupServiceV3 extends $BackupService { // Continue importing the backup the old way await super.verifyWallets(); + await verifyHardwareWallets(password); await super.importKeychainDumpV2(password); await super.importPreferencesDump(); await super.importTransactionDescriptionDump(); @@ -313,6 +315,39 @@ class BackupServiceV3 extends $BackupService { decryptedData.deleteSync(); } + Future verifyHardwareWallets(String password, + {String keychainSalt = secrets.backupKeychainSalt}) async { + final walletInfoSource = await reloadHiveWalletInfoBox(); + final appDir = await getAppDir(); + final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); + final decryptedKeychainDumpFileData = await decryptV2( + keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); + final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) + as Map; + final keychainWalletsInfo = keychainJSON['wallets'] as List; + + final expectedHardwareWallets = keychainWalletsInfo + .where((e) => + (e as Map).containsKey("hardwareWalletType") && + e["hardwareWalletType"] != null) + .toList(); + + for (final expectedHardwareWallet in expectedHardwareWallets) { + final info = expectedHardwareWallet as Map; + final actualWalletInfo = walletInfoSource.values + .where((e) => + e.name == info['name'] && e.type.toString() == info['type']) + .firstOrNull; + if (actualWalletInfo != null && + info["hardwareWalletType"] != + actualWalletInfo.hardwareWalletType?.index) { + actualWalletInfo.hardwareWalletType = + HardwareWalletType.values[info["hardwareWalletType"] as int]; + await actualWalletInfo.save(); + } + } + } + Future exportBackupFileV3(String password, {String nonce = secrets.backupSalt}) async { final metadata = BackupMetadata( version: BackupVersion.v3, @@ -467,4 +502,4 @@ This backup was created on ${DateTime.now().toIso8601String()} file.writeAsBytesSync(data); return file; } -} \ No newline at end of file +} From 17d99e5451d33408859570a80d7c8e502d0d296b Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 17 Jun 2025 00:32:11 +0200 Subject: [PATCH 10/29] feat(ui): add app logo to qr code (#2072) fix(ui): use high instead of low error correction for QR codes --- assets/images/qr-cake.png | Bin 0 -> 73464 bytes lib/src/screens/receive/widgets/qr_image.dart | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 assets/images/qr-cake.png diff --git a/assets/images/qr-cake.png b/assets/images/qr-cake.png new file mode 100644 index 0000000000000000000000000000000000000000..7c54dedb01811abcfc586284b25288ec3414e854 GIT binary patch literal 73464 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 E0S!xubpQYW literal 0 HcmV?d00001 diff --git a/lib/src/screens/receive/widgets/qr_image.dart b/lib/src/screens/receive/widgets/qr_image.dart index 043958ef4..51c0c7431 100644 --- a/lib/src/screens/receive/widgets/qr_image.dart +++ b/lib/src/screens/receive/widgets/qr_image.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:flutter/material.dart'; import 'package:qr_flutter/qr_flutter.dart' as qr; @@ -8,7 +9,7 @@ class QrImage extends StatelessWidget { this.backgroundColor = Colors.white, this.size = 100.0, this.version, - this.errorCorrectionLevel = qr.QrErrorCorrectLevel.L, + this.errorCorrectionLevel = qr.QrErrorCorrectLevel.H, }); final double size; @@ -28,6 +29,7 @@ class QrImage extends StatelessWidget { foregroundColor: foregroundColor, backgroundColor: backgroundColor, padding: const EdgeInsets.all(8.0), + embeddedImage: AssetImage('assets/images/qr-cake.png'), ); } } From 65402ba1eb88c379e19f34f6316897e08df6941f Mon Sep 17 00:00:00 2001 From: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com> Date: Tue, 17 Jun 2025 07:37:49 +0900 Subject: [PATCH 11/29] dcr: Always fetch the current dir path. (#2242) * dcr: Always fetch the current dir path. On ios devices the path will change between updates breaking decred. Never save the path and always check to ensure it is up to date. Previous wallets were also not creating a directory in the correct place. Move those when found. * Update cw_decred/lib/wallet_service.dart * dcr: Update libwallet dep. --------- Co-authored-by: Omar Hatem --- cw_decred/lib/wallet.dart | 15 +++-- cw_decred/lib/wallet_service.dart | 99 +++++++++++++++++++++++++------ scripts/android/build_decred.sh | 2 +- scripts/ios/build_decred.sh | 2 +- scripts/macos/build_decred.sh | 2 +- 5 files changed, 92 insertions(+), 28 deletions(-) diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index ac70a4aaa..97aee775d 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -5,6 +5,8 @@ import 'package:path/path.dart' as p; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cw_decred/amount_format.dart'; import 'package:cw_decred/pending_transaction.dart'; import 'package:cw_decred/transaction_credentials.dart'; @@ -307,9 +309,10 @@ abstract class DecredWalletBase persistantPeer = addr; await _libwallet.closeWallet(walletInfo.name); final network = isTestnet ? "testnet" : "mainnet"; + final dirPath = await pathForWalletDir(name: walletInfo.name, type: WalletType.decred); final config = { "name": walletInfo.name, - "datadir": walletInfo.dirPath, + "datadir": dirPath, "net": network, "unsyncedaddrs": true, }; @@ -605,22 +608,22 @@ abstract class DecredWalletBase final sourceDir = Directory(currentDirPath); final targetDir = Directory(newDirPath); - + if (!targetDir.existsSync()) { await targetDir.create(recursive: true); } - + await for (final entity in sourceDir.list(recursive: true)) { - final relativePath = entity.path.substring(sourceDir.path.length+1); + final relativePath = entity.path.substring(sourceDir.path.length + 1); final targetPath = p.join(targetDir.path, relativePath); - + if (entity is File) { await entity.rename(targetPath); } else if (entity is Directory) { await Directory(targetPath).create(recursive: true); } } - + await sourceDir.delete(recursive: true); } diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index e2313904e..93c708886 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -8,6 +8,7 @@ import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:path/path.dart'; import 'package:hive/hive.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -57,42 +58,93 @@ class DecredWalletService extends WalletService< @override Future create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async { await this.init(); + final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); + final network = isTestnet == true ? testnet : mainnet; final config = { "name": credentials.walletInfo!.name, - "datadir": credentials.walletInfo!.dirPath, + "datadir": dirPath, "pass": credentials.password!, - "net": isTestnet == true ? testnet : mainnet, + "net": network, "unsyncedaddrs": true, }; await libwallet!.createWallet(jsonEncode(config)); final di = DerivationInfo( derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); credentials.walletInfo!.derivationInfo = di; + credentials.walletInfo!.network = network; + // ios will move our wallet directory when updating. Since we must + // recalculate the new path every time we open the wallet, ensure this path + // is not used. An older wallet will have a directory here which is a + // condition for moving the wallet when opening, so this must be kept blank + // going forward. + credentials.walletInfo!.dirPath = ""; + credentials.walletInfo!.path = ""; final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); await wallet.init(); return wallet; } + void copyDirectorySync(Directory source, Directory destination) { + /// create destination folder if not exist + if (!destination.existsSync()) { + destination.createSync(recursive: true); + } + + /// get all files from source (recursive: false is important here) + source.listSync(recursive: false).forEach((entity) { + final newPath = destination.path + Platform.pathSeparator + basename(entity.path); + if (entity is File) { + entity.rename(newPath); + } else if (entity is Directory) { + copyDirectorySync(entity, Directory(newPath)); + } + }); + } + + Future moveWallet(String fromPath, String toPath) async { + final oldWalletDir = new Directory(fromPath); + final newWalletDir = new Directory(toPath); + copyDirectorySync(oldWalletDir, newWalletDir); + // It would be ideal to delete the old directory here, but ios will error + // sometimes with "OS Error: No such file or directory, errno = 2" even + // after checking if it exists. + } + @override Future openWallet(String name, String password) async { final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; - final network = walletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || - walletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet - ? testnet - : mainnet; + if (walletInfo.network == null || walletInfo.network == "") { + walletInfo.network = walletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || + walletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; + } await this.init(); - final walletDirExists = Directory(walletInfo.dirPath).existsSync(); - if (!walletDirExists) { - walletInfo.dirPath = await pathForWalletDir(name: name, type: getType()); + + // Cake wallet version 4.27.0 and earlier gave a wallet dir that did not + // match the name. Move those to the correct place. + final dirPath = await pathForWalletDir(name: name, type: getType()); + if (walletInfo.path != "") { + // On ios the stored dir no longer exists. We can only trust the basename. + // dirPath may already be updated and lost the basename, so look at path. + final randomBasename = basename(walletInfo.path); + final oldDir = await pathForWalletDir(name: randomBasename, type: getType()); + if (oldDir != dirPath) { + await this.moveWallet(oldDir, dirPath); + } + // Clear the path so this does not trigger again. + walletInfo.dirPath = ""; + walletInfo.path = ""; + await walletInfo.save(); } final config = { - "name": walletInfo.name, - "datadir": walletInfo.dirPath, - "net": network, + "name": name, + "datadir": dirPath, + "net": walletInfo.network, "unsyncedaddrs": true, }; await libwallet!.loadWallet(jsonEncode(config)); @@ -127,12 +179,11 @@ class DecredWalletService extends WalletService< await currentWallet.renameWalletFiles(newName); - final newDirPath = await pathForWalletDir(name: newName, type: getType()); final newWalletInfo = currentWalletInfo; newWalletInfo.id = WalletBase.idFor(newName, getType()); newWalletInfo.name = newName; - newWalletInfo.dirPath = newDirPath; - newWalletInfo.network = network; + newWalletInfo.dirPath = ""; + newWalletInfo.path = ""; await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } @@ -141,18 +192,23 @@ class DecredWalletService extends WalletService< Future restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { await this.init(); + final network = isTestnet == true ? testnet : mainnet; + final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); final config = { "name": credentials.walletInfo!.name, - "datadir": credentials.walletInfo!.dirPath, + "datadir": dirPath, "pass": credentials.password!, "mnemonic": credentials.mnemonic, - "net": isTestnet == true ? testnet : mainnet, + "net": network, "unsyncedaddrs": true, }; await libwallet!.createWallet(jsonEncode(config)); final di = DerivationInfo( derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); credentials.walletInfo!.derivationInfo = di; + credentials.walletInfo!.network = network; + credentials.walletInfo!.dirPath = ""; + credentials.walletInfo!.path = ""; final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); await wallet.init(); @@ -165,17 +221,22 @@ class DecredWalletService extends WalletService< Future restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials, {bool? isTestnet}) async { await this.init(); + final network = isTestnet == true ? testnet : mainnet; + final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); final config = { "name": credentials.walletInfo!.name, - "datadir": credentials.walletInfo!.dirPath, + "datadir": dirPath, "pubkey": credentials.pubkey, - "net": isTestnet == true ? testnet : mainnet, + "net": network, "unsyncedaddrs": true, }; await libwallet!.createWatchOnlyWallet(jsonEncode(config)); final di = DerivationInfo( derivationPath: isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath); credentials.walletInfo!.derivationInfo = di; + credentials.walletInfo!.network = network; + credentials.walletInfo!.dirPath = ""; + credentials.walletInfo!.path = ""; final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); await wallet.init(); diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh index 75ed45aca..ce37a7353 100755 --- a/scripts/android/build_decred.sh +++ b/scripts/android/build_decred.sh @@ -7,7 +7,7 @@ cd "$(dirname "$0")" CW_DECRED_DIR=$(realpath ../..)/cw_decred LIBWALLET_PATH="${PWD}/decred/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="dba5327d35cb5d5d1ff113b780869deee154511f" +LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH || true diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh index 6860c7776..37384f4e1 100755 --- a/scripts/ios/build_decred.sh +++ b/scripts/ios/build_decred.sh @@ -3,7 +3,7 @@ set -e . ./config.sh LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="dba5327d35cb5d5d1ff113b780869deee154511f" +LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH diff --git a/scripts/macos/build_decred.sh b/scripts/macos/build_decred.sh index e7e5d492f..b1bbfbc43 100755 --- a/scripts/macos/build_decred.sh +++ b/scripts/macos/build_decred.sh @@ -4,7 +4,7 @@ LIBWALLET_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="dba5327d35cb5d5d1ff113b780869deee154511f" +LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" echo "======================= DECRED LIBWALLET =========================" From c6cb48096dbbf6af04a32c67ebe72a8d4f9d5ab8 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:24:18 +0100 Subject: [PATCH 12/29] refactor: Update CakeImageWidget to allow nullable BoxFit (#2326) --- lib/src/screens/welcome/welcome_page.dart | 1 - lib/src/widgets/cake_image_widget.dart | 12 +++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index a78d826d7..023bafbe2 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -1,6 +1,5 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; diff --git a/lib/src/widgets/cake_image_widget.dart b/lib/src/widgets/cake_image_widget.dart index ca48a2671..b24c7f3d3 100644 --- a/lib/src/widgets/cake_image_widget.dart +++ b/lib/src/widgets/cake_image_widget.dart @@ -7,7 +7,7 @@ class CakeImageWidget extends StatelessWidget { this.imageUrl, this.height, this.width, - this.fit = BoxFit.cover, + this.fit, this.loadingWidget, this.errorWidget, this.color, @@ -16,7 +16,7 @@ class CakeImageWidget extends StatelessWidget { final String? imageUrl; final double? height; final double? width; - final BoxFit fit; + final BoxFit? fit; final Widget? loadingWidget; final Widget? errorWidget; final Color? color; @@ -37,8 +37,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, - fit: fit, - color: color, + fit: fit ?? BoxFit.contain, ) : Image.asset( imageUrl!, @@ -53,8 +52,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, - fit: fit, - color: color, + fit: fit ?? BoxFit.contain, placeholderBuilder: (_) { return loadingWidget ?? const Center(child: CircularProgressIndicator()); }, @@ -64,7 +62,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, - fit: fit, + fit: fit ?? BoxFit.cover, color: color, loadingBuilder: (_, Widget child, ImageChunkEvent? progress) { if (progress == null) return child; From 150becb679b88173cc60187dfdb1b31d2a850b83 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 18 Jun 2025 16:20:03 +0200 Subject: [PATCH 13/29] CW-1073 Implement Monero wallet definition URI scheme (#2323) * feat: add optional parameter to customize address extraction pattern * refactor: add parameter to control address extraction surrounding whitespace validation * fix: ensure proper handling of unmounted context in address extraction logic * test: add comprehensive unit tests for AddressResolver and AddressValidator classes --- lib/core/address_validator.dart | 10 +- lib/entities/parse_address_from_domain.dart | 10 +- .../widgets/extract_address_from_parsed.dart | 41 ++-- .../restore/wallet_restore_from_qr_code.dart | 5 +- test/core/address_validator_test.dart | 178 +++++++++++++++++ .../parse_address_from_domain_test.dart | 187 ++++++++++++++++++ 6 files changed, 401 insertions(+), 30 deletions(-) create mode 100644 test/core/address_validator_test.dart create mode 100644 test/entities/parse_address_from_domain_test.dart diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 911e939d1..c79ac0980 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -38,9 +38,9 @@ class AddressValidator extends TextValidator { '|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}'; case CryptoCurrency.btc: pattern = - '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${RegExp(r'(bc|tb)1q[ac-hj-np-z02-9]{25,39}}').pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; + '${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}'; case CryptoCurrency.ltc: - pattern = '^${RegExp(r'ltc1q[ac-hj-np-z02-9]{25,39}').pattern}\$|^${MwebAddress.regex.pattern}\$'; + pattern = '${P2wpkhAddress.regex.pattern}|${MwebAddress.regex.pattern}'; case CryptoCurrency.nano: pattern = '[0-9a-zA-Z_]+'; case CryptoCurrency.banano: @@ -335,10 +335,6 @@ class AddressValidator extends TextValidator { } } - if (pattern != null) { - return "$BEFORE_REGEX($pattern)$AFTER_REGEX"; - } - - return null; + return pattern != null ? "($pattern)" : null; } } diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 3fbbe0709..01e6322cc 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -165,13 +165,19 @@ class AddressResolver { "zone" ]; - static String? extractAddressByType({required String raw, required CryptoCurrency type}) { - final addressPattern = AddressValidator.getAddressFromStringPattern(type); + static String? extractAddressByType( + {required String raw, + required CryptoCurrency type, + bool requireSurroundingWhitespaces = true}) { + var addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern == null) { throw Exception('Unexpected token: $type for getAddressFromStringPattern'); } + if (requireSurroundingWhitespaces) + addressPattern = "$BEFORE_REGEX$addressPattern$AFTER_REGEX"; + final match = RegExp(addressPattern, multiLine: true).firstMatch(raw); return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'), (Match match) { diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index 29c4eb793..b37f87b7f 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -8,6 +8,8 @@ import 'choose_yat_address_alert.dart'; Future extractAddressFromParsed( BuildContext context, ParsedAddress parsedAddress) async { + if (!context.mounted) return parsedAddress.addresses.first; + var title = ''; var content = ''; var address = ''; @@ -95,16 +97,17 @@ Future extractAddressFromParsed( content += S.of(context).choose_address; address = await showPopUp( - context: context, - builder: (BuildContext context) { - - return WillPopScope( + context: context, + builder: (context) => PopScope( child: ChooseYatAddressAlert( alertTitle: title, alertContent: content, - addresses: parsedAddress.addresses), - onWillPop: () async => false); - }) ?? ''; + addresses: parsedAddress.addresses, + ), + canPop: false, + ), + ) ?? + ''; if (address.isEmpty) { return parsedAddress.name; @@ -113,22 +116,20 @@ Future extractAddressFromParsed( return address; case ParseFrom.contact: case ParseFrom.notParsed: - address = parsedAddress.addresses.first; - return address; + return parsedAddress.addresses.first; } await showPopUp( - context: context, - builder: (BuildContext context) { - - return AlertWithOneAction( - alertTitle: title, - headerTitleText: profileName.isEmpty ? null : profileName, - headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl, - alertContent: content, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: title, + headerTitleText: profileName.isEmpty ? null : profileName, + headerImageProfileUrl: profileImageUrl.isEmpty ? null : profileImageUrl, + alertContent: content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ), + ); return address; } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index c8ff81acc..530ba1725 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -74,7 +74,10 @@ class WalletRestoreFromQRCode { static String? _extractAddressFromUrl(String rawString, WalletType type) { try { return AddressResolver.extractAddressByType( - raw: rawString, type: walletTypeToCryptoCurrency(type)); + raw: rawString, + type: walletTypeToCryptoCurrency(type), + requireSurroundingWhitespaces: false, + ); } catch (_) { return null; } diff --git a/test/core/address_validator_test.dart b/test/core/address_validator_test.dart new file mode 100644 index 000000000..08dd5a9e2 --- /dev/null +++ b/test/core/address_validator_test.dart @@ -0,0 +1,178 @@ +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AddressValidator', () { + setUpAll(() { + S.current = S(); + }); + group('getPattern', () { + test('returns correct pattern for Bitcoin', () { + final pattern = AddressValidator.getPattern(CryptoCurrency.btc); + expect(pattern, isNotEmpty); + expect(pattern, contains('(bc|tb)1q')); + }); + + test('returns correct pattern for Ethereum', () { + final pattern = AddressValidator.getPattern(CryptoCurrency.eth); + expect(pattern, isNotEmpty); + expect(pattern, contains('0x[0-9a-zA-Z]+')); + }); + + test('returns correct pattern for Monero', () { + final pattern = AddressValidator.getPattern(CryptoCurrency.xmr); + expect(pattern, isNotEmpty); + expect(pattern, + contains('4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}')); + }); + + test('returns correct pattern for Litecoin', () { + final pattern = AddressValidator.getPattern(CryptoCurrency.ltc); + expect(pattern, isNotEmpty); + expect( + pattern, + contains( + '(bc|tb|ltc)1q[ac-hj-np-z02-9]{25,39}|(ltc|t)mweb1q[ac-hj-np-z02-9]{90,120}')); + }); + + test('returns empty string for unknown currency', () { + final pattern = AddressValidator.getPattern(CryptoCurrency.btcln); + expect(pattern, isNotEmpty); + }); + }); + + group('getLength', () { + test('returns correct length for Bitcoin', () { + final length = AddressValidator.getLength(CryptoCurrency.btc); + expect(length, isNull); + }); + + test('returns correct length for Ethereum', () { + final length = AddressValidator.getLength(CryptoCurrency.eth); + expect(length, equals([42])); + }); + + test('returns correct length for Monero', () { + final length = AddressValidator.getLength(CryptoCurrency.xmr); + expect(length, isNull); + }); + + test('returns correct length for Dash', () { + final length = AddressValidator.getLength(CryptoCurrency.dash); + expect(length, equals([34])); + }); + }); + + group('getAddressFromStringPattern', () { + test('returns correct pattern for Bitcoin', () { + final pattern = + AddressValidator.getAddressFromStringPattern(CryptoCurrency.btc); + expect(pattern, isNotNull); + expect(pattern, contains('(bc|tb)1q')); + }); + + test('returns correct pattern for Ethereum', () { + final pattern = + AddressValidator.getAddressFromStringPattern(CryptoCurrency.eth); + expect(pattern, isNotNull); + expect(pattern, contains('0x[0-9a-zA-Z]+')); + }); + + test('returns correct pattern for Monero', () { + final pattern = + AddressValidator.getAddressFromStringPattern(CryptoCurrency.xmr); + expect(pattern, isNotNull); + expect(pattern, contains('(4[0-9a-zA-Z]{94})')); + }); + + test('returns null for unsupported currency', () { + final pattern = + AddressValidator.getAddressFromStringPattern(CryptoCurrency.dash); + expect(pattern, isNull); + }); + }); + // 0.000058158099999999995 BTC + group('validation', () { + test('validates valid Bitcoin address', () { + final validator = AddressValidator(type: CryptoCurrency.btc); + expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'), + isTrue); + expect(validator.isValid('3AD1Btx1MzYGmdpNpeujCfuvU5SsU2LX88'), isTrue); + expect(validator.isValid('1HARAhFcvz8ZQp5MhnLFeUynC4bkha3Hv8'), isTrue); + }); + + test('rejects invalid Bitcoin address', () { + final validator = AddressValidator(type: CryptoCurrency.btc); + expect(validator.isValid('invalid_address'), isFalse); + expect(validator.isValid('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043CakeWallet'), + isFalse); + }); + + test('validates valid Ethereum address', () { + final validator = AddressValidator(type: CryptoCurrency.eth); + expect(validator.isValid('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), + isTrue); // WETH contract + }); + + test('rejects invalid Ethereum address', () { + final validator = AddressValidator(type: CryptoCurrency.eth); + expect(validator.isValid('invalid_address'), isFalse); + expect(validator.isValid('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc'), + isFalse); // Too short + expect(validator.isValid('C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'), + isFalse); // Missing 0x prefix + }); + + test('validates valid Monero address', () { + final validator = AddressValidator(type: CryptoCurrency.xmr); + expect( + validator.isValid( + '85s6zfxGAkdCN21h566R8EFDSfThxCrFiEkhw3JEtaXN2DDfahABLXTjRj385Ro7om5saGWJG7iuE6EyW5MYcoz93DLvNqh'), + isTrue); + }); + + test('rejects invalid Monero address', () { + final validator = AddressValidator(type: CryptoCurrency.xmr); + expect(validator.isValid('invalid_address'), isFalse); + expect( + validator.isValid( + '85s6zfxGAkdCN21h566R8EFDSfThxCrFiEkhw3JEtaXN2DDfahABLXTjRj385Ro7om5saGWJG7iuE6EyW5MYcoz93DLvNq'), + isFalse); // Too short + }); + + test('validates valid Litecoin address', () { + final validator = AddressValidator(type: CryptoCurrency.ltc); + expect(validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusx9jstf8qw8'), + isTrue); + expect( + validator.isValid( + 'ltcmweb1qqt9hqch2d0vfdsvt4tf27gullem2tcd57xxrvta9xwvfmwdkn4927q6d8sq6ftw7lkqdkr5g36eqn7w06edgq8tz7gy0nv5d4lhajctkzuath23a'), + isTrue); + }); + + test('rejects invalid Litecoin address', () { + final validator = AddressValidator(type: CryptoCurrency.ltc); + expect(validator.isValid('invalid_address'), isFalse); + expect( + validator.isValid('ltc1qzvxlvlk8wsmue0np20eh3d3qxsusxCakeWallet'), + isFalse); + }); + }); + + group('silentPaymentAddressPattern', () { + test('returns a non-empty pattern', () { + final pattern = AddressValidator.silentPaymentAddressPattern; + expect(pattern, isNotEmpty); + }); + }); + + group('mWebAddressPattern', () { + test('returns a non-empty pattern', () { + final pattern = AddressValidator.mWebAddressPattern; + expect(pattern, isNotEmpty); + }); + }); + }); +} diff --git a/test/entities/parse_address_from_domain_test.dart b/test/entities/parse_address_from_domain_test.dart new file mode 100644 index 000000000..4ed7c8b5f --- /dev/null +++ b/test/entities/parse_address_from_domain_test.dart @@ -0,0 +1,187 @@ +import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AddressResolver', () { + // late MockYatService mockYatService; + // late MockWalletBase mockWallet; + // late MockSettingsStore mockSettingsStore; + // late MockBuildContext mockContext; + // late AddressResolver addressResolver; + // + // setUp(() { + // mockYatService = MockYatService(); + // mockWallet = MockWalletBase(); + // mockSettingsStore = MockSettingsStore(); + // mockContext = MockBuildContext(); + // + // when(mockWallet.type).thenReturn(WalletType.bitcoin); + // when(mockWallet.currency).thenReturn(CryptoCurrency.btc); + // + // addressResolver = AddressResolver( + // yatService: mockYatService, + // wallet: mockWallet, + // settingsStore: mockSettingsStore, + // ); + // }); + + group('extractAddressByType', () { + test('extracts Bitcoin address correctly', () { + final raw = + 'My Bitcoin address is bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq please use it'; + final result = AddressResolver.extractAddressByType( + raw: raw, + type: CryptoCurrency.btc, + ); + expect(result, 'bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'); + }); + + test('extracts Ethereum address correctly', () { + final raw = + 'Send ETH to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 thanks'; + final result = AddressResolver.extractAddressByType( + raw: raw, + type: CryptoCurrency.eth, + ); + expect(result, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); + }); + + test('extracts Monero address correctly', () { + final raw = + 'XMR: 85s6zfxGAkdCN21h566R8EFDSfThxCrFiEkhw3JEtaXN2DDfahABLXTjRj385Ro7om5saGWJG7iuE6EyW5MYcoz93DLvNqh'; + final result = AddressResolver.extractAddressByType( + raw: raw, + type: CryptoCurrency.xmr, + ); + expect(result, + '85s6zfxGAkdCN21h566R8EFDSfThxCrFiEkhw3JEtaXN2DDfahABLXTjRj385Ro7om5saGWJG7iuE6EyW5MYcoz93DLvNqh'); + }); + + test('extracts Bitcoin Cash address correctly', () { + final raw = + 'BCH: bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj'; + final result = AddressResolver.extractAddressByType( + raw: raw, + type: CryptoCurrency.bch, + ); + expect( + result, 'bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj'); + }); + + test('extracts Nano address correctly', () { + final raw = + 'NANO: nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd'; + final result = AddressResolver.extractAddressByType( + raw: raw, + type: CryptoCurrency.nano, + ); + expect(result, + 'nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd'); + }); + + test('returns null for unsupported currency', () { + final raw = 'Some text without an address'; + expect( + () => AddressResolver.extractAddressByType( + raw: raw, + type: CryptoCurrency.btc, + ), + returnsNormally); + + final result = AddressResolver.extractAddressByType( + raw: raw, + type: CryptoCurrency.btc, + ); + expect(result, isNull); + }); + + test('extracts monero address from URI', () { + final raw = + 'monero_wallet:467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz?spend_key=029c559cd7669f14e91fd835144916009f8697ab5ac5c7f7c06e1ff869c17b0b&view_key=afaf646edbff3d3bcee8efd3383ffe5d20c947040f74e1110b70ca0fbb0ef90d'; + final result = AddressResolver.extractAddressByType( + raw: raw, + type: CryptoCurrency.xmr, + requireSurroundingWhitespaces: false); + expect(result, + '467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz'); + }); + + test('extracts monero address from Tweet', () { + final raw = ''' +#XMR +89bH6i3ftaWSWuPJJYSQuuApWJ8xzinCEbbnAXN1Z3mGGUuAFdpBUg82R9MvJDSheJ6kW2dyMQEFUGM4tsZqRb2Q75UXqvc + +#BTC Silent Payments +sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucwj5k7nenpw2r86wf42xv6wqdvxuk5rggrul45 + +#LTC MWEB +ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc + '''; + final resultXmr = AddressResolver.extractAddressByType( + raw: raw, type: CryptoCurrency.xmr); + expect(resultXmr, + '89bH6i3ftaWSWuPJJYSQuuApWJ8xzinCEbbnAXN1Z3mGGUuAFdpBUg82R9MvJDSheJ6kW2dyMQEFUGM4tsZqRb2Q75UXqvc'); + final resultBtc = AddressResolver.extractAddressByType( + raw: raw, type: CryptoCurrency.btc); + expect(resultBtc, + 'sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucwj5k7nenpw2r86wf42xv6wqdvxuk5rggrul45'); + final resultLtc = AddressResolver.extractAddressByType( + raw: raw, type: CryptoCurrency.ltc); + expect(resultLtc, + 'ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc'); + }); + + // test('throws exception for unexpected token', () { + // // Create a custom crypto currency that won't have a pattern + // final customCurrency = CryptoCurrency('CUSTOM', 'Custom'); + // expect(() => AddressResolver.extractAddressByType( + // raw: 'Some text', + // type: customCurrency, + // ), throwsException); + // }); + }); + // + // group('isEmailFormat', () { + // test('returns true for valid email format', () { + // expect(addressResolver.isEmailFormat('user@example.com'), isTrue); + // expect(addressResolver.isEmailFormat('name.surname@domain.co.uk'), isTrue); + // expect(addressResolver.isEmailFormat('user123@subdomain.example.org'), isTrue); + // }); + // + // test('returns false for invalid email format', () { + // expect(addressResolver.isEmailFormat('user@'), isFalse); + // expect(addressResolver.isEmailFormat('@domain.com'), isFalse); + // expect(addressResolver.isEmailFormat('user@domain'), isFalse); + // expect(addressResolver.isEmailFormat('user.domain.com'), isFalse); + // expect(addressResolver.isEmailFormat('user@domain@com'), isFalse); + // expect(addressResolver.isEmailFormat('bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'), isFalse); + // }); + // }); + // + // group('resolve', () { + // test('returns ParsedAddress with original text when no resolution is possible', () async { + // final text = 'bc1qhg4l43pmq5v5atmtlr7gnwyuxs043cvrut5hkq'; + // final result = await addressResolver.resolve(mockContext, text, CryptoCurrency.btc); + // + // expect(result, isA()); + // expect(result.addresses, [text]); + // }); + // + // // Note: More comprehensive tests for the resolve method would require + // // mocking all the external services and APIs that the method calls. + // // This would be quite extensive and would require setting up mock + // // responses for each type of address resolution. + // }); + + group('unstoppableDomains', () { + test('contains expected TLDs', () { + expect(AddressResolver.unstoppableDomains, contains('crypto')); + expect(AddressResolver.unstoppableDomains, contains('eth')); + expect(AddressResolver.unstoppableDomains, contains('bitcoin')); + expect(AddressResolver.unstoppableDomains, contains('x')); + expect(AddressResolver.unstoppableDomains, contains('wallet')); + }); + }); + }); +} From edaf48599350adf7c0c00589801ab842ac0bbffa Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 19 Jun 2025 04:37:41 +0200 Subject: [PATCH 14/29] CW-1069-implement-deuro-dapp-support (#2304) * feat: started dEuro Savings integration * fix: merge conflict regarding theming * feat: Add dEuro Savings Screen * feat: Change DEuro Savings UI * feat: Complete DEuro Savings integration with UI enhancements and transaction support * style: remove forgotten print statements * feat: localize dEuro subtitle * feat: add approval flow and priority handling to DEuro Savings integration - Introduced approval flow for DEuro Savings to enable token approvals. - Added priority handling for deposit and withdrawal operations. - Updated UI to support approval state and interactions. - Localized new strings for multiple languages. - Enhanced transaction handling with separate approval and commit actions. * feat: add support for ERC20 token approval transactions - Introduced `signApprovalTransaction` and `createApprovalTransaction` methods. - Added handling for infinite approvals. - Implemented encoding for approval transaction data. - Enhanced transaction creation flow with approval-specific functionality. * Update UI * feat: enhance DEuro Savings logic and UI with computed property and fix gradient background * feat: localize transaction confirmation content for DEuro Savings * feat: enable interest collection for DEuro Savings with localized support * fix reformatting [skip ci] --------- Co-authored-by: tuxsudo Co-authored-by: OmarHatem --- cw_ethereum/lib/deuro/deuro_savings.dart | 121 ++++ .../lib/deuro/deuro_savings_contract.dart | 543 ++++++++++++++++++ cw_ethereum/pubspec.yaml | 2 +- cw_evm/lib/evm_chain_client.dart | 64 ++- cw_evm/lib/evm_chain_wallet.dart | 40 ++ cw_evm/lib/pending_evm_chain_transaction.dart | 3 + lib/di.dart | 8 +- lib/ethereum/cw_ethereum.dart | 87 ++- lib/polygon/cw_polygon.dart | 61 +- lib/router.dart | 6 + lib/routes.dart | 2 + .../dashboard/pages/cake_features_page.dart | 22 +- .../integrations/deuro/savings_page.dart | 197 +++++++ .../widgets/edit_savings_bottom_sheet.dart | 45 ++ .../deuro/widgets/interest_card_widget.dart | 67 +++ .../integrations/deuro/widgets/numpad.dart | 107 ++++ .../deuro/widgets/savings_card_widget.dart | 263 +++++++++ .../deuro/widgets/savings_edit_sheet.dart | 90 +++ lib/src/widgets/dashboard_card_widget.dart | 1 + .../integrations/deuro_view_model.dart | 119 ++++ res/values/strings_ar.arb | 10 + res/values/strings_bg.arb | 10 + res/values/strings_cs.arb | 10 + res/values/strings_de.arb | 14 +- res/values/strings_en.arb | 10 + res/values/strings_es.arb | 10 + res/values/strings_fr.arb | 10 + res/values/strings_ha.arb | 10 + res/values/strings_hi.arb | 10 + res/values/strings_hr.arb | 10 + res/values/strings_hy.arb | 10 + res/values/strings_id.arb | 10 + res/values/strings_it.arb | 10 + res/values/strings_ja.arb | 10 + res/values/strings_ko.arb | 10 + res/values/strings_my.arb | 10 + res/values/strings_nl.arb | 10 + res/values/strings_pl.arb | 10 + res/values/strings_pt.arb | 10 + res/values/strings_ru.arb | 10 + res/values/strings_th.arb | 10 + res/values/strings_tl.arb | 10 + res/values/strings_tr.arb | 10 + res/values/strings_uk.arb | 10 + res/values/strings_ur.arb | 10 + res/values/strings_vi.arb | 10 + res/values/strings_yo.arb | 10 + res/values/strings_zh.arb | 10 + tool/configure.dart | 15 + 49 files changed, 2081 insertions(+), 66 deletions(-) create mode 100644 cw_ethereum/lib/deuro/deuro_savings.dart create mode 100644 cw_ethereum/lib/deuro/deuro_savings_contract.dart create mode 100644 lib/src/screens/integrations/deuro/savings_page.dart create mode 100644 lib/src/screens/integrations/deuro/widgets/edit_savings_bottom_sheet.dart create mode 100644 lib/src/screens/integrations/deuro/widgets/interest_card_widget.dart create mode 100644 lib/src/screens/integrations/deuro/widgets/numpad.dart create mode 100644 lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart create mode 100644 lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart create mode 100644 lib/view_model/integrations/deuro_view_model.dart diff --git a/cw_ethereum/lib/deuro/deuro_savings.dart b/cw_ethereum/lib/deuro/deuro_savings.dart new file mode 100644 index 000000000..4c37f944c --- /dev/null +++ b/cw_ethereum/lib/deuro/deuro_savings.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_ethereum/deuro/deuro_savings_contract.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_evm/contract/erc20.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/pending_evm_chain_transaction.dart'; +import 'package:web3dart/web3dart.dart'; + +const String savingsGatewayAddress = + "0x073493d73258C4BEb6542e8dd3e1b2891C972303"; + +const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea"; + +class DEuro { + final SavingsGateway _savingsGateway; + final ERC20 _dEuro; + final EthereumWallet _wallet; + + DEuro(EthereumWallet wallet) + : _wallet = wallet, + _savingsGateway = _getSavingsGateway(wallet.getWeb3Client()!), + _dEuro = _getDEuroToken(wallet.getWeb3Client()!); + + static SavingsGateway _getSavingsGateway(Web3Client client) => SavingsGateway( + address: EthereumAddress.fromHex(savingsGatewayAddress), + client: client, + ); + + static ERC20 _getDEuroToken(Web3Client client) => ERC20( + address: EthereumAddress.fromHex(dEuroAddress), + client: client, + ); + + final frontendCode = + Uint8List.fromList(sha256.convert(utf8.encode("wallet")).bytes); + + EthereumAddress get _address => + EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress); + + Future get savingsBalance async => + (await _savingsGateway.savings(accountOwner: _address)).saved; + + Future get accruedInterest => + _savingsGateway.accruedInterest(accountOwner: _address); + + Future get interestRate => _savingsGateway.currentRatePPM(); + + Future get approvedBalance => + _dEuro.allowance(_address, _savingsGateway.self.address); + + Future depositSavings( + BigInt amount, EVMChainTransactionPriority priority) async { + final signedTransaction = await _savingsGateway.save( + (amount: amount, frontendCode: frontendCode), + credentials: _wallet.evmChainPrivateKey, + ); + + final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: amount, + contractAddress: _savingsGateway.self.address.hexEip55, + receivingAddressHex: _savingsGateway.self.address.hexEip55, + priority: priority, + data: _savingsGateway.self.abi.functions[17] + .encodeCall([amount, frontendCode]), + ); + + final sendTransaction = + () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + + return PendingEVMChainTransaction( + sendTransaction: sendTransaction, + signedTransaction: signedTransaction, + fee: BigInt.from(fee.estimatedGasFee), + amount: amount.toString(), + exponent: 18); + } + + Future withdrawSavings( + BigInt amount, EVMChainTransactionPriority priority) async { + final signedTransaction = await _savingsGateway.withdraw( + (target: _address, amount: amount, frontendCode: frontendCode), + credentials: _wallet.evmChainPrivateKey, + ); + + final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: amount, + contractAddress: _savingsGateway.self.address.hexEip55, + receivingAddressHex: _savingsGateway.self.address.hexEip55, + priority: priority, + data: _savingsGateway.self.abi.functions[17] + .encodeCall([amount, frontendCode]), + ); + + final sendTransaction = + () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + + return PendingEVMChainTransaction( + sendTransaction: sendTransaction, + signedTransaction: signedTransaction, + fee: BigInt.from(fee.estimatedGasFee), + amount: amount.toString(), + exponent: 18); + } + + // Set an infinite approval to save gas in the future + Future enableSavings( + EVMChainTransactionPriority priority) async => + (await _wallet.createApprovalTransaction( + BigInt.parse( + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + radix: 16, + ), + _savingsGateway.self.address.hexEip55, + CryptoCurrency.deuro, + priority, + )) as PendingEVMChainTransaction; +} diff --git a/cw_ethereum/lib/deuro/deuro_savings_contract.dart b/cw_ethereum/lib/deuro/deuro_savings_contract.dart new file mode 100644 index 000000000..ca2eb5dcc --- /dev/null +++ b/cw_ethereum/lib/deuro/deuro_savings_contract.dart @@ -0,0 +1,543 @@ +// ignore_for_file: type=lint +// ignore_for_file: unused_local_variable, unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:web3dart/web3dart.dart' as _i1; +import 'dart:typed_data' as _i2; + +final _contractAbi = _i1.ContractAbi.fromJson( + '[{"inputs":[{"internalType":"contract IDecentralizedEURO","name":"deuro_","type":"address"},{"internalType":"uint24","name":"initialRatePPM","type":"uint24"},{"internalType":"address","name":"gateway_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ChangeNotReady","type":"error"},{"inputs":[],"name":"ModuleDisabled","type":"error"},{"inputs":[],"name":"NoPendingChange","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"interest","type":"uint256"}],"name":"InterestCollected","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint24","name":"newRate","type":"uint24"}],"name":"RateChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"who","type":"address"},{"indexed":false,"internalType":"uint24","name":"nextRate","type":"uint24"},{"indexed":false,"internalType":"uint40","name":"nextChange","type":"uint40"}],"name":"RateProposed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Saved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint192","name":"amount","type":"uint192"}],"name":"Withdrawn","type":"event"},{"inputs":[],"name":"GATEWAY","outputs":[{"internalType":"contract IFrontendGateway","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"accountOwner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"accruedInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"targetAmount","type":"uint192"}],"name":"adjust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"applyChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"internalType":"struct Savings.Account","name":"account","type":"tuple"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"name":"calculateInterest","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"currentTicks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"deuro","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"equity","outputs":[{"internalType":"contract IReserve","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextChange","outputs":[{"internalType":"uint40","name":"","type":"uint40"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextRatePPM","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint24","name":"newRatePPM_","type":"uint24"},{"internalType":"address[]","name":"helpers","type":"address[]"}],"name":"proposeChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"refreshBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"refreshMyBalance","outputs":[{"internalType":"uint192","name":"","type":"uint192"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"save","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"savings","outputs":[{"internalType":"uint192","name":"saved","type":"uint192"},{"internalType":"uint64","name":"ticks","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ticks","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint192","name":"amount","type":"uint192"},{"internalType":"bytes32","name":"frontendCode","type":"bytes32"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}]', + 'SavingsGateway', +); + +class SavingsGateway extends _i1.GeneratedContract { + SavingsGateway({ + required _i1.EthereumAddress address, + required _i1.Web3Client client, + int? chainId, + }) : super( + _i1.DeployedContract( + _contractAbi, + address, + ), + client, + chainId, + ); + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> GATEWAY({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[1]; + assert(checkSignature(function, '338c5371')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future accruedInterest({ + required _i1.EthereumAddress accountOwner, + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[2]; + assert(checkSignature(function, '77267ec3')); + final params = [accountOwner]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future accruedInterest$2( + ({_i1.EthereumAddress accountOwner, BigInt timestamp}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[3]; + assert(checkSignature(function, 'a696399d')); + final params = [ + args.accountOwner, + args.timestamp, + ]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future adjust( + ({BigInt targetAmount, _i2.Uint8List frontendCode}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[4]; + assert(checkSignature(function, '753ef93c')); + final params = [ + args.targetAmount, + args.frontendCode, + ]; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future calculateInterest( + ({dynamic account, BigInt ticks}) args, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[7]; + assert(checkSignature(function, '7915ce20')); + final params = [ + args.account, + args.ticks, + ]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future currentRatePPM({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[8]; + assert(checkSignature(function, '06a7b376')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future currentTicks({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[9]; + assert(checkSignature(function, 'b079f163')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> deuro({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[10]; + assert(checkSignature(function, '82b8eaf5')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future<_i1.EthereumAddress> equity({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[11]; + assert(checkSignature(function, '91a0ac6a')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i1.EthereumAddress); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future nextChange({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[12]; + assert(checkSignature(function, 'b6f83c17')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future nextRatePPM({_i1.BlockNum? atBlock}) async { + final function = self.abi.functions[13]; + assert(checkSignature(function, '2e4b20ab')); + final params = []; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as BigInt); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future refreshBalance( + ({_i1.EthereumAddress owner}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[15]; + assert(checkSignature(function, 'b77cd1c7')); + final params = [args.owner]; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future refreshMyBalance({ + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[16]; + assert(checkSignature(function, '85bd12d1')); + final params = []; + return write( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> save( + ({BigInt amount, _i2.Uint8List frontendCode}) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[17]; + assert(checkSignature(function, '9e2363dc')); + final params = [ + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> saveTo( + ({ + _i1.EthereumAddress owner, + BigInt amount, + _i2.Uint8List frontendCode + }) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[19]; + assert(checkSignature(function, 'cbcf9676')); + final params = [ + args.owner, + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// The optional [atBlock] parameter can be used to view historical data. When + /// set, the function will be evaluated in the specified block. By default, the + /// latest on-chain block will be used. + Future savings({ + required _i1.EthereumAddress accountOwner, + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[21]; + assert(checkSignature(function, '1f7cdd5f')); + final params = [accountOwner]; + final response = await read( + function, + params, + atBlock, + ); + return Savings(response); + } + + /// The optional [transaction] parameter can be used to override parameters + /// like the gas price, nonce and max gas. The `data` and `to` fields will be + /// set by the contract. + Future<_i2.Uint8List> withdraw( + ({ + _i1.EthereumAddress target, + BigInt amount, + _i2.Uint8List frontendCode + }) args, { + required _i1.Credentials credentials, + _i1.Transaction? transaction, + }) async { + final function = self.abi.functions[24]; + assert(checkSignature(function, '829a0476')); + final params = [ + args.target, + args.amount, + args.frontendCode, + ]; + return writeRaw( + credentials, + transaction, + function, + params, + ); + } + + /// Returns a live stream of all InterestCollected events emitted by this contract. + Stream interestCollectedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('InterestCollected'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return InterestCollected( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all RateChanged events emitted by this contract. + Stream rateChangedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('RateChanged'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return RateChanged( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all RateProposed events emitted by this contract. + Stream rateProposedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('RateProposed'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return RateProposed( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all Saved events emitted by this contract. + Stream savedEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('Saved'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return Saved( + decoded, + result, + ); + }); + } + + /// Returns a live stream of all Withdrawn events emitted by this contract. + Stream withdrawnEvents({ + _i1.BlockNum? fromBlock, + _i1.BlockNum? toBlock, + }) { + final event = self.event('Withdrawn'); + final filter = _i1.FilterOptions.events( + contract: self, + event: event, + fromBlock: fromBlock, + toBlock: toBlock, + ); + return client.events(filter).map((_i1.FilterEvent result) { + final decoded = event.decodeResults( + result.topics!, + result.data!, + ); + return Withdrawn( + decoded, + result, + ); + }); + } +} + +class Savings { + Savings(List response) + : saved = (response[0] as BigInt), + ticks = (response[1] as BigInt); + + final BigInt saved; + + final BigInt ticks; +} + +class InterestCollected { + InterestCollected( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + interest = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt interest; + + final _i1.FilterEvent event; +} + +class RateChanged { + RateChanged( + List response, + this.event, + ) : newRate = (response[0] as BigInt); + + final BigInt newRate; + + final _i1.FilterEvent event; +} + +class RateProposed { + RateProposed( + List response, + this.event, + ) : who = (response[0] as _i1.EthereumAddress), + nextRate = (response[1] as BigInt), + nextChange = (response[2] as BigInt); + + final _i1.EthereumAddress who; + + final BigInt nextRate; + + final BigInt nextChange; + + final _i1.FilterEvent event; +} + +class Saved { + Saved( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + amount = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt amount; + + final _i1.FilterEvent event; +} + +class Withdrawn { + Withdrawn( + List response, + this.event, + ) : account = (response[0] as _i1.EthereumAddress), + amount = (response[1] as BigInt); + + final _i1.EthereumAddress account; + + final BigInt amount; + + final _i1.FilterEvent event; +} diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml index c68f78fc9..1da1e3b3b 100644 --- a/cw_ethereum/pubspec.yaml +++ b/cw_ethereum/pubspec.yaml @@ -6,7 +6,7 @@ author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: '>=2.18.2 <3.0.0' + sdk: ^3.5.0 flutter: ">=1.17.0" dependencies: diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index 7e6caf374..8b45d5544 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -76,7 +76,7 @@ abstract class EVMChainClient { Future getGasUnitPrice() async { try { final gasPrice = await _client!.getGasPrice(); - + return gasPrice.getInWei.toInt(); } catch (_) { return 0; @@ -101,6 +101,7 @@ abstract class EVMChainClient { String? contractAddress, EtherAmount? gasPrice, EtherAmount? maxFeePerGas, + Uint8List? data, }) async { try { if (contractAddress == null) { @@ -124,7 +125,7 @@ abstract class EVMChainClient { final gasEstimate = await _client!.estimateGas( sender: senderAddress, to: EthereumAddress.fromHex(contractAddress), - data: transfer.encodeCall([ + data: data ?? transfer.encodeCall([ toAddress, value.getInWei, ]), @@ -137,6 +138,21 @@ abstract class EVMChainClient { } } + Uint8List getEncodedDataForApprovalTransaction({ + required EthereumAddress toAddress, + required EtherAmount value, + required EthereumAddress contractAddress, + }) { + final contract = DeployedContract(ethereumContractAbi, contractAddress); + + final approve = contract.function('approve'); + + return approve.encodeCall([ + toAddress, + value.getInWei, + ]); + } + Future signTransaction({ required Credentials privateKey, required String toAddress, @@ -198,6 +214,50 @@ abstract class EVMChainClient { ); } + Future signApprovalTransaction({ + required Credentials privateKey, + required String spender, + required BigInt amount, + required BigInt gasFee, + required int estimatedGasUnits, + required int maxFeePerGas, + required EVMChainTransactionPriority priority, + required int exponent, + required String contractAddress, + }) async { + + final Transaction transaction = createTransaction( + from: privateKey.address, + to: EthereumAddress.fromHex(contractAddress), + maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + amount: EtherAmount.zero(), + maxGas: estimatedGasUnits, + maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + ); + + final erc20 = ERC20( + client: _client!, + address: EthereumAddress.fromHex(contractAddress), + chainId: chainId, + ); + + final signedTransaction = await erc20.approve( + EthereumAddress.fromHex(spender), + amount, + credentials: privateKey, + transaction: transaction, + ); + + return PendingEVMChainTransaction( + signedTransaction: prepareSignedTransactionForSending(signedTransaction), + amount: amount.toString(), + fee: gasFee, + sendTransaction: () => sendTransaction(signedTransaction), + exponent: exponent, + isInfiniteApproval: amount.toRadixString(16) == 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + ); + } + Transaction createTransaction({ required EthereumAddress from, required EthereumAddress to, diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index a1b253dd8..18f6e6ee5 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'dart:typed_data'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; @@ -255,6 +256,7 @@ abstract class EVMChainWalletBase required String? contractAddress, required String receivingAddressHex, required TransactionPriority priority, + Uint8List? data, }) async { try { if (priority is EVMChainTransactionPriority) { @@ -276,6 +278,7 @@ abstract class EVMChainWalletBase gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), toAddress: EthereumAddress.fromHex(receivingAddressHex), maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + data: data, ); final totalGasFee = estimatedGas * maxFeePerGas; @@ -478,6 +481,43 @@ abstract class EVMChainWalletBase return pendingEVMChainTransaction; } + Future createApprovalTransaction( + BigInt amount, + String spender, + CryptoCurrency token, + EVMChainTransactionPriority priority) async { + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element.title == token.title); + assert(transactionCurrency is Erc20Token); + + final data = _client.getEncodedDataForApprovalTransaction( + contractAddress: EthereumAddress.fromHex( + (transactionCurrency as Erc20Token).contractAddress), + value: EtherAmount.fromBigInt(EtherUnit.wei, amount), + toAddress: EthereumAddress.fromHex(spender), + ); + + final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( + amount: amount, + receivingAddressHex: spender, + priority: priority, + contractAddress: transactionCurrency.contractAddress, + data: data, + ); + + return _client.signApprovalTransaction( + privateKey: _evmChainPrivateKey, + spender: spender, + amount: amount, + priority: priority, + gasFee: BigInt.from(gasFeesModel.estimatedGasFee), + maxFeePerGas: gasFeesModel.maxFeePerGas, + estimatedGasUnits: gasFeesModel.estimatedGasUnits, + exponent: transactionCurrency.decimal, + contractAddress: transactionCurrency.contractAddress, + ); + } + Future _updateTransactions() async { try { if (_isTransactionUpdating) { diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index 6861b41f8..61b406470 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -11,6 +11,7 @@ class PendingEVMChainTransaction with PendingTransaction { final BigInt fee; final String amount; final int exponent; + final bool isInfiniteApproval; PendingEVMChainTransaction({ required this.sendTransaction, @@ -18,10 +19,12 @@ class PendingEVMChainTransaction with PendingTransaction { required this.fee, required this.amount, required this.exponent, + this.isInfiniteApproval = false, }); @override String get amountFormatted { + if (isInfiniteApproval) return "∞"; final _amount = (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString(); return _amount.substring(0, min(10, _amount.length)); } diff --git a/lib/di.dart b/lib/di.dart index 1d925150f..46c453d61 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -35,6 +35,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart'; import 'package:cake_wallet/src/screens/settings/background_sync_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart'; @@ -43,6 +44,7 @@ import 'package:cake_wallet/themes/core/theme_store.dart'; import 'package:cake_wallet/view_model/dev/monero_background_sync.dart'; import 'package:cake_wallet/view_model/dev/secure_preferences.dart'; import 'package:cake_wallet/view_model/dev/shared_preferences.dart'; +import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; @@ -1510,6 +1512,10 @@ Future setup({ getIt.registerFactory(() => BackgroundSyncLogsViewModel()); getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get())); - + + getIt.registerFactory(() => DEuroViewModel(getIt())); + + getIt.registerFactory(() => DEuroSavingsPage(getIt())); + _isSetupFinished = true; } diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 40c7a0f77..3e3675d1f 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -67,8 +67,7 @@ class CWEthereum extends Ethereum { @override String getPublicKey(WalletBase wallet) { final privateKeyInUnitInt = (wallet as EthereumWallet).evmChainPrivateKey; - final publicKey = privateKeyInUnitInt.address.hex; - return publicKey; + return privateKeyInUnitInt.address.hex; } @override @@ -138,29 +137,24 @@ class CWEthereum extends Ethereum { } @override - List getERC20Currencies(WalletBase wallet) { - final ethereumWallet = wallet as EthereumWallet; - return ethereumWallet.erc20Currencies; - } + List getERC20Currencies(WalletBase wallet) => + (wallet as EthereumWallet).erc20Currencies; @override - Future addErc20Token(WalletBase wallet, CryptoCurrency token) async { - await (wallet as EthereumWallet).addErc20Token(token as Erc20Token); - } + Future addErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as EthereumWallet).addErc20Token(token as Erc20Token); @override - Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) async => - await (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token); @override - Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) async => - await (wallet as EthereumWallet).removeTokenTransactionsInHistory(token as Erc20Token); + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) => + (wallet as EthereumWallet).removeTokenTransactionsInHistory(token as Erc20Token); @override - Future getErc20Token(WalletBase wallet, String contractAddress) async { - final ethereumWallet = wallet as EthereumWallet; - return await ethereumWallet.getErc20Token(contractAddress, 'eth'); - } + Future getErc20Token(WalletBase wallet, String contractAddress) => + (wallet as EthereumWallet).getErc20Token(contractAddress, 'eth'); @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { @@ -177,23 +171,19 @@ class CWEthereum extends Ethereum { } @override - void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) { - (wallet as EthereumWallet).updateScanProviderUsageState(isEnabled); - } + void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) => + (wallet as EthereumWallet).updateScanProviderUsageState(isEnabled); @override - Web3Client? getWeb3Client(WalletBase wallet) { - return (wallet as EthereumWallet).getWeb3Client(); - } + Web3Client? getWeb3Client(WalletBase wallet) => (wallet as EthereumWallet).getWeb3Client(); + @override String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - void setLedgerConnection( - WalletBase wallet, ledger.LedgerConnection connection) { + void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection) { ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) - .setLedgerConnection( - connection, wallet.walletInfo.derivationInfo?.derivationPath); + .setLedgerConnection(connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override @@ -209,7 +199,44 @@ class CWEthereum extends Ethereum { } @override - List getDefaultTokenContractAddresses() { - return DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); - } + List getDefaultTokenContractAddresses() => + DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); + + Future createTokenApproval(WalletBase wallet, BigInt amount, String spender, + CryptoCurrency token, TransactionPriority priority) => + (wallet as EVMChainWallet).createApprovalTransaction( + amount, spender, token, priority as EVMChainTransactionPriority); + + // Integrations + @override + Future getDEuroSavingsBalance(WalletBase wallet) => + DEuro(wallet as EthereumWallet).savingsBalance; + + @override + Future getDEuroAccruedInterest(WalletBase wallet) => + DEuro(wallet as EthereumWallet).accruedInterest; + + @override + Future getDEuroInterestRate(WalletBase wallet) => + DEuro(wallet as EthereumWallet).interestRate; + + @override + Future getDEuroSavingsApproved(WalletBase wallet) => + DEuro(wallet as EthereumWallet).approvedBalance; + + @override + Future addDEuroSaving( + WalletBase wallet, BigInt amount, TransactionPriority priority) => + DEuro(wallet as EthereumWallet) + .depositSavings(amount, priority as EVMChainTransactionPriority); + + @override + Future removeDEuroSaving( + WalletBase wallet, BigInt amount, TransactionPriority priority) => + DEuro(wallet as EthereumWallet) + .withdrawSavings(amount, priority as EVMChainTransactionPriority); + + @override + Future enableDEuroSaving(WalletBase wallet, TransactionPriority priority) => + DEuro(wallet as EthereumWallet).enableSavings(priority as EVMChainTransactionPriority); } diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index ec98137c5..557cedc39 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -67,8 +67,7 @@ class CWPolygon extends Polygon { @override String getPublicKey(WalletBase wallet) { final privateKeyInUnitInt = (wallet as PolygonWallet).evmChainPrivateKey; - final publicKey = privateKeyInUnitInt.address.hex; - return publicKey; + return privateKeyInUnitInt.address.hex; } @override @@ -137,28 +136,27 @@ class CWPolygon extends Polygon { } @override - List getERC20Currencies(WalletBase wallet) { - final polygonWallet = wallet as PolygonWallet; - return polygonWallet.erc20Currencies; - } + List getERC20Currencies(WalletBase wallet) => + (wallet as PolygonWallet).erc20Currencies; @override - Future addErc20Token(WalletBase wallet, CryptoCurrency token) async => - await (wallet as PolygonWallet).addErc20Token(token as Erc20Token); + Future addErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as PolygonWallet).addErc20Token(token as Erc20Token); @override - Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) async => - await (wallet as PolygonWallet).deleteErc20Token(token as Erc20Token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as PolygonWallet).deleteErc20Token(token as Erc20Token); @override - Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) async => - await (wallet as PolygonWallet).removeTokenTransactionsInHistory(token as Erc20Token); + Future removeTokenTransactionsInHistory( + WalletBase wallet, CryptoCurrency token) => + (wallet as PolygonWallet) + .removeTokenTransactionsInHistory(token as Erc20Token); @override - Future getErc20Token(WalletBase wallet, String contractAddress) async { - final polygonWallet = wallet as PolygonWallet; - return await polygonWallet.getErc20Token(contractAddress, 'polygon'); - } + Future getErc20Token( + WalletBase wallet, String contractAddress) => + (wallet as PolygonWallet).getErc20Token(contractAddress, 'polygon'); @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { @@ -176,23 +174,29 @@ class CWPolygon extends Polygon { } @override - void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled) { - (wallet as PolygonWallet).updateScanProviderUsageState(isEnabled); - } + void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled) => + (wallet as PolygonWallet).updateScanProviderUsageState(isEnabled); @override - Web3Client? getWeb3Client(WalletBase wallet) { - return (wallet as PolygonWallet).getWeb3Client(); - } + Web3Client? getWeb3Client(WalletBase wallet) => + (wallet as PolygonWallet).getWeb3Client(); - String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; + @override + String getTokenAddress(CryptoCurrency asset) => + (asset as Erc20Token).contractAddress; + + @override + Future createTokenApproval(WalletBase wallet, + BigInt amount, String spender, CryptoCurrency token, TransactionPriority priority) => + (wallet as EVMChainWallet) + .createApprovalTransaction(amount, spender, token, priority as EVMChainTransactionPriority); @override void setLedgerConnection( WalletBase wallet, ledger.LedgerConnection connection) { ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials) .setLedgerConnection( - connection, wallet.walletInfo.derivationInfo?.derivationPath); + connection, wallet.walletInfo.derivationInfo?.derivationPath); } @override @@ -206,9 +210,10 @@ class CWPolygon extends Polygon { throw err; } } - + @override - List getDefaultTokenContractAddresses() { - return DefaultPolygonErc20Tokens().initialPolygonErc20Tokens.map((e) => e.contractAddress).toList(); - } + List getDefaultTokenContractAddresses() => DefaultPolygonErc20Tokens() + .initialPolygonErc20Tokens + .map((e) => e.contractAddress) + .toList(); } diff --git a/lib/router.dart b/lib/router.dart index 4150c239f..afb1b46f8 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -48,6 +48,7 @@ import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dar import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; import 'package:cake_wallet/src/screens/faq/faq_page.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart'; @@ -920,6 +921,11 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(), ); + case Routes.dEuroSavings: + return MaterialPageRoute( + builder: (_) => getIt.get(), + ); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index bbaca6618..addaec1d9 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -127,4 +127,6 @@ class Routes { static const walletGroupExistingSeedDescriptionPage = '/wallet_group_existing_seed_description_page'; static const walletSeedVerificationPage = '/wallet_seed_verification_page'; static const exchangeTradeExternalSendPage = '/exchange_trade_external_send_page'; + + static const dEuroSavings = '/integration/dEuro/savings'; } diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index 50257ad07..ba19f5329 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -5,15 +5,16 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class CakeFeaturesPage extends StatelessWidget { - CakeFeaturesPage({required this.dashboardViewModel, required this.cakeFeaturesViewModel}); + CakeFeaturesPage( + {required this.dashboardViewModel, required this.cakeFeaturesViewModel}); final DashboardViewModel dashboardViewModel; final CakeFeaturesViewModel cakeFeaturesViewModel; @@ -58,6 +59,23 @@ class CakeFeaturesPage extends StatelessWidget { fit: BoxFit.cover, ), ), + if (dashboardViewModel.type == WalletType.ethereum) ...[ + DashBoardRoundedCardWidget( + isDarkTheme: dashboardViewModel.isDarkTheme, + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + onTap: () => + Navigator.of(context).pushNamed(Routes.dEuroSavings), + title: S.of(context).deuro_savings, + subTitle: S.of(context).deuro_savings_subtitle, + image: Image.asset( + 'assets/images/deuro_icon.png', + height: 80, + width: 80, + fit: BoxFit.cover, + ), + ), + ], DashBoardRoundedCardWidget( isDarkTheme: dashboardViewModel.isDarkTheme, shadowBlur: dashboardViewModel.getShadowBlur(), diff --git a/lib/src/screens/integrations/deuro/savings_page.dart b/lib/src/screens/integrations/deuro/savings_page.dart new file mode 100644 index 000000000..a58578893 --- /dev/null +++ b/lib/src/screens/integrations/deuro/savings_page.dart @@ -0,0 +1,197 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card_widget.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/gradient_background.dart'; +import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + +class DEuroSavingsPage extends BasePage { + final DEuroViewModel _dEuroViewModel; + + DEuroSavingsPage(this._dEuroViewModel); + + @override + bool get gradientBackground => true; + + @override + Widget Function(BuildContext, Widget) get rootWrapper => + (context, scaffold) => GradientBackground(scaffold: scaffold); + + @override + String get title => S.current.deuro_savings; + + Widget trailing(BuildContext context) => MergeSemantics( + child: SizedBox( + height: 37, + width: 37, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: "Refresh", + child: TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.onSurface, + overlayColor: WidgetStateColor.resolveWith( + (states) => Colors.transparent), + ), + onPressed: _dEuroViewModel.reloadSavingsUserData, + child: Icon( + Icons.refresh, + color: pageIconColor(context), + size: 20, + ), + ), + ), + ), + ), + ); + + @override + Widget body(BuildContext context) { + WidgetsBinding.instance + .addPostFrameCallback((_) => _setReactions(context, _dEuroViewModel)); + + return Container( + width: double.infinity, + child: Column( + children: [ + Observer( + builder: (_) => SavingsCard( + isDarkTheme: currentTheme.isDark, + interestRate: "${_dEuroViewModel.interestRate}%", + savingsBalance: _dEuroViewModel.savingsBalance, + currency: CryptoCurrency.deuro, + onAddSavingsPressed: () => _onSavingsAdd(context), + onRemoveSavingsPressed: () => _onSavingsRemove(context), + onApproveSavingsPressed: _dEuroViewModel.prepareApproval, + isEnabled: _dEuroViewModel.isEnabled, + ), + ), + Observer( + builder: (_) => InterestCardWidget( + isDarkTheme: currentTheme.isDark, + title: S.of(context).deuro_savings_collect_interest, + collectedInterest: _dEuroViewModel.accruedInterest, + onCollectInterest: _dEuroViewModel.prepareCollectInterest, + ), + ), + ], + ), + ); + } + + Future _onSavingsAdd(BuildContext context) async { + final amount = await Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => SavingEditPage(isAdding: true))); + if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, true); + } + + Future _onSavingsRemove(BuildContext context) async { + final amount = await Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => SavingEditPage(isAdding: false))); + if (amount != null) _dEuroViewModel.prepareSavingsEdit(amount, false); + } + + bool _isReactionsSet = false; + + void _setReactions(BuildContext context, DEuroViewModel dEuroViewModel) { + if (_isReactionsSet) return; + + reaction((_) => dEuroViewModel.transaction, (PendingTransaction? tx) async { + if (tx == null) return; + final result = await showModalBottomSheet( + context: context, + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) => ConfirmSendingBottomSheet( + key: ValueKey('savings_page_confirm_sending_dialog_key'), + titleText: S.of(bottomSheetContext).confirm_transaction, + currentTheme: currentTheme, + walletType: WalletType.ethereum, + titleIconPath: CryptoCurrency.deuro.iconPath, + currency: CryptoCurrency.deuro, + amount: S.of(bottomSheetContext).send_amount, + amountValue: tx.amountFormatted, + fiatAmountValue: "", + fee: S.of(bottomSheetContext).send_estimated_fee, + feeValue: tx.feeFormatted, + feeFiatAmount: "", + outputs: [], + onSlideComplete: () async { + Navigator.of(bottomSheetContext).pop(true); + dEuroViewModel.commitTransaction(); + }, + change: tx.change, + ), + ); + + if (result == null) dEuroViewModel.dismissTransaction(); + }); + + reaction((_) => dEuroViewModel.approvalTransaction, (PendingTransaction? tx) async { + if (tx == null) return; + final result = await showModalBottomSheet( + context: context, + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) => ConfirmSendingBottomSheet( + key: ValueKey('savings_page_confirm_approval_dialog_key'), + titleText: S.of(bottomSheetContext).approve_tokens, + currentTheme: currentTheme, + walletType: WalletType.ethereum, + titleIconPath: CryptoCurrency.deuro.iconPath, + currency: CryptoCurrency.deuro, + amount: S.of(bottomSheetContext).send_amount, + amountValue: tx.amountFormatted, + fiatAmountValue: "", + fee: S.of(bottomSheetContext).send_estimated_fee, + feeValue: tx.feeFormatted, + feeFiatAmount: "", + outputs: [], + onSlideComplete: () { + Navigator.of(bottomSheetContext).pop(true); + dEuroViewModel.commitApprovalTransaction(); + }, + change: tx.change, + ), + ); + + if (result == null) dEuroViewModel.dismissTransaction(); + }); + + reaction((_) => dEuroViewModel.state, (ExecutionState state) async { + if (state is TransactionCommitted) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!context.mounted) return; + + await showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext bottomSheetContext) => InfoBottomSheet( + currentTheme: currentTheme, + titleText: S.of(bottomSheetContext).transaction_sent, + contentImage: 'assets/images/birthday_cake.png', + content: S.of(bottomSheetContext).deuro_tx_commited_content, + actionButtonText: S.of(bottomSheetContext).close, + actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'), + actionButton: () => Navigator.of(bottomSheetContext).pop(), + ), + ); + }); + } + }); + + _isReactionsSet = true; + } +} diff --git a/lib/src/screens/integrations/deuro/widgets/edit_savings_bottom_sheet.dart b/lib/src/screens/integrations/deuro/widgets/edit_savings_bottom_sheet.dart new file mode 100644 index 000000000..f76b13e7e --- /dev/null +++ b/lib/src/screens/integrations/deuro/widgets/edit_savings_bottom_sheet.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; + +class EditSavingsBottomSheet extends BaseBottomSheet { + EditSavingsBottomSheet(this.dEuroViewModel, {required super.titleText}); + + final _amountController = TextEditingController(); + final DEuroViewModel dEuroViewModel; + + @override + Widget contentWidget(BuildContext context) => Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: CurrencyAmountTextField( + hasUnderlineBorder: true, + borderWidth: 1.0, + selectedCurrency: CryptoCurrency.deuro.name.toUpperCase(), + amountFocusNode: null, + amountController: _amountController, + tag: CryptoCurrency.deuro.tag, + isAmountEditable: true, + ), + ), + ], + ); + + @override + Widget footerWidget(BuildContext context) => Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 34), + child: LoadingPrimaryButton( + onPressed: () => dEuroViewModel.prepareSavingsEdit(_amountController.text, true), + text: S.of(context).confirm, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isLoading: false, + isDisabled: false, + ), + ); +} diff --git a/lib/src/screens/integrations/deuro/widgets/interest_card_widget.dart b/lib/src/screens/integrations/deuro/widgets/interest_card_widget.dart new file mode 100644 index 000000000..03dd174f6 --- /dev/null +++ b/lib/src/screens/integrations/deuro/widgets/interest_card_widget.dart @@ -0,0 +1,67 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart'; +import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; + +class InterestCardWidget extends StatelessWidget { + InterestCardWidget({ + required this.title, + required this.collectedInterest, + super.key, + required this.isDarkTheme, + required this.onCollectInterest, + }); + + final String title; + final String collectedInterest; + final bool isDarkTheme; + final VoidCallback onCollectInterest; + + @override + Widget build(BuildContext context) { + return Stack(children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: 16), + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + colors: [ + isDarkTheme + ? CustomThemeColors.cardGradientColorPrimaryDark + : CustomThemeColors.cardGradientColorPrimaryLight, + isDarkTheme + ? CustomThemeColors.cardGradientColorSecondaryDark + : CustomThemeColors.cardGradientColorSecondaryLight, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Padding( + padding: EdgeInsets.all(20), + child: Column( + children: [ + SavingsCard.getAssetBalanceRow( + context, + title: title, + subtitle: collectedInterest, + currency: CryptoCurrency.deuro, + hideSymbol: true, + ), + SizedBox(height: 10), + SavingsCard.getButton( + context, + label: S.of(context).deuro_collect_interest, + onPressed: onCollectInterest, + backgroundColor: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onPrimary, + ), + ], + ), + ), + ), + ]); + } +} diff --git a/lib/src/screens/integrations/deuro/widgets/numpad.dart b/lib/src/screens/integrations/deuro/widgets/numpad.dart new file mode 100644 index 000000000..17914376c --- /dev/null +++ b/lib/src/screens/integrations/deuro/widgets/numpad.dart @@ -0,0 +1,107 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class NumberPad extends StatelessWidget { + final VoidCallback? onDecimalPressed; + final VoidCallback onDeletePressed; + final void Function(int index) onNumberPressed; + final FocusNode focusNode; + + const NumberPad({ + super.key, + required this.onNumberPressed, + required this.onDeletePressed, + required this.focusNode, + this.onDecimalPressed, + }); + + @override + Widget build(BuildContext context) => KeyboardListener( + focusNode: focusNode, + onKeyEvent: (keyEvent) { + if (keyEvent is KeyDownEvent) { + if (keyEvent.logicalKey.keyLabel == "Backspace") { + return onDeletePressed(); + } + + if ([".", ","].contains(keyEvent.logicalKey.keyLabel) && + onDecimalPressed != null) { + return onDecimalPressed!(); + } + + int? number = int.tryParse(keyEvent.character ?? ''); + if (number != null) return onNumberPressed(number); + } + }, + child: SizedBox( + height: 300, + child: GridView.count( + childAspectRatio: 2, + shrinkWrap: true, + crossAxisCount: 3, + physics: const NeverScrollableScrollPhysics(), + children: List.generate(12, (index) { + if (index == 9) { + if (onDecimalPressed == null) return Container(); + return InkWell( + onTap: onDecimalPressed, + child: Center( + child: Text( + '.', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 30, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ), + ); + } else if (index == 10) { + index = 0; + } else if (index == 11) { + return MergeSemantics( + child: Container( + child: Semantics( + label: S.of(context).delete, + button: true, + onTap: onDeletePressed, + child: TextButton( + onPressed: onDeletePressed, + style: TextButton.styleFrom( + backgroundColor: + Colors.transparent, + shape: CircleBorder(), + ), + child: Image.asset( + 'assets/images/delete_icon.png', + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ); + } else { + index++; + } + + return InkWell( + onTap: () => onNumberPressed(index), + child: Center( + child: Text( + '$index', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 30, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ), + ); + }), + ), + ), + ); +} diff --git a/lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart b/lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart new file mode 100644 index 000000000..9196929c4 --- /dev/null +++ b/lib/src/screens/integrations/deuro/widgets/savings_card_widget.dart @@ -0,0 +1,263 @@ +import 'dart:math'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/material.dart'; + +class SavingsCard extends StatelessWidget { + final bool isDarkTheme; + final bool isEnabled; + final String interestRate; + final String savingsBalance; + final CryptoCurrency currency; + final VoidCallback onAddSavingsPressed; + final VoidCallback onRemoveSavingsPressed; + final VoidCallback onApproveSavingsPressed; + + const SavingsCard({ + super.key, + required this.isDarkTheme, + required this.interestRate, + required this.savingsBalance, + required this.currency, + required this.onAddSavingsPressed, + required this.onRemoveSavingsPressed, + required this.onApproveSavingsPressed, + this.isEnabled = true, + }); + + @override + Widget build(BuildContext context) => Container( + margin: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + colors: [ + isDarkTheme + ? CustomThemeColors.cardGradientColorPrimaryDark + : CustomThemeColors.cardGradientColorPrimaryLight, + isDarkTheme + ? CustomThemeColors.cardGradientColorSecondaryDark + : CustomThemeColors.cardGradientColorSecondaryLight, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Container( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + getAssetBalanceRow(context, + title: S.of(context).deuro_savings_balance, + subtitle: savingsBalance, + currency: currency), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + 'Current APR', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: + Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + softWrap: true, + ), + ), + Text( + interestRate, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + softWrap: true, + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: isEnabled + ? [ + Expanded( + child: getButton( + context, + label: S.of(context).deuro_savings_add, + imagePath: 'assets/images/received.png', + onPressed: onAddSavingsPressed, + backgroundColor: + Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + SizedBox(width: 12), + Expanded( + child: getButton( + context, + label: S.of(context).deuro_savings_remove, + imagePath: 'assets/images/upload.png', + onPressed: onRemoveSavingsPressed, + backgroundColor: + Theme.of(context).colorScheme.surface, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ] + : [ + Expanded( + child: getButton( + context, + label: S.of(context).deuro_savings_set_approval, + onPressed: onApproveSavingsPressed, + backgroundColor: + Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onPrimary, + ), + ) + ], + ), + ], + ), + )); + + static Widget getButton( + BuildContext context, { + required String label, + String? imagePath, + required VoidCallback onPressed, + required Color backgroundColor, + required Color color, + }) => + Semantics( + label: label, + child: OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + backgroundColor: backgroundColor, + side: BorderSide( + color: Theme.of(context).colorScheme.outlineVariant.withAlpha(0), + width: 0, + ), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (imagePath != null) ...[ + Image.asset( + imagePath, + height: 30, + width: 30, + color: color, + ), + const SizedBox(width: 8), + ], + Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: color, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ); + + static Widget getAssetBalanceRow( + BuildContext context, { + required String title, + required String subtitle, + required CryptoCurrency currency, + bool hideSymbol = true, + }) => + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1, + ), + ), + SizedBox(height: 6), + AutoSizeText( + subtitle, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w900, + fontSize: 24, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.start, + ), + ], + ), + SizedBox( + //width: min(MediaQuery.of(context).size.width * 0.2, 100), + child: Center( + child: Column( + children: [ + CakeImageWidget( + imageUrl: currency.iconPath, + height: 40, + width: 40, + errorWidget: Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title + .substring(0, min(currency.title.length, 2)), + style: + Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 11, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ), + ), + if (!hideSymbol) ...[ + const SizedBox(height: 10), + Text( + currency.title, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + height: 1, + ), + ), + ] + ], + ), + ), + ), + ], + ); +} diff --git a/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart b/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart new file mode 100644 index 000000000..bb1733fb5 --- /dev/null +++ b/lib/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart @@ -0,0 +1,90 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/integrations/deuro/widgets/numpad.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:flutter/material.dart'; + +class SavingEditPage extends BasePage { + final bool isAdding; + + SavingEditPage({required this.isAdding}); + + String get title => + isAdding ? S.current.deuro_savings_add : S.current.deuro_savings_remove; + + @override + Widget body(BuildContext context) => _SavingsEditBody(); +} + +class _SavingsEditBody extends StatefulWidget { + const _SavingsEditBody(); + + @override + State createState() => _SavingsEditBodyState(); +} + +class _SavingsEditBodyState extends State<_SavingsEditBody> { + @override + void initState() { + WidgetsBinding.instance + .addPostFrameCallback((_) => _numpadFocusNode.requestFocus()); + super.initState(); + } + + @override + void dispose() { + _numpadFocusNode.dispose(); + super.dispose(); + } + + String amount = '0'; + final FocusNode _numpadFocusNode = FocusNode(); + + @override + Widget build(BuildContext context) => SafeArea( + child: Column(children: [ + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.only(left: 26, right: 26, top: 10), + child: AutoSizeText( + "${amount.toString()} dEuro", + maxLines: 1, + maxFontSize: 60, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 60, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + ), + )), + NumberPad( + focusNode: _numpadFocusNode, + onNumberPressed: (i) => setState( + () => amount = amount == '0' ? i.toString() : '${amount}${i}', + ), + onDeletePressed: () => setState( + () => amount = amount.length > 1 + ? amount.substring(0, amount.length - 1) + : '0', + ), + onDecimalPressed: () => + setState(() => amount = '${amount.replaceAll('.', '')}.'), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 34), + child: LoadingPrimaryButton( + onPressed: () => Navigator.pop(context, amount), + text: S.of(context).confirm, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + isLoading: false, + isDisabled: false, + ), + ) + ]), + ); +} diff --git a/lib/src/widgets/dashboard_card_widget.dart b/lib/src/widgets/dashboard_card_widget.dart index f1313c525..d5b4f8853 100644 --- a/lib/src/widgets/dashboard_card_widget.dart +++ b/lib/src/widgets/dashboard_card_widget.dart @@ -109,6 +109,7 @@ class DashBoardRoundedCardWidget extends StatelessWidget { ], ), ), + Padding(padding: EdgeInsets.only(left: 10)), if (image != null) image! else if (svgPicture != null) svgPicture!, if (icon != null) icon! ], diff --git a/lib/view_model/integrations/deuro_view_model.dart b/lib/view_model/integrations/deuro_view_model.dart new file mode 100644 index 000000000..09f9a363c --- /dev/null +++ b/lib/view_model/integrations/deuro_view_model.dart @@ -0,0 +1,119 @@ +import 'dart:math'; + +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:mobx/mobx.dart'; + +part 'deuro_view_model.g.dart'; + +class DEuroViewModel = DEuroViewModelBase with _$DEuroViewModel; + +abstract class DEuroViewModelBase with Store { + final AppStore _appStore; + + DEuroViewModelBase(this._appStore) { + reloadInterestRate(); + reloadSavingsUserData(); + } + + @observable + String savingsBalance = '0.00'; + + @observable + ExecutionState state = InitialExecutionState(); + + @observable + String interestRate = '0'; + + @observable + String accruedInterest = '0.00'; + + @observable + BigInt approvedTokens = BigInt.zero; + + @computed + bool get isEnabled => approvedTokens > BigInt.zero; + + @observable + PendingTransaction? transaction = null; + + @observable + PendingTransaction? approvalTransaction = null; + + @action + Future reloadSavingsUserData() async { + final savingsBalanceRaw = + ethereum!.getDEuroSavingsBalance(_appStore.wallet!); + final accruedInterestRaw = + ethereum!.getDEuroAccruedInterest(_appStore.wallet!); + + approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!); + + savingsBalance = ethereum! + .formatterEthereumAmountToDouble(amount: await savingsBalanceRaw) + .toStringAsFixed(6); + accruedInterest = ethereum! + .formatterEthereumAmountToDouble(amount: await accruedInterestRaw) + .toStringAsFixed(6); + } + + @action + Future reloadInterestRate() async { + final interestRateRaw = + await ethereum!.getDEuroInterestRate(_appStore.wallet!); + + interestRate = (interestRateRaw / BigInt.from(10000)).toString(); + } + + @action + Future prepareApproval() async { + final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; + approvalTransaction = + await ethereum!.enableDEuroSaving(_appStore.wallet!, priority); + } + + @action + Future prepareSavingsEdit(String amountRaw, bool isAdding) async { + final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18)); + final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; + transaction = await (isAdding + ? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority) + : ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority)); + } + + Future prepareCollectInterest() => + prepareSavingsEdit(accruedInterest, false); + + @action + Future commitTransaction() async { + if (transaction != null) { + state = TransactionCommitting(); + await transaction!.commit(); + transaction = null; + reloadSavingsUserData(); + state = TransactionCommitted(); + } + } + + @action + Future commitApprovalTransaction() async { + if (approvalTransaction != null) { + state = TransactionCommitting(); + await approvalTransaction!.commit(); + approvalTransaction = null; + reloadSavingsUserData(); + state = TransactionCommitted(); + } + } + + @action + void dismissTransaction() { + transaction == null; + approvalTransaction = null; + state = InitialExecutionState(); + } +} diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 0c63a33b4..a30865b76 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -56,6 +56,7 @@ "apk_update": "تحديث APK", "approve": "ﺪﻤﺘﻌﻳ", "approve_request": "الموافقة على الطلب", + "approve_tokens": "الموافقة على الرموز", "arrive_in_this_address": "سيصل ${currency} ${tag}إلى هذا العنوان", "ascending": "تصاعدي", "ask_each_time": "اسأل في كل مرة", @@ -244,6 +245,15 @@ "descending": "النزول", "description": "ﻒﺻﻭ", "destination_tag": "علامة الوجهة:", + "deuro_collect_interest": "يجمع", + "deuro_savings": "الادخار ديورو", + "deuro_savings_add": "إيداع", + "deuro_savings_balance": "توازن الادخار", + "deuro_savings_collect_interest": "جمع الاهتمام", + "deuro_savings_remove": "ينسحب", + "deuro_savings_set_approval": "تعيين الموافقة", + "deuro_savings_subtitle": "كسب ما يصل إلى 10 ٪ فائدة على مقتنيات Deuro Stablecoin", + "deuro_tx_commited_content": "قد يستغرق الأمر بضع ثوانٍ حتى يتم تأكيد المعاملة وينعكس على الشاشة", "device_is_signing": "الجهاز يوقع", "dfx_option_description": "شراء التشفير مع EUR & CHF. لعملاء البيع بالتجزئة والشركات في أوروبا", "didnt_get_code": "لم تحصل على رمز؟", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 8163da646..f0b992160 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -56,6 +56,7 @@ "apk_update": "APK ъпдейт", "approve": "Одобряване", "approve_request": "Одобрете искане", + "approve_tokens": "Одобрете жетоните", "arrive_in_this_address": "${currency} ${tag}ще отидат на този адрес", "ascending": "Възходящ", "ask_each_time": "Питайте всеки път", @@ -244,6 +245,15 @@ "descending": "Низходящ", "description": "Описание", "destination_tag": "Destination tag:", + "deuro_collect_interest": "Събиране", + "deuro_savings": "Спестявания на Деуро", + "deuro_savings_add": "Депозит", + "deuro_savings_balance": "Спестотен баланс", + "deuro_savings_collect_interest": "Събиране на интерес", + "deuro_savings_remove": "Оттегляне", + "deuro_savings_set_approval": "Задайте одобрение", + "deuro_savings_subtitle": "Печелете до 10% лихва за вашите Deuro Stablecoin Holdings", + "deuro_tx_commited_content": "Може да отнеме няколко секунди, за да може транзакцията да се потвърди и да бъде отразена на екрана", "device_is_signing": "Устройството подписва", "dfx_option_description": "Купете криптовалута с Eur & CHF. За търговски и корпоративни клиенти в Европа", "didnt_get_code": "Не получихте код?", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 80b386859..e4b59562e 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -56,6 +56,7 @@ "apk_update": "aktualizace APK", "approve": "Schvalovat", "approve_request": "Schválit žádost", + "approve_tokens": "Schválit tokeny", "arrive_in_this_address": "${currency} ${tag}přijde na tuto adresu", "ascending": "Vzestupné", "ask_each_time": "Zeptejte se pokaždé", @@ -244,6 +245,15 @@ "descending": "Klesající", "description": "Popis", "destination_tag": "Destination Tag:", + "deuro_collect_interest": "Sbírat", + "deuro_savings": "dEuro úspory", + "deuro_savings_add": "Vklad", + "deuro_savings_balance": "Úspora zůstatek", + "deuro_savings_collect_interest": "Sbírat zájem", + "deuro_savings_remove": "Odstoupit", + "deuro_savings_set_approval": "Stanovit schválení", + "deuro_savings_subtitle": "Získejte až 10% úrok z vašeho Deuro Stablecoin Holdings", + "deuro_tx_commited_content": "Transakce může trvat několik sekund, aby se potvrdila a odrážela se na obrazovce", "device_is_signing": "Zařízení se podpisu", "dfx_option_description": "Koupit krypto s EUR & CHF. Pro maloobchodní a firemní zákazníky v Evropě", "didnt_get_code": "Nepřišel Vám kód?", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 479954b23..7e3f72e2a 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -56,6 +56,7 @@ "apk_update": "APK-Update", "approve": "Genehmigen", "approve_request": "Anfrage genehmigen", + "approve_tokens": "Token genehmigen", "arrive_in_this_address": "${currency} ${tag} wird an dieser Adresse ankommen", "ascending": "Aufsteigend", "ask_each_time": "Jedes Mal fragen", @@ -206,7 +207,7 @@ "copy": "Kopieren", "copy_address": "Adresse kopieren", "copy_id": "ID kopieren", - "copy_payjoin_address": "Kopieren Sie Payjoin -Adresse", + "copy_payjoin_address": "Kopieren Sie Payjoin-Adresse", "copy_payjoin_url": "Payjoin URL kopieren", "copyWalletConnectLink": "Kopieren Sie den WalletConnect-Link von dApp und fügen Sie ihn hier ein", "corrupted_seed_notice": "Die Dateien für diese Wallet sind beschädigt und können nicht geöffnet werden. Bitte sehen Sie sich die Seeds an, speichern Sie sie und stellen Sie die Wallet wieder her.\n\nWenn der Wert leer ist, konnte der Seed nicht korrekt wiederhergestellt werden.", @@ -244,6 +245,15 @@ "descending": "Absteigend", "description": "Beschreibung", "destination_tag": "Ziel-Tag:", + "deuro_collect_interest": "Auszahlen", + "deuro_savings": "dEuro-Savings", + "deuro_savings_add": "Einzahlen", + "deuro_savings_balance": "Sparguthaben", + "deuro_savings_collect_interest": "Interesse sammeln", + "deuro_savings_remove": "Auszahlen", + "deuro_savings_set_approval": "Genehmigung festlegen", + "deuro_savings_subtitle": "Verdienen Sie bis zu 10% Zinsen für Ihre dEuro Stablecoin Holdings", + "deuro_tx_commited_content": "Es kann ein paar Sekunden dauern, bis die Transaktion bestätigt und auf dem Bildschirm angezeigt", "device_is_signing": "Das Gerät unterschreibt", "dfx_option_description": "Kaufen Sie Krypto mit EUR & CHF. Für Einzelhandel und Unternehmenskunden in Europa", "didnt_get_code": "Kein Code?", @@ -1098,4 +1108,4 @@ "you_will_send": "Konvertieren von", "youCanGoBackToYourDapp": "Sie können jetzt zu Ihrem Dapp zurückkehren", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 971a68455..a841c3041 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -56,6 +56,7 @@ "apk_update": "APK update", "approve": "Approve", "approve_request": "Approve Request", + "approve_tokens": "Approve tokens", "arrive_in_this_address": "${currency} ${tag}will arrive in this address", "ascending": "Ascending", "ask_each_time": "Ask each time", @@ -244,6 +245,15 @@ "descending": "Descending", "description": "Description", "destination_tag": "Destination tag:", + "deuro_collect_interest": "Collect", + "deuro_savings": "dEuro Savings", + "deuro_savings_add": "Deposit", + "deuro_savings_balance": "Savings Balance", + "deuro_savings_collect_interest": "Collect interest", + "deuro_savings_remove": "Withdraw", + "deuro_savings_set_approval": "Set approval", + "deuro_savings_subtitle": "Earn up to 10% interest on your dEuro Stablecoin holdings", + "deuro_tx_commited_content": "It might take a couple of seconds for the transaction to confirm and be reflected on screen", "device_is_signing": "Device is signing", "dfx_option_description": "Buy crypto with EUR & CHF. For retail and corporate customers in Europe", "didnt_get_code": "Didn't get code?", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index fe802237a..17687e7c0 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -56,6 +56,7 @@ "apk_update": "Actualización de APK", "approve": "Aprobar", "approve_request": "Aprobar la solicitud", + "approve_tokens": "Aprobar tokens", "arrive_in_this_address": "${currency} ${tag}llegará a esta dirección", "ascending": "Ascendente", "ask_each_time": "Pregunta cada vez", @@ -244,6 +245,15 @@ "descending": "Descendente", "description": "Descripción", "destination_tag": "Etiqueta de destino:", + "deuro_collect_interest": "Recolectar", + "deuro_savings": "ahorros de deuro", + "deuro_savings_add": "Depósito", + "deuro_savings_balance": "Saldo de ahorro", + "deuro_savings_collect_interest": "Cobrar interés", + "deuro_savings_remove": "Retirar", + "deuro_savings_set_approval": "Establecer aprobación", + "deuro_savings_subtitle": "Gane hasta un 10% de interés en sus Holdings de Deuro Stablecoin", + "deuro_tx_commited_content": "La transacción puede tardar un par de segundos en confirmar y reflejarse en la pantalla", "device_is_signing": "El dispositivo está firmando", "dfx_option_description": "Compre cripto con EUR y CHF. Para clientes minoristas y corporativos en Europa", "didnt_get_code": "¿No recibiste el código?", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index b78ef8bc5..e6b942f5c 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -56,6 +56,7 @@ "apk_update": "Mise à jour d'APK", "approve": "Approuver", "approve_request": "Approuver la demande", + "approve_tokens": "Approuver les jetons", "arrive_in_this_address": "${currency} ${tag}arrivera à cette adresse", "ascending": "Ascendant", "ask_each_time": "Demander à chaque fois", @@ -244,6 +245,15 @@ "descending": "Descendant", "description": "Description", "destination_tag": "Tag de destination :", + "deuro_collect_interest": "Collecter", + "deuro_savings": "Économies de deuro", + "deuro_savings_add": "Dépôt", + "deuro_savings_balance": "Solde d'épargne", + "deuro_savings_collect_interest": "Percevoir l'intérêt", + "deuro_savings_remove": "Retirer", + "deuro_savings_set_approval": "Établir l'approbation", + "deuro_savings_subtitle": "Gagnez jusqu'à 10% d'intérêt sur vos avoirs de Deuro Stablecoin", + "deuro_tx_commited_content": "Il pourrait prendre quelques secondes pour que la transaction confirme et soit reflétée à l'écran", "device_is_signing": "L'appareil signale", "dfx_option_description": "Achetez de la crypto avec EUR & CHF. Pour les clients de la vente au détail et des entreprises en Europe", "didnt_get_code": "Vous n'avez pas reçu le code ?", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 710960b7d..e38d03602 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -56,6 +56,7 @@ "apk_update": "apk sabunta", "approve": "Amincewa", "approve_request": "Amince da bukata", + "approve_tokens": "Amince da Alamu", "arrive_in_this_address": "${currency} ${tag} zai je wurin wannan adireshi", "ascending": "Hau", "ask_each_time": "Tambaya kowane lokaci", @@ -244,6 +245,15 @@ "descending": "Saukowa", "description": "Bayani", "destination_tag": "Tambarin makoma:", + "deuro_collect_interest": "Tara", + "deuro_savings": "deuro tanadi", + "deuro_savings_add": "Yi ajiya", + "deuro_savings_balance": "Ma'auni", + "deuro_savings_collect_interest": "Tattara amfani da sha'awa", + "deuro_savings_remove": "Janye", + "deuro_savings_set_approval": "Saita yarda", + "deuro_savings_subtitle": "Sami har zuwa 10% sha'awa a kan Deuro Stovecoin Rike", + "deuro_tx_commited_content": "Yana iya ɗaukar wasu secondsan seconds don ma'amala don tabbatarwa kuma a nuna shi a allon", "device_is_signing": "Na'urar tana shiga", "dfx_option_description": "Buy crypto tare da Eur & Chf. Don Retail da abokan ciniki na kamfanoni a Turai", "didnt_get_code": "Ba a samun code?", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f0a7efaaf..e9c3bca17 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -56,6 +56,7 @@ "apk_update": "APK अद्यतन", "approve": "मंज़ूरी देना", "approve_request": "अनुरोध को स्वीकृत करें", + "approve_tokens": "टोकन को मंजूरी देना", "arrive_in_this_address": "${currency} ${tag}इस पते पर पहुंचेंगे", "ascending": "आरोही", "ask_each_time": "हर बार पूछें", @@ -244,6 +245,15 @@ "descending": "अवरोही", "description": "विवरण", "destination_tag": "गंतव्य टैग:", + "deuro_collect_interest": "इकट्ठा करना", + "deuro_savings": "देउरो बचत", + "deuro_savings_add": "जमा", + "deuro_savings_balance": "बचत शेष", + "deuro_savings_collect_interest": "ब्याज इकट्ठा करना", + "deuro_savings_remove": "निकालना", + "deuro_savings_set_approval": "अनुमोदन निर्धारित करना", + "deuro_savings_subtitle": "अपने Deuro Stablecoin होल्डिंग्स पर 10% ब्याज कमाएँ", + "deuro_tx_commited_content": "लेन -देन की पुष्टि करने और स्क्रीन पर प्रतिबिंबित होने के लिए कुछ सेकंड लग सकते हैं", "device_is_signing": "उपकरण हस्ताक्षर कर रहा है", "dfx_option_description": "EUR और CHF के साथ क्रिप्टो खरीदें। यूरोप में खुदरा और कॉर्पोरेट ग्राहकों के लिए", "didnt_get_code": "कोड नहीं मिला?", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 5ecfb5b21..3f52f5b66 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -56,6 +56,7 @@ "apk_update": "APK ažuriranje", "approve": "Odobriti", "approve_request": "Odobriti zahtjev", + "approve_tokens": "Odobriti tokene", "arrive_in_this_address": "${currency} ${tag}će stići na ovu adresu", "ascending": "Uzlazni", "ask_each_time": "Pitajte svaki put", @@ -244,6 +245,15 @@ "descending": "Silazni", "description": "Opis", "destination_tag": "Odredišna oznaka:", + "deuro_collect_interest": "Prikupiti", + "deuro_savings": "deuro ušteda", + "deuro_savings_add": "Depozit", + "deuro_savings_balance": "Ravnoteža uštede", + "deuro_savings_collect_interest": "Prikupiti interes", + "deuro_savings_remove": "Povući", + "deuro_savings_set_approval": "Odrediti odobrenje", + "deuro_savings_subtitle": "Zaradite do 10% kamate na svoje Deuro Stablecoin Holdings", + "deuro_tx_commited_content": "Možda će trebati nekoliko sekundi da se transakcija potvrdi i odrazi na zaslonu", "device_is_signing": "Uređaj se potpisuje", "dfx_option_description": "Kupite kriptovalute s Eur & CHF. Za maloprodajne i korporativne kupce u Europi", "didnt_get_code": "Ne dobivate kod?", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 5d274c632..d1d661e7c 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -56,6 +56,7 @@ "apk_update": "APK թարմացում", "approve": "Հաստատել", "approve_request": "Հաստատում է հայցը", + "approve_tokens": "Հաստատում է նշանները", "arrive_in_this_address": "${currency} ${tag}կժամանի այս հասցեում", "ascending": "Աճող", "ask_each_time": "Հարցնել ամեն անգամ", @@ -244,6 +245,15 @@ "descending": "Նվազող", "description": "Նկարագրություն", "destination_tag": "Նպատակակետի պիտակ:", + "deuro_collect_interest": "Հավաքել", + "deuro_savings": "dEuro խնայողություններ", + "deuro_savings_add": "Ավանդ", + "deuro_savings_balance": "Խնայողական հավասարակշռություն", + "deuro_savings_collect_interest": "Հավաքեք հետաքրքրություն", + "deuro_savings_remove": "Հեռացնել", + "deuro_savings_set_approval": "Սահմանել հաստատում", + "deuro_savings_subtitle": "Վաստակեք մինչեւ 10% տոկոսադրույքներ ձեր Deuro Stablecoin Holdings- ի համար", + "deuro_tx_commited_content": "Գործարքի հաստատման եւ արտացոլվելու համար գործարքի համար կարող է տեւել մի քանի վայրկյան", "device_is_signing": "Սարքը ստորագրում է", "dfx_option_description": "Գնեք կրիպտոարժույթ EUR և CHF: Կորպորատիվ և մանրածախ հաճախորդների համար Եվրոպայում", "didnt_get_code": "Չեք ստացել կոդը?", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index b7847badf..eaccd3d99 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -56,6 +56,7 @@ "apk_update": "Pembaruan APK", "approve": "Menyetujui", "approve_request": "Menyetujui permintaan", + "approve_tokens": "Menyetujui token", "arrive_in_this_address": "${currency} ${tag} akan tiba di alamat ini", "ascending": "Naik", "ask_each_time": "Tanyakan setiap kali", @@ -244,6 +245,15 @@ "descending": "Menurun", "description": "Keterangan", "destination_tag": "Tag tujuan:", + "deuro_collect_interest": "Mengumpulkan", + "deuro_savings": "Tabungan dEuro", + "deuro_savings_add": "Deposito", + "deuro_savings_balance": "Keseimbangan tabungan", + "deuro_savings_collect_interest": "Mengumpulkan minat", + "deuro_savings_remove": "Menarik", + "deuro_savings_set_approval": "Tetapkan persetujuan", + "deuro_savings_subtitle": "Hasilkan hingga 10% bunga di Deuro Stablecoin Holdings Anda", + "deuro_tx_commited_content": "Mungkin butuh beberapa detik untuk transaksi untuk mengkonfirmasi dan direfleksikan di layar", "device_is_signing": "Perangkat sedang menandatangani", "dfx_option_description": "Beli crypto dengan EUR & CHF. Untuk pelanggan ritel dan perusahaan di Eropa", "didnt_get_code": "Tidak mendapatkan kode?", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a0ff46c4f..65c4085d1 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -56,6 +56,7 @@ "apk_update": "Aggiornamento APK", "approve": "Approvare", "approve_request": "Approvare la richiesta", + "approve_tokens": "Approvare i token", "arrive_in_this_address": "${currency} ${tag}arriverà a questo indirizzo", "ascending": "Ascendente", "ask_each_time": "Chiedi ogni volta", @@ -244,6 +245,15 @@ "descending": "Discendente", "description": "Descrizione", "destination_tag": "Tag destinazione:", + "deuro_collect_interest": "Raccogliere", + "deuro_savings": "Risparmio di dEuro", + "deuro_savings_add": "Depositare", + "deuro_savings_balance": "Saldo di risparmio", + "deuro_savings_collect_interest": "Raccogliere interesse", + "deuro_savings_remove": "Ritirare", + "deuro_savings_set_approval": "Impostare l'approvazione", + "deuro_savings_subtitle": "Guadagna fino al 10% di interesse su Deuro StableCoin Holdings", + "deuro_tx_commited_content": "Potrebbero essere necessari un paio di secondi per confermare la transazione ed essere riflessa sullo schermo", "device_is_signing": "Il dispositivo sta firmando", "dfx_option_description": "Acquista Crypto con EUR & CHF. Per i clienti al dettaglio e aziendali in Europa", "didnt_get_code": "Non hai ricevuto il codice?", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index ac8dafbac..f638c1850 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -56,6 +56,7 @@ "apk_update": "APKアップデート", "approve": "承認する", "approve_request": "リクエストを承認します", + "approve_tokens": "トークンを承認します", "arrive_in_this_address": "${currency} ${tag}はこの住所に到着します", "ascending": "上昇", "ask_each_time": "毎回尋ねてください", @@ -244,6 +245,15 @@ "descending": "下降", "description": "説明", "destination_tag": "宛先タグ:", + "deuro_collect_interest": "集める", + "deuro_savings": "dEuro Savings", + "deuro_savings_add": "デポジット", + "deuro_savings_balance": "貯蓄バランス", + "deuro_savings_collect_interest": "興味を集めます", + "deuro_savings_remove": "撤回する", + "deuro_savings_set_approval": "承認を設定します", + "deuro_savings_subtitle": "Deuro Stablecoin Holdingsで最大10%の利息を稼ぐ", + "deuro_tx_commited_content": "トランザクションが確認され、画面に反映されるまでに数秒かかる場合があります", "device_is_signing": "デバイスが署名しています", "dfx_option_description": "EUR&CHFで暗号を購入します。ヨーロッパの小売および企業の顧客向け", "didnt_get_code": "コードを取得しませんか?", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index e2b21aa04..1c0a4cee7 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -56,6 +56,7 @@ "apk_update": "APK 업데이트", "approve": "승인", "approve_request": "요청 승인", + "approve_tokens": "토큰을 승인합니다", "arrive_in_this_address": "${currency} ${tag}이(가) 이 주소로 도착합니다", "ascending": "오름차순", "ask_each_time": "매번 묻기", @@ -244,6 +245,15 @@ "descending": "내림차순", "description": "설명", "destination_tag": "목적지 태그:", + "deuro_collect_interest": "모으다", + "deuro_savings": "도로 저축", + "deuro_savings_add": "보증금", + "deuro_savings_balance": "저축 잔고", + "deuro_savings_collect_interest": "관심을 모으십시오", + "deuro_savings_remove": "철회하다", + "deuro_savings_set_approval": "승인을 설정하십시오", + "deuro_savings_subtitle": "Deuro Stablecoin Holdings에 최대 10%의이자를 받으십시오.", + "deuro_tx_commited_content": "트랜잭션이 확인하고 화면에 반영되는 데 몇 초가 걸릴 수 있습니다.", "device_is_signing": "장치가 서명 중입니다", "dfx_option_description": "EUR 및 CHF로 암호화폐 구매. 유럽의 개인 및 기업 고객 대상", "didnt_get_code": "코드를 받지 못했나요?", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index edafed5d2..6727c7fcf 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -56,6 +56,7 @@ "apk_update": "APK အပ်ဒိတ်", "approve": "လက်မခံပါ။", "approve_request": "တောင်းဆိုမှုကိုအတည်ပြု", + "approve_tokens": "တိုကင်အတည်ပြု", "arrive_in_this_address": "${currency} ${tag}ဤလိပ်စာသို့ ရောက်ရှိပါမည်။", "ascending": "တက်", "ask_each_time": "တစ်ခုချင်းစီကိုအချိန်မေးပါ", @@ -244,6 +245,15 @@ "descending": "ဆင်း", "description": "ဖော်ပြချက်", "destination_tag": "ခရီးဆုံးအမှတ်-", + "deuro_collect_interest": "စုဝေး", + "deuro_savings": "dEuro ငွေစု", + "deuro_savings_add": "အပ်ငေှ", + "deuro_savings_balance": "ငွေစုချိန်ခွင်လျှာ", + "deuro_savings_collect_interest": "အကျိုးစီးပွားစုဆောင်းပါ", + "deuro_savings_remove": "ဆုတ်ခွာ", + "deuro_savings_set_approval": "အတည်ပြုချက်ကိုသတ်မှတ်ပါ", + "deuro_savings_subtitle": "သင်၏ Deuro Stabloin Holdings တွင် 10% အထိစိတ်ဝင်စားပါ", + "deuro_tx_commited_content": "၎င်းသည်ငွေပေးငွေယူကိုအတည်ပြုရန်နှင့်မျက်နှာပြင်ပေါ်တွင်ထင်ဟပ်ရန်စက္ကန့်အနည်းငယ်ကြာနိုင်သည်", "device_is_signing": "ကိရိယာလက်မှတ်ထိုးနေသည်", "dfx_option_description": "Crypto ကို EUR & CHF ဖြင့် 0 ယ်ပါ။ လက်လီရောင်းဝယ်မှုနှင့်ဥရောပရှိကော်ပိုရိတ်ဖောက်သည်များအတွက်", "didnt_get_code": "ကုဒ်ကို မရဘူးလား?", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index c47160a5f..4f925bc74 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -56,6 +56,7 @@ "apk_update": "APK-update", "approve": "Goedkeuren", "approve_request": "Het verzoek goedkeuren", + "approve_tokens": "Tokens goedkeuren", "arrive_in_this_address": "${currency} ${tag}komt aan op dit adres", "ascending": "Stijgend", "ask_each_time": "Vraag het elke keer", @@ -244,6 +245,15 @@ "descending": "Aflopend", "description": "Beschrijving", "destination_tag": "Bestemmingstag:", + "deuro_collect_interest": "Verzamelen", + "deuro_savings": "dEuro -besparingen", + "deuro_savings_add": "Borg", + "deuro_savings_balance": "Spaarbalans", + "deuro_savings_collect_interest": "Verzamel interesse", + "deuro_savings_remove": "Terugtrekken", + "deuro_savings_set_approval": "Goedkeuring instellen", + "deuro_savings_subtitle": "Verdien tot 10% rente op uw Deuro Stablecoin Holdings", + "deuro_tx_commited_content": "Het kan een paar seconden duren voordat de transactie wordt bevestigd en weerspiegeld op het scherm", "device_is_signing": "Apparaat ondertekent", "dfx_option_description": "Koop crypto met EUR & CHF. Voor retail- en zakelijke klanten in Europa", "didnt_get_code": "Geen code?", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index b3b99e28c..baf0f2502 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -56,6 +56,7 @@ "apk_update": "Aktualizacja APK", "approve": "Zatwierdzić", "approve_request": "Zatwierdzić żądanie", + "approve_tokens": "Zatwierdzić tokeny", "arrive_in_this_address": "${currency} ${tag}dotrze na ten adres", "ascending": "Wznoszący się", "ask_each_time": "Zapytaj za każdym razem", @@ -244,6 +245,15 @@ "descending": "Malejąco", "description": "Opis", "destination_tag": "Tag docelowy:", + "deuro_collect_interest": "Zbierać", + "deuro_savings": "dEuro oszczędności", + "deuro_savings_add": "Depozyt", + "deuro_savings_balance": "Równowaga oszczędności", + "deuro_savings_collect_interest": "Zbieraj zainteresowanie", + "deuro_savings_remove": "Wycofać", + "deuro_savings_set_approval": "Ustaw zatwierdzenie", + "deuro_savings_subtitle": "Zarabiaj do 10% odsetek od Deuro Stablecoin Holdings", + "deuro_tx_commited_content": "Potwierdzenie i odbicie na ekranie może potrwać kilka sekund", "device_is_signing": "Urządzenie podpisuje", "dfx_option_description": "Kup krypto za EUR & CHF. Dla klientów prywatnych i korporacyjnych w Europie", "didnt_get_code": "Nie dostałeś kodu?", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 9b55fc7b6..375996c99 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -56,6 +56,7 @@ "apk_update": "Atualização de APK", "approve": "Aprovar", "approve_request": "Aprovar solicitação", + "approve_tokens": "Aprovar tokens", "arrive_in_this_address": "${currency} ${tag}chegará neste endereço", "ascending": "Ascendente", "ask_each_time": "Pergunte cada vez", @@ -244,6 +245,15 @@ "descending": "descendente", "description": "Descrição", "destination_tag": "Tag de destino:", + "deuro_collect_interest": "Coletar", + "deuro_savings": "dEuro Savings", + "deuro_savings_add": "Depósito", + "deuro_savings_balance": "Balanço de poupança", + "deuro_savings_collect_interest": "Coletar juros", + "deuro_savings_remove": "Retirar", + "deuro_savings_set_approval": "Defina aprovação", + "deuro_savings_subtitle": "Ganhe até 10% de juros em sua Deuro Stablecoin Holdings", + "deuro_tx_commited_content": "Pode levar alguns segundos para a transação confirmar e se refletir na tela", "device_is_signing": "O dispositivo está assinando", "dfx_option_description": "Compre criptografia com EUR & CHF. Para clientes de varejo e corporativo na Europa", "didnt_get_code": "Não recebeu o código?", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 3e61f4277..800201b27 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -56,6 +56,7 @@ "apk_update": "Обновление APK", "approve": "Утвердить", "approve_request": "Утвердить запрос", + "approve_tokens": "Одобрить токены", "arrive_in_this_address": "${currency} ${tag}придет на этот адрес", "ascending": "Восходящий", "ask_each_time": "Спросите каждый раз", @@ -244,6 +245,15 @@ "descending": "Нисходящий", "description": "Описание", "destination_tag": "Целевой тег:", + "deuro_collect_interest": "Собирать", + "deuro_savings": "dEuro Savings", + "deuro_savings_add": "Депозитный", + "deuro_savings_balance": "Сберегательный баланс", + "deuro_savings_collect_interest": "Собирать интерес", + "deuro_savings_remove": "Отзывать", + "deuro_savings_set_approval": "Установить утверждение", + "deuro_savings_subtitle": "Заработайте до 10% процентов на ваших Deuro Stablecoin Holdings", + "deuro_tx_commited_content": "Чтобы подтвердить, может потребоваться пару секунд, чтобы подтвердить и быть отраженным на экране", "device_is_signing": "Устройство подписывает", "dfx_option_description": "Купить крипто с Eur & CHF. Для розничных и корпоративных клиентов в Европе", "didnt_get_code": "Не получить код?", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index a60f1b332..bfaa92b08 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -56,6 +56,7 @@ "apk_update": "ปรับปรุง APK", "approve": "อนุมัติ", "approve_request": "อนุมัติคำขอ", + "approve_tokens": "อนุมัติโทเค็น", "arrive_in_this_address": "${currency} ${tag}จะมาถึงที่อยู่นี้", "ascending": "จากน้อยไปมาก", "ask_each_time": "ถามทุกครั้ง", @@ -244,6 +245,15 @@ "descending": "ลงมา", "description": "คำอธิบาย", "destination_tag": "แท็กปลายทาง:", + "deuro_collect_interest": "เก็บรวบรวม", + "deuro_savings": "การออมของ dEuro", + "deuro_savings_add": "เงินฝาก", + "deuro_savings_balance": "ยอดเงินออม", + "deuro_savings_collect_interest": "เก็บดอกเบี้ย", + "deuro_savings_remove": "ถอน", + "deuro_savings_set_approval": "ตั้งค่าการอนุมัติ", + "deuro_savings_subtitle": "รับดอกเบี้ยมากถึง 10% สำหรับ Deuro Stablecoin Holdings ของคุณ", + "deuro_tx_commited_content": "อาจใช้เวลาสองสามวินาทีในการทำธุรกรรมเพื่อยืนยันและสะท้อนบนหน้าจอ", "device_is_signing": "อุปกรณ์กำลังลงนาม", "dfx_option_description": "ซื้อ crypto ด้วย Eur & CHF สำหรับลูกค้ารายย่อยและลูกค้าในยุโรป", "didnt_get_code": "ไม่ได้รับรหัส?", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 9ed4e13a8..8e571c3cb 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -56,6 +56,7 @@ "apk_update": "APK update", "approve": "Aprubahan", "approve_request": "Aprubahan ang kahilingan", + "approve_tokens": "Aprubahan ang mga token", "arrive_in_this_address": "Ang ${currency} ${tag} ay darating sa address na ito", "ascending": "Umakyat", "ask_each_time": "Magtanong sa tuwing", @@ -244,6 +245,15 @@ "descending": "Pababang", "description": "Paglalarawan", "destination_tag": "Tag ng patutunguhan:", + "deuro_collect_interest": "Mangolekta", + "deuro_savings": "Pagtipid ni dEuro", + "deuro_savings_add": "Deposito", + "deuro_savings_balance": "Balanse sa pagtitipid", + "deuro_savings_collect_interest": "Mangolekta ng interes", + "deuro_savings_remove": "Umatras", + "deuro_savings_set_approval": "Itakda ang pag -apruba", + "deuro_savings_subtitle": "Kumita ng hanggang sa 10% na interes sa iyong mga hawak na Deuro StableCoin", + "deuro_tx_commited_content": "Maaaring tumagal ng ilang segundo para sa transaksyon upang kumpirmahin at maipakita sa screen", "device_is_signing": "Nag -sign ang aparato", "dfx_option_description": "Bumili ng crypto kasama ang EUR & CHF. Para sa mga retail customer at corporate customer sa Europe", "didnt_get_code": "Hindi nakuha ang code?", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 4d790d1c6..9a9690142 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -56,6 +56,7 @@ "apk_update": "APK güncellemesi", "approve": "Onaylamak", "approve_request": "Talebi Onaylama", + "approve_tokens": "Jetonları onaylayın", "arrive_in_this_address": "${currency} ${tag}bu adrese ulaşacak", "ascending": "Yükselen", "ask_each_time": "Her seferinde sor", @@ -244,6 +245,15 @@ "descending": "Azalan", "description": "Tanım", "destination_tag": "Hedef Etiketi:", + "deuro_collect_interest": "TOPLAMAK", + "deuro_savings": "dEuro Tasarruf", + "deuro_savings_add": "Yatırmak", + "deuro_savings_balance": "Tasarruf Bakiyesi", + "deuro_savings_collect_interest": "İlgi toplamak", + "deuro_savings_remove": "Geri çekilmek", + "deuro_savings_set_approval": "Onay ayarlamak", + "deuro_savings_subtitle": "Deuro StableCoin Holdings'e% 10'a kadar faiz kazanın", + "deuro_tx_commited_content": "İşlemin onaylaması ve ekrana yansıtılması birkaç saniye sürebilir", "device_is_signing": "Cihaz imzalıyor", "dfx_option_description": "Eur & chf ile kripto satın alın. Avrupa'daki perakende ve kurumsal müşteriler için", "didnt_get_code": "Kod gelmedi mi?", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 06b6d09e7..e36599a0c 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -56,6 +56,7 @@ "apk_update": "Оновлення APK", "approve": "Затвердити", "approve_request": "Запитайте запит", + "approve_tokens": "Затвердити токени", "arrive_in_this_address": "${currency} ${tag}надійде на цю адресу", "ascending": "Висхід", "ask_each_time": "Запитайте кожен раз", @@ -244,6 +245,15 @@ "descending": "Низхідний", "description": "опис", "destination_tag": "Тег призначення:", + "deuro_collect_interest": "Збирати", + "deuro_savings": "заощадження dEuro", + "deuro_savings_add": "Депозит", + "deuro_savings_balance": "Баланс заощаджень", + "deuro_savings_collect_interest": "Збирати інтерес", + "deuro_savings_remove": "Відступати", + "deuro_savings_set_approval": "Встановити схвалення", + "deuro_savings_subtitle": "Заробляйте до 10% відсотків на ваших Holdings Deuro StableCoin", + "deuro_tx_commited_content": "Це може знадобитися кілька секунд, щоб транзакція підтвердила та відображалася на екрані", "device_is_signing": "Пристрій підписується", "dfx_option_description": "Купуйте криптовалюту з EUR & CHF. Для роздрібних та корпоративних клієнтів у Європі", "didnt_get_code": "Не отримали код?", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index c83de672b..0ce1b4758 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -56,6 +56,7 @@ "apk_update": "APK اپ ڈیٹ", "approve": "ﻭﺮﮐ ﺭﻮﻈﻨﻣ", "approve_request": "درخواست کو منظور کریں", + "approve_tokens": "ٹوکن کو منظور کریں", "arrive_in_this_address": "${currency} ${tag}اس پتے پر پہنچے گا۔", "ascending": "چڑھنے", "ask_each_time": "ہر بار پوچھیں", @@ -244,6 +245,15 @@ "descending": "اترتے ہوئے", "description": "ﻞﯿﺼﻔﺗ", "destination_tag": "منزل کا ٹیگ:", + "deuro_collect_interest": "جمع کریں", + "deuro_savings": "ڈیورو کی بچت", + "deuro_savings_add": "جمع کروائیں", + "deuro_savings_balance": "بچت کا توازن", + "deuro_savings_collect_interest": "دلچسپی جمع کریں", + "deuro_savings_remove": "واپس لے لو", + "deuro_savings_set_approval": "منظوری طے کریں", + "deuro_savings_subtitle": "اپنے ڈیورو اسٹبل کوئن ہولڈنگز پر 10 ٪ سود حاصل کریں", + "deuro_tx_commited_content": "لین دین کی تصدیق اور اسکرین پر عکاسی کرنے میں اس میں کچھ سیکنڈ لگ سکتے ہیں", "device_is_signing": "ڈیوائس پر دستخط کر رہے ہیں", "dfx_option_description": "یورو اور سی ایچ ایف کے ساتھ کرپٹو خریدیں۔ یورپ میں خوردہ اور کارپوریٹ صارفین کے لئے", "didnt_get_code": "کوڈ نہیں ملتا؟", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 14eeee189..61cadb10b 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -56,6 +56,7 @@ "apk_update": "Cập nhật APK", "approve": "Phê duyệt", "approve_request": "Phê duyệt yêu cầu", + "approve_tokens": "Phê duyệt mã thông báo", "arrive_in_this_address": "${currency} ${tag} sẽ đến địa chỉ này", "ascending": "Tăng dần", "ask_each_time": "Hỏi mỗi lần", @@ -243,6 +244,15 @@ "descending": "Giảm dần", "description": "Mô tả", "destination_tag": "Thẻ đích:", + "deuro_collect_interest": "Sưu tầm", + "deuro_savings": "Tiết kiệm dEuro", + "deuro_savings_add": "Tiền gửi", + "deuro_savings_balance": "Số dư tiết kiệm", + "deuro_savings_collect_interest": "Thu tiền lãi", + "deuro_savings_remove": "Rút", + "deuro_savings_set_approval": "Đặt phê duyệt", + "deuro_savings_subtitle": "Kiếm tới 10% tiền lãi cho Deuro Storcoin Holdings của bạn", + "deuro_tx_commited_content": "Có thể mất vài giây để giao dịch xác nhận và được phản ánh trên màn hình", "device_is_signing": "Thiết bị đang ký", "dfx_option_description": "Mua tiền điện tử bằng EUR & CHF. Dành cho khách hàng bán lẻ và doanh nghiệp tại Châu Âu", "didnt_get_code": "Không nhận được mã?", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index fee486da4..761667b17 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -56,6 +56,7 @@ "apk_update": "Àtúnse áàpù títun wà", "approve": "Fi ọwọ si", "approve_request": "IKILỌ RẸ", + "approve_tokens": "Ṣe fọwọsi awọn àmi", "arrive_in_this_address": "${currency} ${tag} máa dé sí àdírẹ́sì yìí", "ascending": "Goke", "ask_each_time": "Beere lọwọ kọọkan", @@ -244,6 +245,15 @@ "descending": "Sọkalẹ", "description": "Apejuwe", "destination_tag": "Orúkọ tí ìbí tó a ránṣẹ́ sí:", + "deuro_collect_interest": "Kojọ", + "deuro_savings": "dEuro Awọn ifowopamọ", + "deuro_savings_add": "Owo ifipamọ", + "deuro_savings_balance": "Iwontunws.funfun ifowopamọ", + "deuro_savings_collect_interest": "Gba iwulo", + "deuro_savings_remove": "Yọkuro", + "deuro_savings_set_approval": "Ṣeto ifọwọsi", + "deuro_savings_subtitle": "Jo'gun to 10% iwulo lori awọn idaduro Duroblockoin rẹ", + "deuro_tx_commited_content": "O le gba tọkọtaya kan ti awọn aaya fun idunadura lati jẹrisi ati ṣe afihan loju iboju", "device_is_signing": "Ẹrọ n forukọsilẹ", "dfx_option_description": "Ra Crypto pẹlu EUR & CHF. Fun soobu ati awọn alabara ile-iṣẹ ni Yuroopu", "didnt_get_code": "Ko gba koodu?", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index ffa8509c4..5831a51c0 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -56,6 +56,7 @@ "apk_update": "APK更新", "approve": "批准", "approve_request": "批准请求", + "approve_tokens": "批准令牌", "arrive_in_this_address": "${currency} ${tag}将到达此地址", "ascending": "上升", "ask_each_time": "每次问", @@ -244,6 +245,15 @@ "descending": "下降", "description": "描述", "destination_tag": "目标Tag:", + "deuro_collect_interest": "收集", + "deuro_savings": "dEuro储蓄", + "deuro_savings_add": "订金", + "deuro_savings_balance": "储蓄平衡", + "deuro_savings_collect_interest": "收集兴趣", + "deuro_savings_remove": "提取", + "deuro_savings_set_approval": "设定批准", + "deuro_savings_subtitle": "您的Deuro Stablecoin Holdings最多可赚取10%的利息", + "deuro_tx_commited_content": "交易可能需要几秒钟才能确认并在屏幕上反射", "device_is_signing": "设备正在签名", "dfx_option_description": "用Eur&Chf购买加密货币。对于欧洲的零售和企业客户", "didnt_get_code": "没有获取代码?", diff --git a/tool/configure.dart b/tool/configure.dart index 612958f4a..95c8a4d5a 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -670,6 +670,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_base.dart'; @@ -697,6 +698,7 @@ import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_ethereum/ethereum_wallet.dart'; import 'package:cw_ethereum/ethereum_wallet_service.dart'; import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart'; +import 'package:cw_ethereum/deuro/deuro_savings.dart'; import 'package:eth_sig_util/util/utils.dart'; @@ -744,6 +746,16 @@ abstract class Ethereum { void updateEtherscanUsageState(WalletBase wallet, bool isEnabled); Web3Client? getWeb3Client(WalletBase wallet); String getTokenAddress(CryptoCurrency asset); + + Future createTokenApproval(WalletBase wallet, BigInt amount, String spender, CryptoCurrency token, TransactionPriority priority); + + Future getDEuroSavingsBalance(WalletBase wallet); + Future getDEuroAccruedInterest(WalletBase wallet); + Future getDEuroInterestRate(WalletBase wallet); + Future getDEuroSavingsApproved(WalletBase wallet); + Future addDEuroSaving(WalletBase wallet, BigInt amount, TransactionPriority priority); + Future removeDEuroSaving(WalletBase wallet, BigInt amount, TransactionPriority priority); + Future enableDEuroSaving(WalletBase wallet, TransactionPriority priority); void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); @@ -777,6 +789,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_base.dart'; @@ -846,6 +859,8 @@ abstract class Polygon { Future deleteErc20Token(WalletBase wallet, CryptoCurrency token); Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token); Future getErc20Token(WalletBase wallet, String contractAddress); + + Future createTokenApproval(WalletBase wallet, BigInt amount, String spender, CryptoCurrency token, TransactionPriority priority); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled); From 21d5c51cc965e071094de370620e4eb37605ec1d Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 19 Jun 2025 05:38:58 +0300 Subject: [PATCH 15/29] remove unused file [skip ci] --- .../widgets/edit_savings_bottom_sheet.dart | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 lib/src/screens/integrations/deuro/widgets/edit_savings_bottom_sheet.dart diff --git a/lib/src/screens/integrations/deuro/widgets/edit_savings_bottom_sheet.dart b/lib/src/screens/integrations/deuro/widgets/edit_savings_bottom_sheet.dart deleted file mode 100644 index f76b13e7e..000000000 --- a/lib/src/screens/integrations/deuro/widgets/edit_savings_bottom_sheet.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:flutter/material.dart'; - -class EditSavingsBottomSheet extends BaseBottomSheet { - EditSavingsBottomSheet(this.dEuroViewModel, {required super.titleText}); - - final _amountController = TextEditingController(); - final DEuroViewModel dEuroViewModel; - - @override - Widget contentWidget(BuildContext context) => Column( - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 10), - child: CurrencyAmountTextField( - hasUnderlineBorder: true, - borderWidth: 1.0, - selectedCurrency: CryptoCurrency.deuro.name.toUpperCase(), - amountFocusNode: null, - amountController: _amountController, - tag: CryptoCurrency.deuro.tag, - isAmountEditable: true, - ), - ), - ], - ); - - @override - Widget footerWidget(BuildContext context) => Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 34), - child: LoadingPrimaryButton( - onPressed: () => dEuroViewModel.prepareSavingsEdit(_amountController.text, true), - text: S.of(context).confirm, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - isLoading: false, - isDisabled: false, - ), - ); -} From 4b137bc96856e618ba151f62f84c5ac1ea76e3ac Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 19 Jun 2025 18:55:41 +0200 Subject: [PATCH 16/29] CW-1091-payjoin-error-handeling (#2317) * feat: stop polling payjoin on switch wallet * refactor: improve Payjoin session handling and cleanup unused methods - Replaced `initReceiver` with `getUnusedReceiver` to reuse existing Payjoin sessions. - Streamlined session initialization by removing `spawnNewReceiver`. - Adjusted wallet sync reactions to resume Payjoin sessions when necessary. * fix: Receiver.fromJson correctly handle parameter format in Payjoin manager * fix: try reloading unspents if unspents are empty; No Unpsents available are now recoverable errors * fix: ensure transaction details display only if transactionInfo is available and adjust payjoin success status handling * fix: adjust payjoin success status handling for pending transactions * fix: add error handling for Payjoin initialization and receiver creation [skip-ci] * fix: add unrecoverable error handling for Payjoin sender sessions --- cw_bitcoin/lib/bitcoin_wallet.dart | 6 ++ cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 23 ++++--- cw_bitcoin/lib/payjoin/manager.dart | 67 ++++++++++--------- .../lib/payjoin/payjoin_receive_worker.dart | 2 +- cw_bitcoin/lib/payjoin/storage.dart | 11 ++- lib/reactions/on_current_wallet_change.dart | 2 +- .../on_wallet_sync_status_change.dart | 12 +++- .../payjoin_transaction_list_item.dart | 5 +- .../payjoin_details_view_model.dart | 7 +- 9 files changed, 82 insertions(+), 53 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index a23b72660..9231022f6 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -266,6 +266,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { derivationPath: walletInfo.derivationInfo!.derivationPath!); } + @override + Future close({bool shouldCleanup = false}) async { + payjoinManager.cleanupSessions(); + super.close(shouldCleanup: shouldCleanup); + } + late final PayjoinManager payjoinManager; bool get isPayjoinAvailable => unspentCoinsInfo.values diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index a92c4770f..b33d722ab 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -59,19 +59,26 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @action Future initPayjoin() async { - await payjoinManager.initPayjoin(); - currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); - payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); + try { + await payjoinManager.initPayjoin(); + currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); - payjoinManager.resumeSessions(); + payjoinManager.resumeSessions(); + } catch (e) { + printV(e); + } } @action Future newPayjoinReceiver() async { - currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); - payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); + try { + currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress); + payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint(); - printV("Initializing new Payjoin Receiver"); - payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!); + payjoinManager.spawnReceiver(receiver: currentPayjoinReceiver!); + } catch (e) { + printV(e); + } } } diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart index ad6fef0d8..95a523d89 100644 --- a/cw_bitcoin/lib/payjoin/manager.dart +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -53,7 +53,7 @@ class PayjoinManager { } final receiver = Receiver.fromJson(json: session.receiver!); printV("Resuming Payjoin Receiver Session ${receiver.id()}"); - return _spawnReceiver(receiver: receiver); + return spawnReceiver(receiver: receiver); }); printV("Resumed ${spawnedSessions.length} Payjoin Sessions"); @@ -121,15 +121,13 @@ class PayjoinManager { } } catch (e) { _cleanupSession(pjUri); - printV(e); - await _payjoinStorage.markSenderSessionUnrecoverable(pjUri); - completer.completeError(e); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, e.toString()); + completer.complete(); } } else if (message is PayjoinSessionError) { _cleanupSession(pjUri); if (message is UnrecoverableError) { - printV(message.message); - await _payjoinStorage.markSenderSessionUnrecoverable(pjUri); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, message.message); completer.complete(); } else if (message is RecoverableError) { completer.complete(); @@ -149,42 +147,41 @@ class PayjoinManager { return completer.future; } - Future initReceiver(String address, + Future getUnusedReceiver(String address, [bool isTestnet = false]) async { - try { - final ohttpKeys = await PayjoinUri.fetchOhttpKeys( - ohttpRelay: await randomOhttpRelayUrl(), - payjoinDirectory: payjoinDirectoryUrl, - ); + final session = _payjoinStorage.getUnusedActiveReceiverSession(_wallet.id); - final newReceiver = await NewReceiver.create( - address: address, - network: isTestnet ? Network.testnet : Network.bitcoin, - directory: payjoinDirectoryUrl, - ohttpKeys: ohttpKeys, - ); - final persister = PayjoinReceiverPersister.impl(); - final receiverToken = await newReceiver.persist(persister: persister); - final receiver = - await Receiver.load(persister: persister, token: receiverToken); + if (session != null) { + await PayjoinUri.Url.fromStr(payjoinDirectoryUrl); - await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); - - return receiver; - } catch (e) { - throw Exception('Error initializing Payjoin Receiver: $e'); + return Receiver.fromJson(json: session.receiver!); } + + return initReceiver(address); } - Future spawnNewReceiver({ - required Receiver receiver, - bool isTestnet = false, - }) async { + Future initReceiver(String address, [bool isTestnet = false]) async { + final ohttpKeys = await PayjoinUri.fetchOhttpKeys( + ohttpRelay: await randomOhttpRelayUrl(), + payjoinDirectory: payjoinDirectoryUrl, + ); + + final newReceiver = await NewReceiver.create( + address: address, + network: isTestnet ? Network.testnet : Network.bitcoin, + directory: payjoinDirectoryUrl, + ohttpKeys: ohttpKeys, + ); + final persister = PayjoinReceiverPersister.impl(); + final receiverToken = await newReceiver.persist(persister: persister); + final receiver = await Receiver.load(persister: persister, token: receiverToken); + await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); - return _spawnReceiver(isTestnet: isTestnet, receiver: receiver); + + return receiver; } - Future _spawnReceiver({ + Future spawnReceiver({ required Receiver receiver, bool isTestnet = false, }) async { @@ -229,6 +226,10 @@ class PayjoinManager { case PayjoinReceiverRequestTypes.getCandidateInputs: utxos = _wallet.getUtxoWithPrivateKeys(); + if (utxos.isEmpty) { + await _wallet.updateAllUnspents(); + utxos = _wallet.getUtxoWithPrivateKeys(); + } mainToIsolateSendPort?.send({ 'requestId': message['requestId'], 'result': utxos, diff --git a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart index e86624291..e4cd8a101 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart @@ -174,7 +174,7 @@ class PayjoinReceiverWorker { final listUnspent = await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs); final unspent = listUnspent as List; - if (unspent.isEmpty) throw Exception('No unspent outputs available'); + if (unspent.isEmpty) throw RecoverableError('No unspent outputs available'); final selectedUtxo = await _inputPairFromUtxo(unspent[0]); final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]); diff --git a/cw_bitcoin/lib/payjoin/storage.dart b/cw_bitcoin/lib/payjoin/storage.dart index 9c1c83253..5fb9d5716 100644 --- a/cw_bitcoin/lib/payjoin/storage.dart +++ b/cw_bitcoin/lib/payjoin/storage.dart @@ -23,6 +23,14 @@ class PayjoinStorage { ), ); + PayjoinSession? getUnusedActiveReceiverSession(String walletId) => + _payjoinSessionSources.values + .where((session) => + session.walletId == walletId && + session.status == PayjoinSessionStatus.created.name && + !session.isSenderSession) + .firstOrNull; + Future markReceiverSessionComplete( String sessionId, String txId, String amount) async { final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; @@ -76,10 +84,11 @@ class PayjoinStorage { await session.save(); } - Future markSenderSessionUnrecoverable(String pjUrl) async { + Future markSenderSessionUnrecoverable(String pjUrl, String reason) async { final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; session.status = PayjoinSessionStatus.unrecoverable.name; + session.error = reason; await session.save(); } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 6a0be67e9..08f1a25ee 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -65,7 +65,7 @@ void startCurrentWalletChangeReaction( final node = settingsStore.getCurrentNode(wallet.type); - startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); + startWalletSyncStatusChangeReaction(wallet, settingsStore); startCheckConnectionReaction(wallet, settingsStore); await Future.delayed(Duration.zero); diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index bf2e8ed0d..bbf4f44fe 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -1,5 +1,7 @@ -import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/wallet_base.dart'; @@ -12,7 +14,7 @@ ReactionDisposer? _onWalletSyncStatusChangeReaction; void startWalletSyncStatusChangeReaction( WalletBase, TransactionInfo> wallet, - FiatConversionStore fiatConversionStore) { + SettingsStore settingsStore) { _onWalletSyncStatusChangeReaction?.reaction.dispose(); _onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async { try { @@ -25,6 +27,12 @@ void startWalletSyncStatusChangeReaction( if (status is SyncedSyncStatus || status is FailedSyncStatus) { await WakelockPlus.disable(); } + + if (status is SyncedSyncStatus && + wallet.type == WalletType.bitcoin && + settingsStore.usePayjoin) { + bitcoin!.resumePayjoinSessions(wallet); + } } catch (e) { printV(e.toString()); } diff --git a/lib/view_model/dashboard/payjoin_transaction_list_item.dart b/lib/view_model/dashboard/payjoin_transaction_list_item.dart index 93ed094a9..9605ab8fa 100644 --- a/lib/view_model/dashboard/payjoin_transaction_list_item.dart +++ b/lib/view_model/dashboard/payjoin_transaction_list_item.dart @@ -20,9 +20,8 @@ class PayjoinTransactionListItem extends ActionListItem { String get status { switch (session.status) { case 'success': - if (transaction?.isPending == true) - return S.current.payjoin_request_awaiting_tx; - return S.current.successful; + if (transaction?.isPending == false) return S.current.successful; + return S.current.payjoin_request_awaiting_tx; case 'inProgress': return S.current.payjoin_request_in_progress; case 'unrecoverable': diff --git a/lib/view_model/payjoin_details_view_model.dart b/lib/view_model/payjoin_details_view_model.dart index 47e6d2c5d..b19207373 100644 --- a/lib/view_model/payjoin_details_view_model.dart +++ b/lib/view_model/payjoin_details_view_model.dart @@ -69,7 +69,7 @@ abstract class PayjoinDetailsViewModelBase with Store { title: S.current.error, value: payjoinSession.error!, ), - if (payjoinSession.txId?.isNotEmpty == true) + if (payjoinSession.txId?.isNotEmpty == true && transactionInfo != null) StandartListItem( title: S.current.transaction_details_transaction_id, value: payjoinSession.txId!, @@ -107,9 +107,8 @@ abstract class PayjoinDetailsViewModelBase with Store { String _getStatusString() { switch (payjoinSession.status) { case 'success': - if (transactionInfo?.isPending == true) - return S.current.payjoin_request_awaiting_tx; - return S.current.successful; + if (transactionInfo?.isPending == false) return S.current.successful; + return S.current.payjoin_request_awaiting_tx; case 'inProgress': return S.current.payjoin_request_in_progress; case 'unrecoverable': From 18c2ba93668dce0239859809a141493f89006906 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 19 Jun 2025 19:00:16 +0200 Subject: [PATCH 17/29] CW-1090-ledger-issues (#2314) * build: bump ledger_flutter_plus dependencies * fix: handle connection errors occurring during connecting to a ledger device * fix: enhance ledger error handling and improve wallet connection reliability * fix: enhance ledger error handling and improve wallet connection reliability * feat: add localized strings for "Try again" and update Ledger error handling on auth * fix: handle rethrow behavior in onAuthenticationStateChange * fix: improve Ledger error handling and refine error code interpretation * fix: refine Ledger error code interpretation and enhance error handling [skip-ci] * add missing ledger error codes --------- Co-authored-by: Omar Hatem --- cw_evm/pubspec.yaml | 1 + lib/core/wallet_loading_service.dart | 3 + .../on_authentication_state_change.dart | 50 +++++++++++++--- .../connect_device/connect_device_page.dart | 5 +- lib/utils/exception_handler.dart | 57 ++++++++++++++++++- .../hardware_wallet/ledger_view_model.dart | 38 +++++++------ .../wallet_hardware_restore_view_model.dart | 4 +- pubspec_base.yaml | 4 +- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + 36 files changed, 159 insertions(+), 31 deletions(-) diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml index 2e37a3b6e..80788ca06 100644 --- a/cw_evm/pubspec.yaml +++ b/cw_evm/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: git: url: https://github.com/cake-tech/ledger-flutter-plus-plugins path: packages/ledger-ethereum + ref: f4761cd5171d4c1e2e42fd3298261650539fb2db dependency_overrides: web3dart: diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 382b1d6c2..f16bf7e14 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -73,8 +73,11 @@ class WalletLoadingService { return wallet; } catch (error, stack) { await ExceptionHandler.resetLastPopupDate(); + final isLedgerError = await ExceptionHandler.isLedgerError(error); + if (isLedgerError) rethrow; await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack)); + // try fetching the seeds of the corrupted wallet to show it to the user String corruptedWalletsSeeds = "Corrupted wallets seeds (if retrievable, empty otherwise):"; try { diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 46ac481bb..8cde67731 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; @@ -63,14 +64,49 @@ void startAuthenticationStateChange( monero!.setGlobalLedgerConnection(ledgerVM.connection); showPopUp( context: context, - builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).proceed_on_device, - alertContent: S.of(context).proceed_on_device_description, - buttonText: S.of(context).cancel, - alertBarrierDismissible: false, - buttonAction: () => Navigator.of(context).pop()), + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).proceed_on_device, + alertContent: S.of(context).proceed_on_device_description, + buttonText: S.of(context).cancel, + alertBarrierDismissible: false, + buttonAction: () => Navigator.of(context).pop(), + ), ); - await loadCurrentWallet(); + bool tryOpening = true; + while (tryOpening) { + try { + await loadCurrentWallet(); + tryOpening = false; + } on Exception catch (e) { + final errorCode = RegExp(r'0x\S*?(?= )').firstMatch( + e.toString()); + + final errorMessage = ledgerVM.interpretErrorCode( + errorCode?.group(0).toString().replaceAll("0x", "") ?? + ""); + if (errorMessage != null) { + await showPopUp( + context: context, + builder: (context) => AlertWithTwoActions( + alertTitle: "Ledger Error", + alertContent: errorMessage, + leftButtonText: S.of(context).try_again, + alertBarrierDismissible: false, + actionLeftButton: () => Navigator.of(context).pop(), + rightButtonText: S.of(context).cancel, + actionRightButton: () { + tryOpening = false; + Navigator.of(context).pop(); + }, + ), + ); + } else { + tryOpening = false; + rethrow; + } + } + } + getIt.get().showNext(); await navigatorKey.currentState! .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index 87c8e29a9..9e332369e 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -165,8 +165,9 @@ class ConnectDevicePageBodyState extends State { } Future _connectToDevice(LedgerDevice device) async { - await widget.ledgerVM.connectLedger(device, widget.walletType); - widget.onConnectDevice(context, widget.ledgerVM); + final isConnected = + await widget.ledgerVM.connectLedger(device, widget.walletType); + if (isConnected) widget.onConnectDevice(context, widget.ledgerVM); } String _getDeviceTileLeading(LedgerDeviceType deviceInfo) { diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 954d4f532..a6d3774ce 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -4,8 +4,10 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/utils/package_info.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/root_dir.dart'; @@ -15,7 +17,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mailer/flutter_mailer.dart'; -import 'package:cake_wallet/utils/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ExceptionHandler { @@ -113,6 +114,8 @@ class ExceptionHandler { } static Future onError(FlutterErrorDetails errorDetails) async { + if (await onLedgerError(errorDetails)) return; + if (kDebugMode || kProfileMode) { FlutterError.presentError(errorDetails); printV(errorDetails.toString()); @@ -183,6 +186,57 @@ class ExceptionHandler { _hasError = false; } + static const List _ledgerErrors = [ + 'Wrong Device Status', + 'PlatformException(133, Failed to write: (Unknown Error: 133), null, null)', + 'PlatformException(IllegalArgument, Unknown deviceId:', + 'ServiceNotSupportedException(ConnectionType.ble, Required service not supported. Write characteristic: false, Notify characteristic: false)', + 'Exception: 6e01', // Wrong App + 'Exception: 6d02', + 'Exception: 6511', + 'Exception: 6e00', + 'Exception: 6985', + 'Exception: 5515', + ]; + + static bool isLedgerError(Object exception) => + _ledgerErrors.any((element) => exception.toString().contains(element)); + + static Future onLedgerError(FlutterErrorDetails errorDetails) async { + if (!isLedgerError(errorDetails.exception)) return false; + + String? interpretErrorCode(String errorCode) { + if (errorCode.contains("6985")) { + return S.current.ledger_error_tx_rejected_by_user; + } else if (errorCode.contains("5515")) { + return S.current.ledger_error_device_locked; + } else + if (["6e01", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) { + return S.current.ledger_error_wrong_app; + } + return null; + } + + printV(errorDetails.exception); + + if (navigatorKey.currentContext != null) { + await showPopUp( + context: navigatorKey.currentContext!, + builder: (context) => AlertWithOneAction( + alertTitle: "Ledger Error", + alertContent: + interpretErrorCode(errorDetails.exception.toString()) ?? + S.of(context).ledger_connection_error, + buttonText: S.of(context).close, + buttonAction: () => Navigator.of(context).pop(), + ), + ); + } + + _hasError = false; + return true; + } + /// Ignore User related errors or system errors static bool _ignoreError(String error) => _ignoredErrors.any((element) => error.contains(element)); @@ -227,6 +281,7 @@ class ExceptionHandler { "core/auth_service.dart:64", "core/key_service.dart:14", "core/wallet_loading_service.dart:139", + "Wrong Device Status: 0x5515 (UNKNOWN)", ]; static Future _addDeviceInfo(File file) async { diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index f555da0cf..132a5deb9 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -96,7 +96,8 @@ abstract class LedgerViewModelBase with Store { if (!Platform.isIOS) await ledgerPlusUSB.stopScanning(); } - Future connectLedger(sdk.LedgerDevice device, WalletType type) async { + Future connectLedger(sdk.LedgerDevice device, WalletType type) async { + if (_isConnecting) return false; _isConnecting = true; _connectingWalletType = type; if (isConnected) { @@ -110,17 +111,25 @@ abstract class LedgerViewModelBase with Store { : ledgerPlusUSB; if (_connectionChangeSubscription == null) { - _connectionChangeSubscription = - ledger.deviceStateChanges.listen(_connectionChangeListener); + _connectionChangeSubscription = ledger + .deviceStateChanges(device.id) + .listen(_connectionChangeListener); } - _connection = await ledger.connect(device); + try { + _connection = await ledger.connect(device); + _isConnecting = false; + return true; + } catch (e) { + printV(e); + } _isConnecting = false; + return false; } StreamSubscription? _connectionChangeSubscription; sdk.LedgerConnection? _connection; - bool _isConnecting = true; + bool _isConnecting = false; WalletType? _connectingWalletType; void _connectionChangeListener(sdk.BleConnectionState event) { @@ -168,17 +177,14 @@ abstract class LedgerViewModelBase with Store { } String? interpretErrorCode(String errorCode) { - switch (errorCode) { - case "6985": - return S.current.ledger_error_tx_rejected_by_user; - case "5515": - return S.current.ledger_error_device_locked; - case "6d02": // UNKNOWN_APDU - case "6511": - case "6e00": - return S.current.ledger_error_wrong_app; - default: - return null; + if (errorCode.contains("6985")) { + return S.current.ledger_error_tx_rejected_by_user; + } else if (errorCode.contains("5515")) { + return S.current.ledger_error_device_locked; + } else + if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) { + return S.current.ledger_error_wrong_app; } + return null; } } diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 541e169d0..0cfc18c16 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -80,11 +80,9 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with availableAccounts.addAll(accounts); _nextIndex += limit; - // } on LedgerException catch (e) { - // error = ledgerViewModel.interpretErrorCode(e.errorCode.toRadixString(16)); } catch (e) { printV(e); - error = S.current.ledger_connection_error; + error = ledgerViewModel.interpretErrorCode(e.toString()) ?? S.current.ledger_connection_error; } isLoadingMoreAccounts = false; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 21166bd74..e11703607 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -110,7 +110,7 @@ dependencies: ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus - ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76 + ref: 60817d4b20144f9da9029f5034790272795b9d38 hashlib: ^1.19.2 on_chain: git: @@ -165,7 +165,7 @@ dependency_overrides: ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus - ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76 + ref: 60817d4b20144f9da9029f5034790272795b9d38 web_socket_channel: ^3.0.2 freezed_annotation: 2.4.4 diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index a30865b76..f12171f31 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -979,6 +979,7 @@ "transport_type": "نوع النقل", "trongrid_history": "تاريخ ترونغريد", "trusted": "موثوق به", + "try_again": "حاول ثانية", "tx_commit_exception_no_dust_on_change": "يتم رفض المعاملة مع هذا المبلغ. باستخدام هذه العملات المعدنية ، يمكنك إرسال ${min} دون تغيير أو ${max} الذي يعيد التغيير.", "tx_commit_failed": "فشل ارتكاب المعاملة. يرجى الاتصال بالدعم.", "tx_commit_failed_no_peers": "فشل المعاملة في البث ، يرجى المحاولة مرة أخرى في ثانية أو نحو ذلك", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index f0b992160..e1d364b95 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -979,6 +979,7 @@ "transport_type": "Тип транспорт", "trongrid_history": "Trongrid History", "trusted": "Надежден", + "try_again": "Опитайте отново", "tx_commit_exception_no_dust_on_change": "Сделката се отхвърля с тази сума. С тези монети можете да изпратите ${min} без промяна или ${max}, която връща промяна.", "tx_commit_failed": "Компетацията на транзакцията не успя. Моля, свържете се с поддръжката.", "tx_commit_failed_no_peers": "Сделката не успя да излъчи, моля, опитайте отново след секунда или така", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index e4b59562e..0a6693316 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -979,6 +979,7 @@ "transport_type": "Typ transportu", "trongrid_history": "Trongridní historie", "trusted": "Důvěřovat", + "try_again": "Zkuste to znovu", "tx_commit_exception_no_dust_on_change": "Transakce je zamítnuta s touto částkou. S těmito mincemi můžete odeslat ${min} bez změny nebo ${max}, které se vrátí změna.", "tx_commit_failed": "Transakce COMPORT selhala. Kontaktujte prosím podporu.", "tx_commit_failed_no_peers": "Transakce se nepodařilo vysílat, zkuste to prosím znovu za vteřinu", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 7e3f72e2a..5e9c2209c 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -980,6 +980,7 @@ "transport_type": "Transporttyp", "trongrid_history": "Trongrid-Historie", "trusted": "Vertrauenswürdige", + "try_again": "Erneut versuchen", "tx_commit_exception_no_dust_on_change": "Die Transaktion wird diesen Betrag abgelehnt. Mit diesen Münzen können Sie ${min} ohne Veränderung oder ${max} senden, die Änderungen zurückgeben.", "tx_commit_failed": "Transaktionsausschüsse ist fehlgeschlagen. Bitte wenden Sie sich an Support.", "tx_commit_failed_no_peers": "Transaktion konnte nicht übertragen werden. Bitte versuchen Sie es in einer Sekunde oder so erneut", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index a841c3041..ad4894238 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -980,6 +980,7 @@ "transport_type": "Transport Type", "trongrid_history": "TronGrid history", "trusted": "Trusted", + "try_again": "Try again", "tx_commit_exception_no_dust_on_change": "The transaction is rejected with this amount. With these coins you can send ${min} without change or ${max} that returns change.", "tx_commit_failed": "Transaction commit failed. Please contact support.", "tx_commit_failed_no_peers": "Transaction failed to broadcast, please try again in a second or so", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 17687e7c0..3f8657ec4 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -980,6 +980,7 @@ "transport_type": "Tipo de transporte", "trongrid_history": "Historia trongrid", "trusted": "de confianza", + "try_again": "Intentar otra vez", "tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.", "tx_commit_failed": "La confirmación de transacción falló. Ponte en contacto con el soporte.", "tx_commit_failed_no_peers": "La transacción no se transmitió, intenta nuevamente en un segundo más o menos", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index e6b942f5c..ba3ae4859 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -979,6 +979,7 @@ "transport_type": "Type de transport", "trongrid_history": "Histoire de la trongride", "trusted": "de confiance", + "try_again": "Essayer à nouveau", "tx_commit_exception_no_dust_on_change": "La transaction est rejetée avec ce montant. Avec ces pièces, vous pouvez envoyer ${min} sans changement ou ${max} qui renvoie le changement.", "tx_commit_failed": "La validation de la transaction a échoué. Veuillez contacter l'assistance.", "tx_commit_failed_no_peers": "La transaction n'a pas été diffusée, veuillez réessayer dans une seconde environ", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index e38d03602..ebd236332 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -981,6 +981,7 @@ "transport_type": "Nau'in sufuri", "trongrid_history": "Tarihin Trongrid", "trusted": "Amintacce", + "try_again": "Gwada kuma", "tx_commit_exception_no_dust_on_change": "An ƙi ma'amala da wannan adadin. Tare da waɗannan tsabar kudi Zaka iya aika ${min}, ba tare da canji ba ko ${max} wanda ya dawo canzawa.", "tx_commit_failed": "Ma'amala ya kasa. Da fatan za a tuntuɓi goyan baya.", "tx_commit_failed_no_peers": "Kasuwanci ya kasa watsa, don Allah sake gwadawa a cikin na biyu ko", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index e9c3bca17..2e71b6685 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -981,6 +981,7 @@ "transport_type": "परिवहन प्रकार", "trongrid_history": "ट्रॉन्ग्रिड का इतिहास", "trusted": "भरोसा", + "try_again": "पुनः प्रयास करें", "tx_commit_exception_no_dust_on_change": "लेनदेन को इस राशि से खारिज कर दिया जाता है। इन सिक्कों के साथ आप चेंज या ${min} के बिना ${max} को भेज सकते हैं जो परिवर्तन लौटाता है।", "tx_commit_failed": "लेन -देन प्रतिबद्ध विफल। कृपया संपर्क समर्थन करें।", "tx_commit_failed_no_peers": "लेन -देन प्रसारित करने में विफल रहा, कृपया एक या दो सेकंड में पुनः प्रयास करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 3f52f5b66..b0aa12937 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -979,6 +979,7 @@ "transport_type": "Transportni tip", "trongrid_history": "Povijest Trongrida", "trusted": "vjerovao", + "try_again": "Pokušajte ponovo", "tx_commit_exception_no_dust_on_change": "Transakcija se odbija s tim iznosom. Pomoću ovih kovanica možete poslati ${min} bez promjene ili ${max} koja vraća promjenu.", "tx_commit_failed": "Obveza transakcije nije uspjela. Molimo kontaktirajte podršku.", "tx_commit_failed_no_peers": "Transakcija nije uspjela emitirati, pokušajte ponovo u sekundi ili tako", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index d1d661e7c..cafabe5a6 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -977,6 +977,7 @@ "transport_type": "Տրանսպորտի տեսակը", "trongrid_history": "TronGrid պատմություն", "trusted": "Վստահելի", + "try_again": "Կրկին փորձեք", "tx_commit_exception_no_dust_on_change": "Փոխանցումը մերժվել է այս գումարով: Այս արժույթներով կարող եք ուղարկել ${min} առանց փոփոխության կամ ${max} որը վերադարձնում է փոփոխությունը", "tx_commit_failed": "Փոխանցումը ձախողվել է: Խնդրում ենք դիմել աջակցությանը", "tx_commit_failed_no_peers": "Գործարքը չի հաջողվել հեռարձակել, խնդրում ենք կրկին փորձել մեկ վայրկյանում", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index eaccd3d99..e726ffb3c 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -982,6 +982,7 @@ "transport_type": "Jenis transportasi", "trongrid_history": "Sejarah Trongrid", "trusted": "Dipercayai", + "try_again": "Coba lagi", "tx_commit_exception_no_dust_on_change": "Transaksi ditolak dengan jumlah ini. Dengan koin ini Anda dapat mengirim ${min} tanpa perubahan atau ${max} yang mengembalikan perubahan.", "tx_commit_failed": "Transaksi Gagal. Silakan hubungi Dukungan.", "tx_commit_failed_no_peers": "Transaksi gagal untuk disiarkan, silakan coba lagi sebentar lagi", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 65c4085d1..4e94c349f 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -980,6 +980,7 @@ "transport_type": "Tipo di trasporto", "trongrid_history": "Cronologia TronGrid", "trusted": "Fidato", + "try_again": "Riprova", "tx_commit_exception_no_dust_on_change": "La transazione viene respinta con questo importo. Con queste monete è possibile inviare ${min} senza modifiche o ${max} che restituisce il cambiamento.", "tx_commit_failed": "Commit di transazione non riuscita. Si prega di contattare il supporto.", "tx_commit_failed_no_peers": "Errore nella trasmissione della transazione, si prega di provare nuovamente", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index f638c1850..960634e27 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -980,6 +980,7 @@ "transport_type": "輸送タイプ", "trongrid_history": "トロンリッドの歴史", "trusted": "信頼できる", + "try_again": "もう一度やり直してください", "tx_commit_exception_no_dust_on_change": "この金額ではトランザクションは拒否されます。 これらのコインを使用すると、おつりなしの ${min} またはおつりを返す ${max} を送信できます。", "tx_commit_failed": "トランザクションコミットは失敗しました。サポートに連絡してください。", "tx_commit_failed_no_peers": "トランザクションはブロードキャストに失敗しました。一瞬かそこらで再試行してください", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 1c0a4cee7..e107e1a29 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -980,6 +980,7 @@ "transport_type": "전송 유형", "trongrid_history": "TronGrid 내역", "trusted": "신뢰됨", + "try_again": "다시 시도하십시오", "tx_commit_exception_no_dust_on_change": "이 금액으로는 트랜잭션이 거부됩니다. 이 코인으로는 잔돈 없이 ${min}을(를) 보내거나 잔돈이 반환되는 ${max}을(를) 보낼 수 있습니다.", "tx_commit_failed": "트랜잭션 커밋 실패. 지원팀에 문의하세요.", "tx_commit_failed_no_peers": "트랜잭션 전파 실패. 잠시 후 다시 시도하세요.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 6727c7fcf..d9f3c46da 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -979,6 +979,7 @@ "transport_type": "သယ်ယူပို့ဆောင်ရေးအမျိုးအစား", "trongrid_history": "Trongrid သမိုင်း", "trusted": "ယုံတယ်။", + "try_again": "ထပ်ကြိုးစားပါ", "tx_commit_exception_no_dust_on_change": "အဆိုပါငွေပေးငွေယူကဒီပမာဏနှင့်အတူပယ်ချခံရသည်။ ဤဒင်္ဂါးပြားများနှင့်အတူပြောင်းလဲမှုကိုပြန်လည်ပြောင်းလဲခြင်းသို့မဟုတ် ${min} မပါဘဲ ${max} ပေးပို့နိုင်သည်။", "tx_commit_failed": "ငွေပေးငွေယူကျူးလွန်မှုပျက်ကွက်။ ကျေးဇူးပြုပြီးပံ့ပိုးမှုဆက်သွယ်ပါ။", "tx_commit_failed_no_peers": "ငွေပေးငွေယူထုတ်လွှင့်ရန်ပျက်ကွက်ပါက ကျေးဇူးပြု. ဒုတိယသို့မဟုတ်ထိုအတိုင်းထပ်မံကြိုးစားပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 4f925bc74..756f0b311 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -979,6 +979,7 @@ "transport_type": "Transporttype", "trongrid_history": "Trongrid geschiedenis", "trusted": "vertrouwd", + "try_again": "Probeer het opnieuw", "tx_commit_exception_no_dust_on_change": "De transactie wordt afgewezen met dit bedrag. Met deze munten kunt u ${min} verzenden zonder verandering of ${max} die wijziging retourneert.", "tx_commit_failed": "Transactiebewissing is mislukt. Neem contact op met de ondersteuning.", "tx_commit_failed_no_peers": "De transactie is niet uitgezonden, probeer het opnieuw binnen een seconde of zo", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index baf0f2502..78a1af410 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -979,6 +979,7 @@ "transport_type": "Typ transportu", "trongrid_history": "Historia Trongrida", "trusted": "Zaufany", + "try_again": "Spróbuj ponownie", "tx_commit_exception_no_dust_on_change": "Transakcja została odrzucana z tą kwotą. Za pomocą tych monet możesz wysłać ${min} bez reszty lub ${max}, które zwrócą resztę.", "tx_commit_failed": "Zatwierdzenie transakcji nie powiodło się. Skontaktuj się z obsługą.", "tx_commit_failed_no_peers": "Transakcja nie była transmitowana, spróbuj ponownie za około sekundę", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 375996c99..64c54916a 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -981,6 +981,7 @@ "transport_type": "Tipo de transporte", "trongrid_history": "História de Trongrid", "trusted": "confiável", + "try_again": "Tente novamente", "tx_commit_exception_no_dust_on_change": "A transação é rejeitada com esse valor. Com essas moedas, você pode enviar ${min} sem alteração ou ${max} que retorna alterações.", "tx_commit_failed": "A confirmação da transação falhou. Entre em contato com o suporte.", "tx_commit_failed_no_peers": "A transação não foi transmitida, tente novamente em um segundo", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 800201b27..7f0ce362a 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -980,6 +980,7 @@ "transport_type": "Транспортный тип", "trongrid_history": "История Тронгрида", "trusted": "доверенный", + "try_again": "Попробуйте еще раз", "tx_commit_exception_no_dust_on_change": "Транзакция отклоняется с этой суммой. С этими монетами вы можете отправлять ${min} без изменения или ${max}, которые возвращают изменение.", "tx_commit_failed": "Комплект транзакции не удался. Пожалуйста, свяжитесь с поддержкой.", "tx_commit_failed_no_peers": "Транзакция не смогла передать, попробуйте еще раз через секунду или около того", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index bfaa92b08..9908f93a0 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -979,6 +979,7 @@ "transport_type": "ประเภทการขนส่ง", "trongrid_history": "ประวัติศาสตร์ Trongrid", "trusted": "มั่นคง", + "try_again": "ลองอีกครั้ง", "tx_commit_exception_no_dust_on_change": "ธุรกรรมถูกปฏิเสธด้วยจำนวนเงินนี้ ด้วยเหรียญเหล่านี้คุณสามารถส่ง ${min} โดยไม่ต้องเปลี่ยนแปลงหรือ ${max} ที่ส่งคืนการเปลี่ยนแปลง", "tx_commit_failed": "การทำธุรกรรมล้มเหลว กรุณาติดต่อฝ่ายสนับสนุน", "tx_commit_failed_no_peers": "การทำธุรกรรมล้มเหลวในการออกอากาศโปรดลองอีกครั้งในวินาทีหรือมากกว่านั้น", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 8e571c3cb..9ad025aa7 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -979,6 +979,7 @@ "transport_type": "Uri ng transportasyon", "trongrid_history": "Kasaysayan ng TronGrid", "trusted": "Pinagkakatiwalaan", + "try_again": "Subukang muli", "tx_commit_exception_no_dust_on_change": "Ang transaksyon ay tinanggihan sa halagang ito. Sa mga barya na ito maaari kang magpadala ng ${min} nang walang sukli o ${max} na nagbabalik ng sukli.", "tx_commit_failed": "Nabigo ang transaksyon. Mangyaring makipag-ugnay sa suporta.", "tx_commit_failed_no_peers": "Nabigo ang transaksyon na mag -broadcast, mangyaring subukang muli sa isang segundo o higit pa", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 9a9690142..add8ee22f 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -979,6 +979,7 @@ "transport_type": "Taşıma tipi", "trongrid_history": "Trongrid tarihi", "trusted": "Güvenilir", + "try_again": "Tekrar deneyin", "tx_commit_exception_no_dust_on_change": "İşlem bu miktarla reddedilir. Bu madeni paralarla değişiklik yapmadan ${min} veya değişikliği döndüren ${max} gönderebilirsiniz.", "tx_commit_failed": "İşlem taahhüdü başarısız oldu. Lütfen Destek ile iletişime geçin.", "tx_commit_failed_no_peers": "İşlem yayın yapamadı, lütfen bir saniye içinde tekrar deneyin", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index e36599a0c..07ed5d532 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -980,6 +980,7 @@ "transport_type": "Транспортний тип", "trongrid_history": "Тронгрідська історія", "trusted": "довіряють", + "try_again": "Спробуйте ще раз", "tx_commit_exception_no_dust_on_change": "Транзакція відхилена цією сумою. За допомогою цих монет ви можете надіслати ${min} без змін або ${max}, що повертає зміни.", "tx_commit_failed": "Транзакційна комісія не вдалося. Будь ласка, зв'яжіться з підтримкою.", "tx_commit_failed_no_peers": "Транзакція не вдалося транслювати, спробуйте ще раз за секунду або близько того", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 0ce1b4758..7240db6b0 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -981,6 +981,7 @@ "transport_type": "ٹرانسپورٹ کی قسم", "trongrid_history": "ٹرانگریڈ ہسٹری", "trusted": "قابل اعتماد", + "try_again": "دوبارہ کوشش کریں", "tx_commit_exception_no_dust_on_change": "اس رقم سے لین دین کو مسترد کردیا گیا ہے۔ ان سککوں کے ذریعہ آپ بغیر کسی تبدیلی کے ${min} یا ${max} بھیج سکتے ہیں جو لوٹتے ہیں۔", "tx_commit_failed": "ٹرانزیکشن کمٹ ناکام ہوگیا۔ براہ کرم سپورٹ سے رابطہ کریں۔", "tx_commit_failed_no_peers": "ٹرانزیکشن نشر کرنے میں ناکام ، براہ کرم ایک سیکنڈ یا اس میں دوبارہ کوشش کریں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 61cadb10b..3dd184ddd 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -976,6 +976,7 @@ "transport_type": "Loại vận chuyển", "trongrid_history": "Lịch sử TronGrid", "trusted": "Đã tin cậy", + "try_again": "Hãy thử lại", "tx_commit_exception_no_dust_on_change": "Giao dịch bị từ chối với số tiền này. Với số tiền này bạn có thể gửi ${min} mà không cần đổi tiền lẻ hoặc ${max} trả lại tiền lẻ.", "tx_commit_failed": "Giao dịch không thành công. Vui lòng liên hệ với hỗ trợ.", "tx_commit_failed_no_peers": "Giao dịch không phát sóng, vui lòng thử lại trong một giây hoặc lâu hơn", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 761667b17..0a3e382e7 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -980,6 +980,7 @@ "transport_type": "Iru irinna", "trongrid_history": "Itan Trongrid", "trusted": "A ti fọkàn ẹ̀ tán", + "try_again": "Gbiyanju lẹẹkansi", "tx_commit_exception_no_dust_on_change": "Iṣowo naa ti kọ pẹlu iye yii. Pẹlu awọn owó wọnyi o le firanṣẹ ${min} laisi ayipada tabi ${max} ni iyipada iyipada.", "tx_commit_failed": "Idunadura iṣowo kuna. Jọwọ kan si atilẹyin.", "tx_commit_failed_no_peers": "Idunadura kuna lati wa igbohungbe, jọwọ gbiyanju lẹẹkansi ni iṣẹju keji tabi bẹẹ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 5831a51c0..2f02673b7 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -979,6 +979,7 @@ "transport_type": "运输类型", "trongrid_history": "Trongrid历史", "trusted": "值得信赖", + "try_again": "再试一次", "tx_commit_exception_no_dust_on_change": "交易被此金额拒绝。使用这些硬币,您可以发送${min}无需更改或返回${max}的变化。", "tx_commit_failed": "交易承诺失败。请联系支持。", "tx_commit_failed_no_peers": "交易无法广播,请在一秒钟左右的时间内重试", From 5082dc20f33ef8472bea8c5d904d2eff14dc334b Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 20 Jun 2025 21:56:18 +0200 Subject: [PATCH 18/29] CW-519 Enable built-in Tor (#1950) * tor wip * Enable tor on iOS * Prevent app lag when node is exceptionally slow (usually over tor) * fix: logic in daemonBlockchainHeight refresh fix: storing tor state * Pin ledger_flutter_plus dependency to fix builds * bump arti version * wip * add single httpclient * route everything I was able to catch trough the built-in tor node * Enable proxy for http.Client [run tests] * add tor proxy support to cw_evm, cw_tron and cw_polygon [run tests] * remove log pollution, cleanup [skip slack] * fix tests not working in latest main [skip slack] [run tests] * remove cw_wownero import * fix build issues * migrate all remaining calls to use ProxyWrapper add a CI action to enforce using ProxyWrapper instead of http/http.dart to prevent leaks * fix tor background sync (will work on test builds after #2142 is merged and this PR is rebased on top) * wip [skip ci] * relicense to GPLv3 add socks5 license, build fixes * use ProxyWrapper instead of http in robinhood * Revert "relicense to GPLv3" * feat(cw_bitcoin): support socks proxy and CakeTor * fix(tor): migrate OCP and EVM over to ProxyWrapper() * chore: cleanup fix: show tor loading screen when app is starting * fix: tor switch properly dismisses fullscreen loading dialog fix: connectToNode after tor startup on app start * fix(tor): status check for xmr/wow/zano * fix(tor): onramper request fix * fix(api): ServicesResponse is now being cached and doesn't fetch data everytime DashboardViewModel is being rebuilt fix(tor): do not fallback to clearnet when tor failed. fix(tor): do not leak connections during app startup chore: refactor bootstrap() function to be separated into bootstrapOffline and bootstrapOnline fix(cw_bitcoin): migrate payjoin to use ProxyWrapper * [skip ci] remove print * address comments from review * fix: derusting tor implementation Instead of rust-based Arti I've moved back to the OG C++ tor implementation. This fixed all issues we had with Tor. - onion services now work - all requests are going through without random errors - we don't have to navigate a maze of multiple forks of multiple packages - fully working `torrc` config file (probably will be needed for Tari). - logging for Tor client - and so on. feat: network logging tab feat: use built-in proxy on Tails - this should resolve all issues for Tails users (needs testing though) * fix conflicts with main bump https to fix build issue relax store() call * fix(cw_wownero): tor connection fix(tor): connection issues * fix(cw_evm): add missing chainId fix(cw_core): solana rpc fix * feat: mark tor as experimental fix: drop anonpay onion authority fix: drop fiatapi onion authority fix: drop trocador onion authority fix: disable networkimage when tor is enabled fix: handle cakepay errors gracefully * fix re-formatting [skip ci] * changes from review * Delete android/.kotlin/sessions/kotlin-compiler-2468481326039681181.salive * fix missing imports * Update pubspec_base.yaml --------- Co-authored-by: OmarHatem --- .github/workflows/no_http_imports.yaml | 21 + .github/workflows/no_print_in_dart.yaml | 2 +- assets/images/tor_logo.svg | 76 +++ cw_bitcoin/lib/electrum.dart | 18 +- cw_bitcoin/lib/electrum_wallet.dart | 52 +- .../lib/payjoin/payjoin_receive_worker.dart | 28 +- .../lib/payjoin/payjoin_send_worker.dart | 20 +- cw_bitcoin/pubspec.lock | 33 +- cw_bitcoin/pubspec.yaml | 4 + cw_core/lib/get_height_by_date.dart | 22 +- cw_core/lib/node.dart | 71 ++- cw_core/lib/solana_rpc_http_service.dart | 15 +- cw_core/lib/utils/proxy_logger/abstract.dart | 29 ++ .../proxy_logger/memory_proxy_logger.dart | 63 +++ .../lib/utils/proxy_logger/silent_logger.dart | 17 + cw_core/lib/utils/proxy_socket/abstract.dart | 47 ++ cw_core/lib/utils/proxy_socket/insecure.dart | 34 ++ cw_core/lib/utils/proxy_socket/secure.dart | 34 ++ cw_core/lib/utils/proxy_socket/socks.dart | 36 ++ cw_core/lib/utils/proxy_wrapper.dart | 447 ++++++++++++++++++ cw_core/lib/utils/tor/abstract.dart | 38 ++ cw_core/lib/utils/tor/android.dart | 73 +++ cw_core/lib/utils/tor/disabled.dart | 21 + cw_core/lib/utils/tor/tails.dart | 21 + cw_core/pubspec.lock | 35 +- cw_core/pubspec.yaml | 17 +- cw_decred/pubspec.lock | 29 +- cw_ethereum/lib/ethereum_client.dart | 4 +- cw_evm/lib/evm_chain_client.dart | 8 +- cw_monero/lib/api/wallet.dart | 23 +- cw_monero/lib/monero_wallet.dart | 11 +- cw_monero/pubspec.lock | 43 +- cw_nano/lib/nano_client.dart | 99 ++-- cw_nano/pubspec.lock | 33 +- cw_polygon/lib/polygon_client.dart | 4 +- cw_solana/lib/solana_client.dart | 54 ++- cw_tron/lib/tron_client.dart | 9 +- cw_tron/lib/tron_http_provider.dart | 21 +- cw_wownero/lib/api/wallet.dart | 13 +- cw_wownero/lib/wownero_wallet.dart | 11 +- cw_wownero/pubspec.lock | 33 +- cw_zano/pubspec.lock | 29 +- .../robots/wallet_keys_robot.dart | 42 +- lib/anonpay/anonpay_api.dart | 47 +- lib/anypay/anypay_api.dart | 17 +- lib/buy/dfx/dfx_buy_provider.dart | 24 +- lib/buy/kryptonim/kryptonim.dart | 9 +- lib/buy/meld/meld_buy_provider.dart | 14 +- lib/buy/moonpay/moonpay_provider.dart | 24 +- lib/buy/onramper/onramper_buy_provider.dart | 20 +- lib/buy/robinhood/robinhood_buy_provider.dart | 18 +- lib/buy/wyre/wyre_buy_provider.dart | 42 +- lib/cake_pay/cake_pay_api.dart | 42 +- lib/core/background_sync.dart | 8 +- lib/core/fiat_conversion_service.dart | 34 +- .../open_cryptopay_service.dart | 18 +- lib/core/yat_service.dart | 7 +- lib/di.dart | 11 +- lib/entities/ens_record.dart | 8 +- lib/entities/fio_address_provider.dart | 18 +- lib/entities/preferences_key.dart | 1 + lib/entities/unstoppable_domain_address.dart | 7 +- lib/entities/wellknown_record.dart | 7 +- lib/entities/zano_alias.dart | 7 +- .../provider/chainflip_exchange_provider.dart | 8 +- .../provider/changenow_exchange_provider.dart | 20 +- .../provider/exolix_exchange_provider.dart | 21 +- .../letsexchange_exchange_provider.dart | 17 +- .../provider/sideshift_exchange_provider.dart | 31 +- .../simpleswap_exchange_provider.dart | 23 +- .../stealth_ex_exchange_provider.dart | 31 +- .../provider/swaptrade_exchange_provider.dart | 26 +- .../provider/thorchain_exchange.provider.dart | 13 +- .../provider/trocador_exchange_provider.dart | 27 +- .../provider/xoswap_exchange_provider.dart | 19 +- lib/main.dart | 43 +- lib/mastodon/mastodon_api.dart | 8 +- lib/reactions/bootstrap.dart | 15 +- lib/reactions/check_connection.dart | 8 + lib/reactions/on_current_wallet_change.dart | 5 + lib/router.dart | 18 +- lib/routes.dart | 4 +- lib/src/screens/dev/network_requests.dart | 155 ++++++ lib/src/screens/nodes/widgets/node_form.dart | 21 +- .../settings/connection_sync_page.dart | 27 +- .../screens/settings/other_settings_page.dart | 6 + lib/src/screens/settings/tor_page.dart | 269 ----------- lib/src/screens/start_tor/start_tor_page.dart | 96 ++++ .../chain_service/eth/evm_chain_service.dart | 9 +- .../wc_pairing_detail_page.dart | 6 +- .../screens/wallet_keys/wallet_keys_page.dart | 6 +- lib/src/widgets/provider_optoin_tile.dart | 7 +- lib/src/widgets/standard_checkbox.dart | 4 +- lib/store/settings_store.dart | 12 + lib/store/yat/yat_store.dart | 4 - lib/twitter/twitter_api.dart | 5 +- lib/utils/feature_flag.dart | 3 +- lib/utils/image_utill.dart | 8 +- lib/utils/tor.dart | 80 ++++ .../cake_pay_cards_list_view_model.dart | 31 +- .../dashboard/dashboard_view_model.dart | 54 ++- .../dashboard/home_settings_view_model.dart | 15 +- lib/view_model/dashboard/nft_view_model.dart | 22 +- .../dev/network_requests_view_model.dart | 16 + .../dev/send_network_requests_view_model.dart | 16 + .../exchange/exchange_view_model.dart | 9 +- .../node_create_or_edit_view_model.dart | 4 + .../node_list/node_list_view_model.dart | 3 +- .../node_list/pow_node_list_view_model.dart | 3 +- lib/view_model/start_tor_view_model.dart | 95 ++++ pubspec_base.yaml | 14 +- res/values/strings_ar.arb | 6 + res/values/strings_bg.arb | 6 + res/values/strings_cs.arb | 6 + res/values/strings_de.arb | 6 + res/values/strings_en.arb | 6 + res/values/strings_es.arb | 6 + res/values/strings_fr.arb | 6 + res/values/strings_ha.arb | 6 + res/values/strings_hi.arb | 6 + res/values/strings_hr.arb | 6 + res/values/strings_hy.arb | 6 + res/values/strings_id.arb | 6 + res/values/strings_it.arb | 6 + res/values/strings_ja.arb | 6 + res/values/strings_ko.arb | 8 +- res/values/strings_my.arb | 6 + res/values/strings_nl.arb | 6 + res/values/strings_pl.arb | 8 +- res/values/strings_pt.arb | 6 + res/values/strings_ru.arb | 6 + res/values/strings_th.arb | 6 + res/values/strings_tl.arb | 6 + res/values/strings_tr.arb | 6 + res/values/strings_uk.arb | 6 + res/values/strings_ur.arb | 6 + res/values/strings_vi.arb | 6 + res/values/strings_yo.arb | 6 + res/values/strings_zh.arb | 6 + 139 files changed, 2754 insertions(+), 878 deletions(-) create mode 100644 .github/workflows/no_http_imports.yaml create mode 100644 assets/images/tor_logo.svg create mode 100644 cw_core/lib/utils/proxy_logger/abstract.dart create mode 100644 cw_core/lib/utils/proxy_logger/memory_proxy_logger.dart create mode 100644 cw_core/lib/utils/proxy_logger/silent_logger.dart create mode 100644 cw_core/lib/utils/proxy_socket/abstract.dart create mode 100644 cw_core/lib/utils/proxy_socket/insecure.dart create mode 100644 cw_core/lib/utils/proxy_socket/secure.dart create mode 100644 cw_core/lib/utils/proxy_socket/socks.dart create mode 100644 cw_core/lib/utils/proxy_wrapper.dart create mode 100644 cw_core/lib/utils/tor/abstract.dart create mode 100644 cw_core/lib/utils/tor/android.dart create mode 100644 cw_core/lib/utils/tor/disabled.dart create mode 100644 cw_core/lib/utils/tor/tails.dart create mode 100644 lib/src/screens/dev/network_requests.dart delete mode 100644 lib/src/screens/settings/tor_page.dart create mode 100644 lib/src/screens/start_tor/start_tor_page.dart create mode 100644 lib/utils/tor.dart create mode 100644 lib/view_model/dev/network_requests_view_model.dart create mode 100644 lib/view_model/dev/send_network_requests_view_model.dart create mode 100644 lib/view_model/start_tor_view_model.dart diff --git a/.github/workflows/no_http_imports.yaml b/.github/workflows/no_http_imports.yaml new file mode 100644 index 000000000..dad6821ac --- /dev/null +++ b/.github/workflows/no_http_imports.yaml @@ -0,0 +1,21 @@ +name: No http imports + +on: [pull_request] + +jobs: + PR_test_build: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - name: Check for http package usage + if: github.event_name == 'pull_request' + run: | + GIT_GREP_OUT="$(git grep package:http | (grep .dart: || test $? = 1) | (grep -v proxy_wrapper.dart || test $? = 1) | (grep -v very_insecure_http_do_not_use || test $? = 1) || true)" + [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 + echo "$GIT_GREP_OUT" + echo "There are .dart files which use http imports" + echo "Using http package breaks proxy integration" + echo "Please use ProxyWrapper.getHttpClient() from package:cw_core/utils/proxy_wrapper.dart" + exit 1 + \ No newline at end of file diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml index 9c3d82bc2..507793bd8 100644 --- a/.github/workflows/no_print_in_dart.yaml +++ b/.github/workflows/no_print_in_dart.yaml @@ -15,5 +15,5 @@ jobs: [[ "x$GIT_GREP_OUT" == "x" ]] && exit 0 echo "$GIT_GREP_OUT" echo "There are .dart files which use print() statements" - echo "Please use printV from package: cw_core/utils/print_verbose.dart" + echo "Please use printV from package:cw_core/utils/print_verbose.dart" exit 1 diff --git a/assets/images/tor_logo.svg b/assets/images/tor_logo.svg new file mode 100644 index 000000000..ebd00324d --- /dev/null +++ b/assets/images/tor_logo.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 1f5c369e3..2ddd30df6 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -5,6 +5,8 @@ import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; @@ -42,7 +44,7 @@ class ElectrumClient { static const aliveTimerDuration = Duration(seconds: 4); bool get isConnected => _isConnected; - Socket? socket; + ProxySocket? socket; void Function(ConnectionStatus)? onConnectionStatusChange; int _id; final Map _tasks; @@ -72,18 +74,11 @@ class ElectrumClient { } catch (_) {} socket = null; + final ssl = !(useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))); try { - if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { - socket = await Socket.connect(host, port, timeout: connectionTimeout); - } else { - socket = await SecureSocket.connect( - host, - port, - timeout: connectionTimeout, - onBadCertificate: (_) => true, - ); - } + socket = await ProxyWrapper().getSocksSocket(ssl, host, port, connectionTimeout: connectionTimeout); } catch (e) { + printV("connect: $e"); if (e is HandshakeException) { useSSL = !(useSSL ?? false); } @@ -105,7 +100,6 @@ class ElectrumClient { // use ping to determine actual connection status since we could've just not timed out yet: // _setConnectionStatus(ConnectionStatus.connected); - socket!.listen( (Uint8List event) { try { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 35c15682c..bb9cea1bc 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/format_amount.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -49,7 +50,6 @@ import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:sp_scanner/sp_scanner.dart'; import 'package:hex/hex.dart'; -import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -493,10 +493,9 @@ abstract class ElectrumWalletBase Future updateFeeRates() async { if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) { try { - final response = await http - .get(Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) - .timeout(Duration(seconds: 5)); - + final response = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/fees/recommended")) + .timeout(Duration(seconds: 15)); final result = json.decode(response.body) as Map; final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; @@ -1176,20 +1175,18 @@ abstract class ElectrumWalletBase } }); - return PendingBitcoinTransaction( - transaction, - type, - electrumClient: electrumClient, - amount: estimatedTx.amount, - fee: estimatedTx.fee, - feeRate: feeRateInt.toString(), - network: network, - hasChange: estimatedTx.hasChange, - isSendAll: estimatedTx.isSendAll, - hasTaprootInputs: hasTaprootInputs, - utxos: estimatedTx.utxos, - publicKeys: estimatedTx.publicKeys - )..addListener((transaction) async { + return PendingBitcoinTransaction(transaction, type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, + publicKeys: estimatedTx.publicKeys) + ..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { transactionHistory.transactions.values.forEach((tx) { @@ -1880,20 +1877,17 @@ abstract class ElectrumWalletBase if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { try { - final blockHash = await http.get( - Uri.parse( - "https://mempool.cakewallet.com/api/v1/block-height/$height", - ), - ); + final blockHash = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block-height/$height")) + .timeout(Duration(seconds: 15)); if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty && jsonDecode(blockHash.body) != null) { - final blockResponse = await http.get( - Uri.parse( - "https://mempool.cakewallet.com/api/v1/block/${blockHash.body}", - ), - ); + final blockResponse = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block/${blockHash}")) + .timeout(Duration(seconds: 15)); + if (blockResponse.statusCode == 200 && blockResponse.body.isNotEmpty && jsonDecode(blockResponse.body)['timestamp'] != null) { diff --git a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart index e4cd8a101..c56148de2 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart @@ -8,11 +8,12 @@ import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_bitcoin/psbt/signer.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:payjoin_flutter/bitcoin_ffi.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/receive.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; +import 'package:http/http.dart' as very_insecure_http_do_not_use; // for errors enum PayjoinReceiverRequestTypes { processOriginalTx, @@ -28,7 +29,7 @@ class PayjoinReceiverWorker { final pendingRequests = >{}; PayjoinReceiverWorker._(this.sendPort); - + static final client = ProxyWrapper().getHttpIOClient(); static Future run(List args) async { await pj.core.init(); @@ -42,11 +43,10 @@ class PayjoinReceiverWorker { receivePort.listen(worker.handleMessage); try { - final httpClient = http.Client(); final receiver = Receiver.fromJson(json: receiverJson); final uncheckedProposal = - await worker.receiveUncheckedProposal(httpClient, receiver); + await worker.receiveUncheckedProposal(receiver); final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast(); sendPort.send({ @@ -57,14 +57,14 @@ class PayjoinReceiverWorker { final payjoinProposal = await worker.processPayjoinProposal( uncheckedProposal, ); - final psbt = await worker.sendFinalProposal(httpClient, payjoinProposal); + final psbt = await worker.sendFinalProposal(payjoinProposal); sendPort.send({ 'type': PayjoinReceiverRequestTypes.proposalSent, 'psbt': psbt, }); } catch (e) { if (e is HttpException || - (e is http.ClientException && + (e is very_insecure_http_do_not_use.ClientException && e.message.contains("Software caused connection abort"))) { sendPort.send(PayjoinSessionError.recoverable(e.toString())); } else { @@ -98,16 +98,16 @@ class PayjoinReceiverWorker { return completer.future; } - Future receiveUncheckedProposal( - http.Client httpClient, Receiver session) async { + Future receiveUncheckedProposal(Receiver session) async { while (true) { printV("Polling for Proposal (${session.id()})"); final extractReq = await session.extractReq( - ohttpRelay: PayjoinManager.randomOhttpRelayUrl()); + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); final request = extractReq.$1; final url = Uri.parse(request.url.asString()); - final httpRequest = await httpClient.post(url, + final httpRequest = await client.post(url, headers: {'Content-Type': request.contentType}, body: request.body); final proposal = await session.processRes( @@ -116,14 +116,14 @@ class PayjoinReceiverWorker { } } - Future sendFinalProposal( - http.Client httpClient, PayjoinProposal finalProposal) async { + Future sendFinalProposal(PayjoinProposal finalProposal) async { final req = await finalProposal.extractReq( - ohttpRelay: PayjoinManager.randomOhttpRelayUrl()); + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); final proposalReq = req.$1; final proposalCtx = req.$2; - final request = await httpClient.post( + final request = await client.post( Uri.parse(proposalReq.url.asString()), headers: {"Content-Type": proposalReq.contentType}, body: proposalReq.body, diff --git a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart index f02e59f47..c11342800 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart @@ -5,7 +5,7 @@ import 'dart:isolate'; import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:payjoin_flutter/common.dart'; import 'package:payjoin_flutter/send.dart'; import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; @@ -44,17 +44,17 @@ class PayjoinSenderWorker { sendPort.send(e); } } + final client = ProxyWrapper().getHttpIOClient(); /// Run a payjoin sender (V2 protocol first, fallback to V1). Future runSender(Sender sender) async { - final httpClient = http.Client(); try { - return await _runSenderV2(sender, httpClient); + return await _runSenderV2(sender); } catch (e) { printV(e); if (e is pj_error.FfiCreateRequestError) { - return await _runSenderV1(sender, httpClient); + return await _runSenderV1(sender); } else if (e is HttpException) { printV(e); throw Exception(PayjoinSessionError.recoverable(e.toString())); @@ -65,14 +65,14 @@ class PayjoinSenderWorker { } /// Attempt to send payjoin using the V2 of the protocol. - Future _runSenderV2(Sender sender, http.Client httpClient) async { + Future _runSenderV2(Sender sender) async { try { final postRequest = await sender.extractV2( ohttpProxyUrl: await pj_uri.Url.fromStr(PayjoinManager.randomOhttpRelayUrl()), ); - final postResult = await _postRequest(httpClient, postRequest.$1); + final postResult = await _postRequest(postRequest.$1); final getContext = await postRequest.$2.processResponse(response: postResult); @@ -84,7 +84,7 @@ class PayjoinSenderWorker { final getRequest = await getContext.extractReq( ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), ); - final getRes = await _postRequest(httpClient, getRequest.$1); + final getRes = await _postRequest(getRequest.$1); final proposalPsbt = await getContext.processResponse( response: getRes, ohttpCtx: getRequest.$2, @@ -98,10 +98,10 @@ class PayjoinSenderWorker { } /// Attempt to send payjoin using the V1 of the protocol. - Future _runSenderV1(Sender sender, http.Client httpClient) async { + Future _runSenderV1(Sender sender) async { try { final postRequest = await sender.extractV1(); - final response = await _postRequest(httpClient, postRequest.$1); + final response = await _postRequest(postRequest.$1); sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted}); @@ -111,7 +111,7 @@ class PayjoinSenderWorker { } } - Future> _postRequest(http.Client client, Request req) async { + Future> _postRequest(Request req) async { final httpRequest = await client.post(Uri.parse(req.url.asString()), headers: {'Content-Type': req.contentType}, body: req.body); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 83af100ae..c2987894c 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -948,11 +948,21 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: "direct main" + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -1038,10 +1048,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 09369de8e..c24732c3a 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -54,6 +54,10 @@ dependencies: git: url: https://github.com/cake-tech/ledger-flutter-plus-plugins path: packages/ledger-litecoin + socks_socket: + git: + url: https://github.com/sneurlax/socks_socket + ref: e6232c53c1595469931ababa878759a067c02e94 dev_dependencies: flutter_test: diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index aee12b423..4786336af 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -1,7 +1,7 @@ +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:intl/intl.dart'; import 'dart:convert'; -import 'package:http/http.dart' as http; // FIXME: Hardcoded values; Works only for monero @@ -234,10 +234,14 @@ int getHavenHeightByDate({required DateTime date}) { } Future getHavenCurrentHeight() async { - final response = await http.get(Uri.parse('https://explorer.havenprotocol.org/api/networkinfo')); + final req = await ProxyWrapper().getHttpClient() + .getUrl(Uri.parse('https://explorer.havenprotocol.org/api/networkinfo')) + .timeout(Duration(seconds: 15)); + final response = await req.close(); + final stringResponse = await response.transform(utf8.decoder).join(); if (response.statusCode == 200) { - final info = jsonDecode(response.body); + final info = jsonDecode(stringResponse); return info['data']['height'] as int; } else { throw Exception('Failed to load current blockchain height'); @@ -269,13 +273,13 @@ const bitcoinDates = { }; Future getBitcoinHeightByDateAPI({required DateTime date}) async { - final response = await http.get( - Uri.parse( - "https://mempool.cakewallet.com/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}", - ), - ); + final req = await ProxyWrapper().getHttpClient() + .getUrl(Uri.parse("https://mempool.cakewallet.com/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}")) + .timeout(Duration(seconds: 15)); + final response = await req.close(); + final stringResponse = await response.transform(utf8.decoder).join(); - return jsonDecode(response.body)['height'] as int; + return jsonDecode(stringResponse)['height'] as int; } int getBitcoinHeightByDate({required DateTime date}) { diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 38fcde9e1..fdffb844b 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -1,12 +1,12 @@ import 'dart:io'; import 'package:cw_core/keyable.dart'; +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'dart:convert'; -import 'package:http/http.dart' as http; import 'package:hive/hive.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:http/io_client.dart' as ioc; import 'dart:math' as math; import 'package:convert/convert.dart'; @@ -184,23 +184,17 @@ class Node extends HiveObject with Keyable { final body = {'jsonrpc': '2.0', 'id': '0', 'method': "getinfo"}; try { - final authenticatingClient = HttpClient(); - authenticatingClient.badCertificateCallback = - ((X509Certificate cert, String host, int port) => true); - - final http.Client client = ioc.IOClient(authenticatingClient); - final jsonBody = json.encode(body); - final response = await client.post( - rpcUri, + final response = await ProxyWrapper().post( + clearnetUri: rpcUri, headers: {'Content-Type': 'application/json'}, body: jsonBody, ); - printV("node check response: ${response.body}"); - + final resBody = json.decode(response.body) as Map; + return resBody['result']['height'] != null; } catch (e) { printV("error: $e"); @@ -218,11 +212,7 @@ class Node extends HiveObject with Keyable { final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName}; try { - final authenticatingClient = HttpClient(); - authenticatingClient.badCertificateCallback = - ((X509Certificate cert, String host, int port) => true); - - final http.Client client = ioc.IOClient(authenticatingClient); + final client = ProxyWrapper().getHttpIOClient(); final jsonBody = json.encode(body); @@ -242,15 +232,15 @@ class Node extends HiveObject with Keyable { return !(response['offline'] as bool); } - printV("node check response: ${response.body}"); + final responseString = await response.body; - if ((response.body.contains("400 Bad Request") // Some other generic error + if ((responseString.contains("400 Bad Request") // Some other generic error || - response.body.contains("plain HTTP request was sent to HTTPS port") // Cloudflare + responseString.contains("plain HTTP request was sent to HTTPS port") // Cloudflare || response.headers["location"] != null // Generic reverse proxy || - response.body + responseString .contains("301 Moved Permanently") // Poorly configured generic reverse proxy ) && !(useSSL ?? false)) { @@ -277,15 +267,16 @@ class Node extends HiveObject with Keyable { } Future requestNodeWithProxy() async { - if (!isValidProxyAddress /* && !Tor.instance.enabled*/) { + if (!isValidProxyAddress && !CakeTor.instance.enabled) { return false; } String? proxy = socksProxyAddress; - // if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) { - // proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; - // } + if ((proxy?.isEmpty ?? true) && CakeTor.instance.enabled) { + proxy = "${InternetAddress.loopbackIPv4.address}:${CakeTor.instance.port}"; + } + printV("proxy: $proxy"); if (proxy == null) { return false; } @@ -305,13 +296,9 @@ class Node extends HiveObject with Keyable { // you try to communicate with it Future requestElectrumServer() async { try { - final Socket socket; - if (useSSL == true) { - socket = await SecureSocket.connect(uri.host, uri.port, - timeout: Duration(seconds: 5), onBadCertificate: (_) => true); - } else { - socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); - } + final ProxySocket socket; + socket = await ProxyWrapper().getSocksSocket(useSSL ?? false, uri.host, uri.port); + socket.destroy(); return true; @@ -322,8 +309,8 @@ class Node extends HiveObject with Keyable { Future requestNanoNode() async { try { - final response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, headers: {"Content-Type": "application/json", "nano-app": "cake-wallet"}, body: jsonEncode( { @@ -332,7 +319,8 @@ class Node extends HiveObject with Keyable { }, ), ); - final data = await jsonDecode(response.body); + + final data = jsonDecode(response.body); if (response.statusCode != 200 || data["error"] != null || data["balance"] == null || @@ -348,13 +336,14 @@ class Node extends HiveObject with Keyable { Future requestEthereumServer() async { try { - final response = await http.get( - uri, - headers: {'Content-Type': 'application/json'}, - ); + final req = await ProxyWrapper().getHttpClient() + .getUrl(uri,) + .timeout(Duration(seconds: 15)); + final response = await req.close(); return response.statusCode >= 200 && response.statusCode < 300; - } catch (_) { + } catch (err) { + printV("Failed to request ethereum server: $err"); return false; } } @@ -462,7 +451,7 @@ class DaemonRpc { /// Perform a JSON-RPC call with Digest Authentication. Future> call(String method, Map params) async { - final http.Client client = http.Client(); + final client = ProxyWrapper().getHttpIOClient(); final DigestAuth digestAuth = DigestAuth(username, password); // Initial request to get the `WWW-Authenticate` header. diff --git a/cw_core/lib/solana_rpc_http_service.dart b/cw_core/lib/solana_rpc_http_service.dart index fbe9a29dc..1c6e975aa 100644 --- a/cw_core/lib/solana_rpc_http_service.dart +++ b/cw_core/lib/solana_rpc_http_service.dart @@ -1,20 +1,19 @@ import 'dart:convert'; -import 'package:http/http.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:on_chain/solana/solana.dart'; class SolanaRPCHTTPService implements SolanaJSONRPCService { SolanaRPCHTTPService( - {required this.url, Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)}) - : client = client ?? Client(); + {required this.url, + this.defaultRequestTimeout = const Duration(seconds: 30)}); @override final String url; - final Client client; final Duration defaultRequestTimeout; - @override - Future> call(SolanaRequestDetails params, [Duration? timeout]) async { - final response = await client.post( - Uri.parse(url), + Future> call(SolanaRequestDetails params, + [Duration? timeout]) async { + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(url), body: params.toRequestBody(), headers: { 'Content-Type': 'application/json', diff --git a/cw_core/lib/utils/proxy_logger/abstract.dart b/cw_core/lib/utils/proxy_logger/abstract.dart new file mode 100644 index 000000000..303b640f1 --- /dev/null +++ b/cw_core/lib/utils/proxy_logger/abstract.dart @@ -0,0 +1,29 @@ +import 'dart:typed_data'; +import 'package:http/http.dart' as very_insecure_http_do_not_use; + +enum RequestNetwork { + clearnet, + tor, +} + +enum RequestMethod { + get, + post, + put, + delete, + + newHttpClient, + newHttpIOClient, + newProxySocket, +} + +abstract class ProxyLogger { + void log({ + required Uri? uri, + required RequestMethod method, + required Uint8List body, + required very_insecure_http_do_not_use.Response? response, + required RequestNetwork network, + required String? error, + }); +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_logger/memory_proxy_logger.dart b/cw_core/lib/utils/proxy_logger/memory_proxy_logger.dart new file mode 100644 index 000000000..e2929da12 --- /dev/null +++ b/cw_core/lib/utils/proxy_logger/memory_proxy_logger.dart @@ -0,0 +1,63 @@ +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_logger/abstract.dart'; +import 'package:http/http.dart' as very_insecure_http_do_not_use; + +class MemoryProxyLoggerEntry { + MemoryProxyLoggerEntry({ + required this.trace, + required this.uri, + required this.body, + required this.network, + required this.method, + required this.response, + required this.error, + }) : time = DateTime.now(); + + final StackTrace trace; + final Uri? uri; + final Uint8List body; + final RequestNetwork network; + final very_insecure_http_do_not_use.Response? response; + final RequestMethod method; + final String? error; + final DateTime time; + @override + String toString() => """MemoryProxyLoggerEntry( + uri: $uri, + body: $body, + network: $network, + method: $method, + response: + code: ${response?.statusCode}, + headers: ${response?.headers}, + body: ${response?.body}, + error: $error, + time: $time, + trace: ${trace} +);"""; +} + +class MemoryProxyLogger implements ProxyLogger { + static List logs = []; + @override + void log({ + required Uri? uri, + required RequestMethod method, + required Uint8List body, + required very_insecure_http_do_not_use.Response? response, + required RequestNetwork network, + required String? error, + }) { + final trace = StackTrace.current; + logs.add(MemoryProxyLoggerEntry( + method: method, + trace: trace, + uri: uri, + body: body, + network: network, + response: response, + error: error,), + ); + } +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_logger/silent_logger.dart b/cw_core/lib/utils/proxy_logger/silent_logger.dart new file mode 100644 index 000000000..1cea0d011 --- /dev/null +++ b/cw_core/lib/utils/proxy_logger/silent_logger.dart @@ -0,0 +1,17 @@ +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_logger/abstract.dart'; +import 'package:http/http.dart' as very_insecure_http_do_not_use; + +// we are not doing anything +class SilentProxyLogger implements ProxyLogger { + @override + void log({ + required Uri? uri, + required RequestMethod method, + required Uint8List body, + required very_insecure_http_do_not_use.Response? response, + required RequestNetwork network, + required String? error, + }) {} +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_socket/abstract.dart b/cw_core/lib/utils/proxy_socket/abstract.dart new file mode 100644 index 000000000..b4b628f74 --- /dev/null +++ b/cw_core/lib/utils/proxy_socket/abstract.dart @@ -0,0 +1,47 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_socket/insecure.dart'; +import 'package:cw_core/utils/proxy_socket/secure.dart'; +import 'package:cw_core/utils/proxy_socket/socks.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:socks_socket/socks_socket.dart'; + +class ProxyAddress { + final String host; + final int port; + + ProxyAddress({required this.host, required this.port}); +} + +abstract class ProxySocket { + static Future connect(bool sslEnabled, ProxyAddress address, {Duration? connectionTimeout}) async { + if (CakeTor.instance.started) { + var socksSocket = await SOCKSSocket.create( + proxyHost: InternetAddress.loopbackIPv4.address, + proxyPort: CakeTor.instance.port, + sslEnabled: sslEnabled, + ); + await socksSocket.connect(); + await socksSocket.connectTo(address.host, address.port); + return ProxySocketSocks(socksSocket); + } + if (sslEnabled == false) { + return ProxySocketInsecure(await Socket.connect(address.host, address.port, timeout: connectionTimeout)); + } else { + return ProxySocketSecure(await SecureSocket.connect( + address.host, + address.port, + timeout: connectionTimeout, + onBadCertificate: (_) => true, + )); + } + } + + Future close(); + Future destroy(); + Future write(String data); + StreamSubscription> listen(Function(Uint8List event) onData, {Function (Object error)? onError, Function ()? onDone, bool cancelOnError = true}); + ProxyAddress get address; +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_socket/insecure.dart b/cw_core/lib/utils/proxy_socket/insecure.dart new file mode 100644 index 000000000..aeac474d7 --- /dev/null +++ b/cw_core/lib/utils/proxy_socket/insecure.dart @@ -0,0 +1,34 @@ + +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:io'; + +class ProxySocketInsecure implements ProxySocket { + final Socket socket; + + ProxySocketInsecure(this.socket); + + ProxyAddress get address => ProxyAddress(host: socket.remoteAddress.host, port: socket.remotePort); + + @override + Future close() => socket.close(); + + @override + Future destroy() async => socket.destroy(); + + @override + Future write(String data) async => socket.write(data); + + @override + StreamSubscription> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) { + return socket.listen( + (data) { + onData(Uint8List.fromList(data)); + }, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} \ No newline at end of file diff --git a/cw_core/lib/utils/proxy_socket/secure.dart b/cw_core/lib/utils/proxy_socket/secure.dart new file mode 100644 index 000000000..2efd13ee4 --- /dev/null +++ b/cw_core/lib/utils/proxy_socket/secure.dart @@ -0,0 +1,34 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_socket/abstract.dart'; + +class ProxySocketSecure implements ProxySocket { + final SecureSocket socket; + + ProxySocketSecure(this.socket); + + ProxyAddress get address => ProxyAddress(host: socket.remoteAddress.host, port: socket.remotePort); + + @override + Future close() => socket.close(); + + @override + Future destroy() async => socket.destroy(); + + @override + Future write(String data) async => socket.write(data); + + @override + StreamSubscription> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) { + return socket.listen( + (data) { + onData(Uint8List.fromList(data)); + }, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} diff --git a/cw_core/lib/utils/proxy_socket/socks.dart b/cw_core/lib/utils/proxy_socket/socks.dart new file mode 100644 index 000000000..a4e5ddeb6 --- /dev/null +++ b/cw_core/lib/utils/proxy_socket/socks.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:socks_socket/socks_socket.dart'; + +class ProxySocketSocks implements ProxySocket { + final SOCKSSocket socket; + + ProxySocketSocks(this.socket); + + @override + ProxyAddress get address => ProxyAddress(host: socket.proxyHost, port: socket.proxyPort); + + @override + Future close() => socket.close(); + + @override + Future destroy() => close(); + + @override + Future write(String data) async => socket.write(data); + + @override + StreamSubscription> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) { + return socket.listen( + (data) { + onData(Uint8List.fromList(data)); + }, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} + diff --git a/cw_core/lib/utils/proxy_wrapper.dart b/cw_core/lib/utils/proxy_wrapper.dart new file mode 100644 index 000000000..e43f34ff1 --- /dev/null +++ b/cw_core/lib/utils/proxy_wrapper.dart @@ -0,0 +1,447 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:cw_core/utils/proxy_logger/abstract.dart'; +import 'package:cw_core/utils/proxy_socket/abstract.dart'; +import 'package:cw_core/utils/tor/abstract.dart'; +import 'package:cw_core/utils/tor/android.dart'; +import 'package:cw_core/utils/tor/disabled.dart'; +import 'package:http/http.dart'; +import 'package:socks5_proxy/socks_client.dart'; +import 'package:http/io_client.dart' as ioc; + +class ProxyWrapper { + static final ProxyWrapper _proxyWrapper = ProxyWrapper._internal(); + static ProxyLogger? logger; + + factory ProxyWrapper() { + return _proxyWrapper; + } + + ProxyWrapper._internal(); + Future getSocksSocket(bool sslEnabled, String host, int port, {Duration? connectionTimeout}) async { + logger?.log( + uri: Uri( + scheme: sslEnabled ? "https" : "http", + host: host, + port: port, + ), + method: RequestMethod.newProxySocket, + body: Uint8List(0), + response: null, + network: requestNetwork(), + error: null + ); + return ProxySocket.connect(sslEnabled, ProxyAddress(host: host, port: port), connectionTimeout: connectionTimeout); + } + + RequestNetwork requestNetwork() { + return CakeTor.instance.started ? RequestNetwork.tor : RequestNetwork.clearnet; + } + + ioc.IOClient getHttpIOClient({int? portOverride, bool internal = false}) { + if (!internal) { + logger?.log( + uri: null, + method: RequestMethod.newHttpIOClient, + body: Uint8List(0), + response: null, + network: requestNetwork(), + error: null, + ); + } + // ignore: deprecated_member_use_from_same_package + final httpClient = ProxyWrapper().getHttpClient(portOverride: portOverride, internal: true); + return ioc.IOClient(httpClient); + } + + int getPort() => CakeTor.instance.port; + + @Deprecated('Use ProxyWrapper().get/post/put methods instead, and provide proper clearnet and onion uri.') + HttpClient getHttpClient({int? portOverride, bool internal = false}) { + if (!internal) { + logger?.log( + uri: null, + method: RequestMethod.newProxySocket, + body: Uint8List(0), + response: null, + network: requestNetwork(), + error: null + ); + } + if (CakeTor.instance.started) { + // Assign connection factory. + final client = HttpClient(); + SocksTCPClient.assignToHttpClient(client, [ + ProxySettings( + InternetAddress.loopbackIPv4, + CakeTor.instance.port, + password: null, + ), + ]); + return client; + } else { + return HttpClient(); + } + } + + + + Future _make({ + required RequestMethod method, + required ioc.IOClient client, + required Uri uri, + required Map? headers, + String? body, + }) async { + Object? error; + Response? resp; + try { + switch (method) { + case RequestMethod.get: + resp = await client. get( + uri, + headers: headers, + ); + break; + case RequestMethod.delete: + resp = await client.delete( + uri, + headers: headers, + body: body, + ); + break; + case RequestMethod.post: + resp = await client.post( + uri, + headers: headers, + body: body, + ); + break; + case RequestMethod.put: + resp = await client.put( + uri, + headers: headers, + body: body, + ); + break; + case RequestMethod.newHttpClient: + case RequestMethod.newHttpIOClient: + case RequestMethod.newProxySocket: + throw UnimplementedError(); + } + return resp; + } catch (e) { + error = e; + rethrow; + } finally { + logger?.log( + uri: uri, + method: RequestMethod.get, + body: utf8.encode(body ?? ''), + response: resp, + network: requestNetwork(), + error: error?.toString(), + ); + } + } + + Future get({ + Map? headers, + int? portOverride, + Uri? clearnetUri, + Uri? onionUri, + }) async { + ioc.IOClient? torClient; + bool torEnabled = CakeTor.instance.started; + + if (CakeTor.instance.started) { + torEnabled = true; + } else { + torEnabled = false; + } + + // if tor is enabled, try to connect to the onion url first: + if (torEnabled) { + try { + // ignore: deprecated_member_use_from_same_package + torClient = await getHttpIOClient(portOverride: portOverride, internal: true); + } catch (_) { + rethrow; + } + + if (onionUri != null) { + try { + return await _make( + method: RequestMethod.get, + client: torClient, + uri: onionUri, + headers: headers, + ); + } catch (_) { + rethrow; + } + } + + if (clearnetUri != null) { + try { + return await _make( + method: RequestMethod.get, + client: torClient, + uri: clearnetUri, + headers: headers, + ); + } catch (_) { + rethrow; + } + } + } + + if (clearnetUri != null) { + try { + return HttpOverrides.runZoned( + () async { + return await _make( + method: RequestMethod.get, + client: ioc.IOClient(), + uri: clearnetUri, + headers: headers, + ); + }, + ); + } catch (_) { + // we weren't able to get a response: + rethrow; + } + } + + throw Exception("Unable to connect to server"); + } + + + Future post({ + Map? headers, + int? portOverride, + Uri? clearnetUri, + Uri? onionUri, + String? body, + bool allowMitmMoneroBypassSSLCheck = false, + }) async { + HttpClient? torHttpClient; + HttpClient cleatnetHttpClient = HttpClient(); + if (allowMitmMoneroBypassSSLCheck) { + cleatnetHttpClient.badCertificateCallback = + ((X509Certificate cert, String host, int port) => true); + } + + ioc.IOClient clearnetClient = ioc.IOClient(cleatnetHttpClient); + + + bool torEnabled = CakeTor.instance.started; + + if (torEnabled) { + try { + // ignore: deprecated_member_use_from_same_package + torHttpClient = await getHttpClient(portOverride: portOverride); + } catch (_) { + rethrow; + } + if (allowMitmMoneroBypassSSLCheck) { + torHttpClient.badCertificateCallback = + ((X509Certificate cert, String host, int port) => true); + } + if (onionUri != null) { + try { + return await _make( + method: RequestMethod.post, + client: ioc.IOClient(torHttpClient), + uri: onionUri, + headers: headers, + body: body, + ); + } catch (_) { + rethrow; + } + } + + if (clearnetUri != null) { + try { + return await _make( + method: RequestMethod.post, + client: ioc.IOClient(torHttpClient), + uri: clearnetUri, + headers: headers, + body: body, + ); + } catch (_) { + rethrow; + } + } + } + + if (clearnetUri != null) { + try { + return HttpOverrides.runZoned( + () async { + return await _make( + method: RequestMethod.post, + client: clearnetClient, + uri: clearnetUri, + headers: headers, + body: body, + ); + }, + ); + } catch (_) { + rethrow; + } + } + + throw Exception("Unable to connect to server"); + } + + Future put({ + Map? headers, + int? portOverride, + Uri? clearnetUri, + Uri? onionUri, + String? body, + }) async { + ioc.IOClient? torClient; + bool torEnabled = CakeTor.instance.started; + + if (torEnabled) { + try { + // ignore: deprecated_member_use_from_same_package + torClient = await getHttpIOClient(portOverride: portOverride, internal: true); + } catch (_) {} + + if (onionUri != null) { + try { + return await _make( + method: RequestMethod.put, + client: torClient!, + uri: onionUri, + headers: headers, + body: body, + ); + } catch (_) { + rethrow; + } + } + + if (clearnetUri != null) { + try { + return await _make( + method: RequestMethod.put, + client: torClient!, + uri: clearnetUri, + headers: headers, + body: body, + ); + } catch (_) { + rethrow; + } + } + } + + if (clearnetUri != null) { + try { + return HttpOverrides.runZoned( + () async { + return await _make( + method: RequestMethod.put, + client: ioc.IOClient(), + uri: clearnetUri, + headers: headers, + body: body, + ); + }, + ); + } catch (_) { + // we weren't able to get a response: + rethrow; + } + } + + throw Exception("Unable to connect to server"); + } + + Future delete({ + Map? headers, + int? portOverride, + Uri? clearnetUri, + Uri? onionUri, + }) async { + ioc.IOClient? torClient; + bool torEnabled = CakeTor.instance.started; + + if (CakeTor.instance.started) { + torEnabled = true; + } else { + torEnabled = false; + } + + // if tor is enabled, try to connect to the onion url first: + if (torEnabled) { + try { + // ignore: deprecated_member_use_from_same_package + torClient = await getHttpIOClient(portOverride: portOverride, internal: true); + } catch (_) { + rethrow; + } + + if (onionUri != null) { + try { + return await _make( + method: RequestMethod.delete, + client: torClient, + uri: onionUri, + headers: headers, + ); + } catch (_) { + rethrow; + } + } + + if (clearnetUri != null) { + try { + return await _make( + method: RequestMethod.delete, + client: torClient, + uri: clearnetUri, + headers: headers, + ); + } catch (_) { + rethrow; + } + } + } + + if (clearnetUri != null) { + try { + return HttpOverrides.runZoned( + () async { + return await _make( + method: RequestMethod.delete, + client: ioc.IOClient(), + uri: clearnetUri, + headers: headers, + ); + }, + ); + } catch (_) { + // we weren't able to get a response: + rethrow; + } + } + + throw Exception("Unable to connect to server"); + } +} + + +class CakeTor { + static final CakeTorInstance instance = CakeTorInstance.getInstance(); +} diff --git a/cw_core/lib/utils/tor/abstract.dart b/cw_core/lib/utils/tor/abstract.dart new file mode 100644 index 000000000..aaecabc56 --- /dev/null +++ b/cw_core/lib/utils/tor/abstract.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/tor/android.dart'; +import 'package:cw_core/utils/tor/disabled.dart'; +import 'package:cw_core/utils/tor/tails.dart'; + +abstract class CakeTorInstance { + bool get started; + + int get port => -1; + + bool get enabled => false; + + bool get bootstrapped => false; + + Future start(); + Future stop(); + + static CakeTorInstance getInstance() { + if (Platform.isAndroid) { + return CakeTorAndroid(); + } + if (Platform.isLinux) { + try { + final os = File("/etc/os-release").readAsLinesSync(); + for (var line in os) { + if (!line.startsWith("ID=")) continue; + if (!line.contains("tails")) continue; + return CakeTorTails(); + } + } catch (e) { + printV("Failed to identify linux version - /etc/os-release missing"); + } + } + return CakeTorDisabled(); + } +} \ No newline at end of file diff --git a/cw_core/lib/utils/tor/android.dart b/cw_core/lib/utils/tor/android.dart new file mode 100644 index 000000000..dfc1bfbe4 --- /dev/null +++ b/cw_core/lib/utils/tor/android.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/tor/abstract.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; +import 'package:tor_binary/tor_binary_platform_interface.dart'; + +class CakeTorAndroid implements CakeTorInstance { + @override + bool get bootstrapped => _proc != null; + + @override + bool get enabled => _proc != null; + + @override + int get port => 42142; + + @override + Future start() async { + await _runEmbeddedTor(); + } + + @override + bool get started => _proc != null; + + @override + Future stop() async { + _proc?.kill(); + await _proc?.exitCode; + _proc = null; + } + + static Process? _proc; + + Future _runEmbeddedTor() async { + final dir = await getApplicationCacheDirectory(); + + final torBinPath = p.join((await TorBinaryPlatform.instance.getBinaryPath())!, "libtor.so"); + printV("torPath: $torBinPath"); + + if (started) { + printV("Proxy is running"); + return; + } + + printV("Starting embedded tor"); + printV("app docs: $dir"); + final torrc = """ +SocksPort $port +Log notice file ${p.join(dir.path, "tor.log")} +RunAsDaemon 0 +DataDirectory ${p.join(dir.path, "tor-data")} +"""; + final torrcPath = p.join(dir.absolute.path, "torrc"); + File(torrcPath).writeAsStringSync(torrc); + + if (_proc != null) { + try { + _proc?.kill(); + await _proc?.exitCode; + _proc = null; + } catch (e) { + printV(e); + } + } + printV("path: $torBinPath -f $torrcPath"); + _proc = await Process.start(torBinPath, ["-f", torrcPath]); + _proc?.stdout.transform(utf8.decoder).forEach(printV); + _proc?.stderr.transform(utf8.decoder).forEach(printV); + } +} \ No newline at end of file diff --git a/cw_core/lib/utils/tor/disabled.dart b/cw_core/lib/utils/tor/disabled.dart new file mode 100644 index 000000000..8bd3d837e --- /dev/null +++ b/cw_core/lib/utils/tor/disabled.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/utils/tor/abstract.dart'; + +class CakeTorDisabled implements CakeTorInstance { + @override + bool get bootstrapped => false; + + @override + bool get enabled => false; + + @override + int get port => -1; + + @override + Future start() => throw UnimplementedError(); + + @override + bool get started => false; + + @override + Future stop() => throw UnimplementedError(); +} \ No newline at end of file diff --git a/cw_core/lib/utils/tor/tails.dart b/cw_core/lib/utils/tor/tails.dart new file mode 100644 index 000000000..c37ee72aa --- /dev/null +++ b/cw_core/lib/utils/tor/tails.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/utils/tor/abstract.dart'; + +class CakeTorTails implements CakeTorInstance { + @override + bool get bootstrapped => true; + + @override + bool get enabled => true; + + @override + int get port => 9150; + + @override + Future start() async {} + + @override + bool get started => true; + + @override + Future stop() async {} +} \ No newline at end of file diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index c4e178ccf..d7bfdbe2a 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -479,7 +479,7 @@ packages: description: path: "." ref: cake-update-v2 - resolved-ref: "01cbbacbb05d2113aafa8b7c4a2bb766f749d8d8" + resolved-ref: "096865a8c6b89c260beadfec04f7e184c40a3273" url: "https://github.com/cake-tech/on_chain.git" source: git version: "3.7.0" @@ -635,11 +635,21 @@ packages: socks5_proxy: dependency: "direct main" description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: "direct main" + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -716,10 +726,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: "direct main" + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 03f039d9a..07f87475a 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -25,16 +25,23 @@ dependencies: url: https://github.com/cake-tech/cake_backup.git ref: main version: 1.0.0 - socks5_proxy: ^1.0.4 + socks5_proxy: + git: + url: https://github.com/LacticWhale/socks_dart + ref: 27ad7c2efae8d7460325c74b90f660085cbd0685 unorm_dart: ^0.3.0 on_chain: git: url: https://github.com/cake-tech/on_chain.git ref: cake-update-v2 -# tor: -# git: -# url: https://github.com/cake-tech/tor.git -# ref: main + socks_socket: + git: + url: https://github.com/sneurlax/socks_socket + ref: e6232c53c1595469931ababa878759a067c02e94 + tor_binary: + git: + url: https://github.com/MrCyjaneK/flutter-tor_binary + ref: cb811c610871a9517d47134b87c2f590c15c96c5 dev_dependencies: flutter_test: diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index f1d4dd57a..f7a5f335d 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -666,11 +666,21 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -751,6 +761,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index 1298d54f5..259e7d11d 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -19,7 +19,7 @@ class EthereumClient extends EVMChainClient { Future> fetchTransactions(String address, {String? contractAddress}) async { try { - final response = await httpClient.get(Uri.https("api.etherscan.io", "/v2/api", { + final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", { "chainid": "$chainId", "module": "account", "action": contractAddress != null ? "tokentx" : "txlist", @@ -51,7 +51,7 @@ class EthereumClient extends EVMChainClient { @override Future> fetchInternalTransactions(String address) async { try { - final response = await httpClient.get(Uri.https("api.etherscan.io", "/v2/api", { + final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", { "chainid": "$chainId", "module": "account", "action": "txlistinternal", diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index 8b45d5544..5f383b5a3 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -5,6 +5,7 @@ import 'dart:developer'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_erc20_balance.dart'; @@ -12,13 +13,12 @@ import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/.secrets.g.dart' as secrets; import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart' as hex; -import 'package:http/http.dart'; import 'package:web3dart/web3dart.dart'; import 'contract/erc20.dart'; abstract class EVMChainClient { - final httpClient = Client(); + late final client = ProxyWrapper().getHttpIOClient(); Web3Client? _client; //! To be overridden by all child classes @@ -47,7 +47,7 @@ abstract class EVMChainClient { } _client = - Web3Client(isModifiedNodeUri ? rpcUri!.toString() : node.uri.toString(), httpClient); + Web3Client(isModifiedNodeUri ? rpcUri!.toString() : node.uri.toString(), client); return true; } catch (e) { @@ -353,7 +353,7 @@ abstract class EVMChainClient { }, ); - final response = await httpClient.get( + final response = await client.get( uri, headers: { "Accept": "application/json", diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 8c5ab2d41..e98ba71ca 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -146,7 +146,25 @@ int getUnlockedBalance({int accountIndex = 0}) => int getCurrentHeight() => currentWallet?.blockChainHeight() ?? 0; -int getNodeHeightSync() => currentWallet?.daemonBlockChainHeight() ?? 0; + +int cachedNodeHeight = 0; +bool isHeightRefreshing = false; +int getNodeHeightSync() { + if (isHeightRefreshing == false) { + (() async { + try { + isHeightRefreshing = true; + final wptrAddress = currentWallet!.ffiAddress(); + cachedNodeHeight = await Isolate.run(() async { + return monero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress)); + }); + } finally { + isHeightRefreshing = false; + } + })(); + } + return cachedNodeHeight; +} bool isConnectedSync() => currentWallet?.connected() != 0; @@ -202,7 +220,6 @@ Future setupNodeSync( } void startRefreshSync() { - currentWallet!.refreshAsync(); currentWallet!.startRefresh(); } @@ -228,7 +245,7 @@ void storeSync({bool force = false}) async { return monero.Wallet_synchronized(Pointer.fromAddress(addr)); }); if (lastStorePointer == addr && - lastStoreHeight + 5000 > currentWallet!.blockChainHeight() && + lastStoreHeight + 75000 > currentWallet!.blockChainHeight() && !synchronized && !force) { return; diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 1a6f11a1d..9a8cb70b4 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -17,6 +17,7 @@ import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; @@ -207,6 +208,14 @@ abstract class MoneroWalletBase extends WalletBase connectToNode({required Node node}) async { + String socksProxy = node.socksProxyAddress ?? ''; + printV("bootstrapped: ${CakeTor.instance.bootstrapped}"); + printV(" enabled: ${CakeTor.instance.enabled}"); + printV(" port: ${CakeTor.instance.port}"); + printV(" started: ${CakeTor.instance.started}"); + if (CakeTor.instance.enabled) { + socksProxy = "127.0.0.1:${CakeTor.instance.port}"; + } try { syncStatus = ConnectingSyncStatus(); await monero_wallet.setupNodeSync( @@ -216,7 +225,7 @@ abstract class MoneroWalletBase extends WalletBase getBalance(String address) async { - final response = await http.post( - _node!.uri, + final response = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: jsonEncode( { @@ -76,7 +76,8 @@ class NanoClient { }, ), ); - final data = await jsonDecode(response.body); + + final data = jsonDecode(response.body) as Map; if (response.statusCode != 200 || data["error"] != null || data["balance"] == null || @@ -93,8 +94,8 @@ class NanoClient { Future getAccountInfo(String address) async { try { - final response = await http.post( - _node!.uri, + final response = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: jsonEncode( { @@ -104,8 +105,9 @@ class NanoClient { }, ), ); - final data = await jsonDecode(response.body); - return AccountInfoResponse.fromJson(data as Map); + + final data = jsonDecode(response.body) as Map; + return AccountInfoResponse.fromJson(data); } catch (e) { printV("error while getting account info $e"); return null; @@ -114,8 +116,8 @@ class NanoClient { Future getBlockContents(String block) async { try { - final response = await http.post( - _node!.uri, + final response = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: jsonEncode( { @@ -125,7 +127,8 @@ class NanoClient { }, ), ); - final data = await jsonDecode(response.body); + + final data = jsonDecode(response.body) as Map; return BlockContentsResponse.fromJson(data["contents"] as Map); } catch (e) { printV("error while getting block info $e"); @@ -181,8 +184,8 @@ class NanoClient { } Future requestWork(String hash) async { - final response = await http.post( - _powNode!.uri, + final response = await ProxyWrapper().post( + clearnetUri: _powNode!.uri, headers: getHeaders(_powNode!.uri.host), body: json.encode( { @@ -191,8 +194,9 @@ class NanoClient { }, ), ); + if (response.statusCode == 200) { - final Map decoded = json.decode(response.body) as Map; + final decoded = jsonDecode(response.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); } @@ -224,13 +228,13 @@ class NanoClient { "block": block, }); - final processResponse = await http.post( - _node!.uri, + final processResponse = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: processBody, ); - final Map decoded = json.decode(processResponse.body) as Map; + final Map decoded = jsonDecode(processResponse.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); } @@ -423,12 +427,11 @@ class NanoClient { "subtype": "receive", "block": receiveBlock, }); - final processResponse = await http.post( - _node!.uri, + final processResponse = await ProxyWrapper().post( + clearnetUri: _node!.uri, headers: getHeaders(_node!.uri.host), body: processBody, ); - final Map decoded = json.decode(processResponse.body) as Map; if (decoded.containsKey("error")) { throw Exception("Received error ${decoded["error"]}"); @@ -440,16 +443,17 @@ class NanoClient { required String destinationAddress, required String privateKey, }) async { - final receivableResponse = await http.post(_node!.uri, - headers: getHeaders(_node!.uri.host), - body: jsonEncode({ - "action": "receivable", - "account": destinationAddress, - "count": "-1", - "source": true, - })); - - final receivableData = await jsonDecode(receivableResponse.body); + final receivableResponse = await ProxyWrapper().post( + clearnetUri: _node!.uri, + headers: getHeaders(_node!.uri.host), + body: jsonEncode({ + "action": "receivable", + "account": destinationAddress, + "count": "-1", + "source": true, + }), + ); + final receivableData = jsonDecode(receivableResponse.body) as Map; if (receivableData["blocks"] == "" || receivableData["blocks"] == null) { return 0; } @@ -492,15 +496,18 @@ class NanoClient { Future> fetchTransactions(String address) async { try { - final response = await http.post(_node!.uri, - headers: getHeaders(_node!.uri.host), - body: jsonEncode({ - "action": "account_history", - "account": address, - "count": "100", - // "raw": true, - })); - final data = await jsonDecode(response.body); + final response = await ProxyWrapper().post( + clearnetUri: _node!.uri, + headers: getHeaders(_node!.uri.host), + body: jsonEncode({ + "action": "account_history", + "account": address, + "count": "100", + // "raw": true, + }), + ); + + final data = jsonDecode(response.body) as Map; final transactions = data["history"] is List ? data["history"] as List : []; // Map the transactions list to NanoTransactionModel using the factory @@ -516,13 +523,14 @@ class NanoClient { Future> getN2Reps() async { final uri = Uri.parse(N2_REPS_ENDPOINT); - final response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, headers: getHeaders(uri.host), body: jsonEncode({"action": "reps"}), ); try { - final List nodes = (json.decode(response.body) as List) + + final List nodes = (jsonDecode(response.body) as List) .map((dynamic e) => N2Node.fromJson(e as Map)) .toList(); return nodes; @@ -533,8 +541,8 @@ class NanoClient { Future getRepScore(String rep) async { final uri = Uri.parse(N2_REPS_ENDPOINT); - final response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, headers: getHeaders(uri.host), body: jsonEncode({ "action": "rep_info", @@ -542,7 +550,8 @@ class NanoClient { }), ); try { - final N2Node node = N2Node.fromJson(json.decode(response.body) as Map); + + final N2Node node = N2Node.fromJson(jsonDecode(response.body) as Map); return node.score ?? 100; } catch (error) { return 100; diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index 20b18e6ed..aa85d6dde 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -784,11 +784,21 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -865,10 +875,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart index 7e9e882fa..5c344debe 100644 --- a/cw_polygon/lib/polygon_client.dart +++ b/cw_polygon/lib/polygon_client.dart @@ -40,7 +40,7 @@ class PolygonClient extends EVMChainClient { Future> fetchTransactions(String address, {String? contractAddress}) async { try { - final response = await httpClient.get(Uri.https("api.etherscan.io", "/v2/api", { + final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", { "chainid": "$chainId", "module": "account", "action": contractAddress != null ? "tokentx" : "txlist", @@ -68,7 +68,7 @@ class PolygonClient extends EVMChainClient { @override Future> fetchInternalTransactions(String address) async { try { - final response = await httpClient.get(Uri.https("api.etherscan.io", "/v2/api", { + final response = await client.get(Uri.https("api.etherscan.io", "/v2/api", { "chainid": "$chainId", "module": "account", "action": "txlistinternal", diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 128366c73..d57da5af0 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/solana_rpc_http_service.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_solana/pending_solana_transaction.dart'; @@ -11,7 +13,6 @@ import 'package:cw_solana/solana_balance.dart'; import 'package:cw_solana/solana_exceptions.dart'; import 'package:cw_solana/solana_transaction_model.dart'; import 'package:cw_solana/spl_token.dart'; -import 'package:http/http.dart' as http; import 'package:on_chain/solana/solana.dart'; import 'package:on_chain/solana/src/instructions/associated_token_account/constant.dart'; import 'package:on_chain/solana/src/models/pda/pda.dart'; @@ -20,10 +21,10 @@ import 'package:on_chain/solana/src/rpc/models/models/confirmed_transaction_meta import '.secrets.g.dart' as secrets; class SolanaWalletClient { - final httpClient = http.Client(); - SolanaRPC? _provider; // Minimum amount in SOL to consider a transaction valid (to filter spam) static const double minValidAmount = 0.00000003; + final httpClient = ProxyWrapper().getHttpClient(); + SolanaRPC? _provider; bool connect(Node node) { try { @@ -86,7 +87,8 @@ class SolanaWalletClient { } } - Future getSplTokenBalance(String mintAddress, String walletAddress) async { + Future getSplTokenBalance( + String mintAddress, String walletAddress) async { // Fetch the token accounts (a token can have multiple accounts for various uses) final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress); @@ -122,14 +124,16 @@ class SolanaWalletClient { ), ); - final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol; + final fee = + (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol; return fee; } catch (_) { return 0.0; } } - Future getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async { + Future getEstimatedFee( + SolanaPublicKey publicKey, Commitment commitment) async { final message = await _getMessageForNativeTransaction( publicKey: publicKey, destinationAddress: publicKey.toAddress().address, @@ -429,17 +433,20 @@ class SolanaWalletClient { } })); - final versionedBatchResponses = batchResponses.whereType(); + final versionedBatchResponses = + batchResponses.whereType(); - final parsedTransactionsFutures = versionedBatchResponses.map((tx) => parseTransaction( - txResponse: tx, - splTokenSymbol: splTokenSymbol, - walletAddress: walletAddress?.address ?? address.address, - )); + final parsedTransactionsFutures = + versionedBatchResponses.map((tx) => parseTransaction( + txResponse: tx, + splTokenSymbol: splTokenSymbol, + walletAddress: walletAddress?.address ?? address.address, + )); final parsedTransactions = await Future.wait(parsedTransactionsFutures); - transactions.addAll(parsedTransactions.whereType().toList()); + transactions.addAll( + parsedTransactions.whereType().toList()); // Only update UI if we have new valid transactions if (parsedTransactions.isNotEmpty) { @@ -508,8 +515,8 @@ class SolanaWalletClient { } Future fetchSPLTokenInfo(String mintAddress) async { - final programAddress = - MetaplexTokenMetaDataProgramUtils.findMetadataPda(mint: SolAddress(mintAddress)); + final programAddress = MetaplexTokenMetaDataProgramUtils.findMetadataPda( + mint: SolAddress(mintAddress)); final token = await _provider!.request( SolanaRPCGetMetadataAccount( @@ -530,8 +537,9 @@ class SolanaWalletClient { // iconPath = await _client.getIconImageFromTokenUri(metadata.uri); // } catch (_) {} - String filteredTokenSymbol = - metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + String filteredTokenSymbol = metadata.symbol + .replaceFirst(RegExp('^\\\$'), '') + .replaceAll('\u0000', ''); return SPLToken.fromMetadata( name: metadata.name, @@ -647,7 +655,8 @@ class SolanaWalletClient { return message; } - Future _getFeeFromCompiledMessage(Message message, Commitment commitment) async { + Future _getFeeFromCompiledMessage( + Message message, Commitment commitment) async { final base64Message = base64Encode(message.serialize()); final fee = await getFeeForMessage(base64Message, commitment); @@ -786,7 +795,8 @@ class SolanaWalletClient { required SolAddress mintAddress, required bool shouldCreateATA, }) async { - final associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + final associatedTokenAccount = + AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, owner: ownerAddress, ); @@ -857,7 +867,8 @@ class SolanaWalletClient { final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt(); ProgramDerivedAddress? associatedSenderAccount; try { - associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + associatedSenderAccount = + AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, owner: ownerPrivateKey.publicKey().toAddress(), ); @@ -992,7 +1003,8 @@ class SolanaWalletClient { if (uri.isEmpty || uri == '…') return null; try { - final response = await httpClient.get(Uri.parse(uri)); + final client = ProxyWrapper().getHttpIOClient(); + final response = await client.get(Uri.parse(uri)); final jsonResponse = json.decode(response.body) as Map; diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index ee93fbd53..cceec5327 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -5,6 +5,7 @@ import 'dart:developer'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_tron/pending_tron_transaction.dart'; import 'package:cw_tron/tron_abi.dart'; import 'package:cw_tron/tron_balance.dart'; @@ -13,12 +14,12 @@ import 'package:cw_tron/tron_token.dart'; import 'package:cw_tron/tron_transaction_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:http/http.dart'; import '.secrets.g.dart' as secrets; import 'package:on_chain/on_chain.dart'; class TronClient { - final httpClient = Client(); + late final client = ProxyWrapper().getHttpIOClient(); + TronProvider? _provider; // This is an internal tracker, so we don't have to "refetch". int _nativeTxEstimatedFee = 0; @@ -28,7 +29,7 @@ class TronClient { Future> fetchTransactions(String address, {String? contractAddress}) async { try { - final response = await httpClient.get( + final response = await client.get( Uri.https( "api.trongrid.io", "/v1/accounts/$address/transactions", @@ -61,7 +62,7 @@ class TronClient { Future> fetchTrc20ExcludedTransactions(String address) async { try { - final response = await httpClient.get( + final response = await client.get( Uri.https( "api.trongrid.io", "/v1/accounts/$address/transactions/trc20", diff --git a/cw_tron/lib/tron_http_provider.dart b/cw_tron/lib/tron_http_provider.dart index 8a3301f87..420ff85b0 100644 --- a/cw_tron/lib/tron_http_provider.dart +++ b/cw_tron/lib/tron_http_provider.dart @@ -1,22 +1,22 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:on_chain/tron/tron.dart'; import '.secrets.g.dart' as secrets; class TronHTTPProvider implements TronServiceProvider { TronHTTPProvider( {required this.url, - http.Client? client, - this.defaultRequestTimeout = const Duration(seconds: 30)}) - : client = client ?? http.Client(); + this.defaultRequestTimeout = const Duration(seconds: 30)}); + @override final String url; - final http.Client client; + late final client = ProxyWrapper().getHttpIOClient(); final Duration defaultRequestTimeout; @override - Future> get(TronRequestDetails params, [Duration? timeout]) async { + Future> get(TronRequestDetails params, + [Duration? timeout]) async { final response = await client.get(Uri.parse(params.url(url)), headers: { 'Content-Type': 'application/json', if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, @@ -27,13 +27,16 @@ class TronHTTPProvider implements TronServiceProvider { } @override - Future> post(TronRequestDetails params, [Duration? timeout]) async { + Future> post(TronRequestDetails params, + [Duration? timeout]) async { final response = await client .post(Uri.parse(params.url(url)), headers: { 'Content-Type': 'application/json', - if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, - if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey, + if (url.contains("trongrid")) + 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + if (url.contains("nownodes")) + 'api-key': secrets.tronNowNodesApiKey, }, body: params.toRequestBody()) .timeout(timeout ?? defaultRequestTimeout); diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 20783490d..a4ba8cbb2 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -110,7 +110,16 @@ int getUnlockedBalance({int accountIndex = 0}) => int getCurrentHeight() => wownero.Wallet_blockChainHeight(wptr!); -int getNodeHeightSync() => wownero.Wallet_daemonBlockChainHeight(wptr!); +int cachedNodeHeight = 0; +int getNodeHeightSync() { + (() async { + final wptrAddress = wptr!.address; + cachedNodeHeight = await Isolate.run(() async { + return wownero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress)); + }); + })(); + return cachedNodeHeight; +} bool isConnectedSync() => wownero.Wallet_connected(wptr!) != 0; @@ -154,7 +163,7 @@ Future setupNodeSync( } void startRefreshSync() { - wownero.Wallet_refreshAsync(wptr!); + // wownero.Wallet_refreshAsync(wptr!); wownero.Wallet_startRefresh(wptr!); } diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index e26672277..2befaa20d 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -16,6 +16,7 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wownero_amount_format.dart'; @@ -183,6 +184,14 @@ abstract class WowneroWalletBase @override Future connectToNode({required Node node}) async { + String socksProxy = node.socksProxyAddress ?? ''; + printV("bootstrapped: ${CakeTor.instance.bootstrapped}"); + printV(" enabled: ${CakeTor.instance.enabled}"); + printV(" port: ${CakeTor.instance.port}"); + printV(" started: ${CakeTor.instance.started}"); + if (CakeTor.instance.enabled) { + socksProxy = "127.0.0.1:${CakeTor.instance.port}"; + } try { syncStatus = ConnectingSyncStatus(); await wownero_wallet.setupNode( @@ -192,7 +201,7 @@ abstract class WowneroWalletBase useSSL: node.isSSL, isLightWallet: false, // FIXME: hardcoded value - socksProxyAddress: node.socksProxyAddress); + socksProxyAddress: socksProxy); wownero_wallet.setTrustedDaemon(node.trusted); syncStatus = ConnectedSyncStatus(); diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index dac4bda39..0fa39123b 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -683,11 +683,21 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -764,10 +774,19 @@ packages: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.1" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 3c790399c..41edf154e 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -680,11 +680,21 @@ packages: socks5_proxy: dependency: transitive description: - name: socks5_proxy - sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" - url: "https://pub.dev" - source: hosted - version: "1.0.6" + path: "." + ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685" + url: "https://github.com/LacticWhale/socks_dart" + source: git + version: "2.1.0" + socks_socket: + dependency: transitive + description: + path: "." + ref: e6232c53c1595469931ababa878759a067c02e94 + resolved-ref: e6232c53c1595469931ababa878759a067c02e94 + url: "https://github.com/sneurlax/socks_socket" + source: git + version: "1.1.1" source_gen: dependency: transitive description: @@ -765,6 +775,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tor_binary: + dependency: transitive + description: + path: "." + ref: cb811c610871a9517d47134b87c2f590c15c96c5 + resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5 + url: "https://github.com/MrCyjaneK/flutter-tor_binary" + source: git + version: "4.7.14" tuple: dependency: transitive description: diff --git a/integration_test/robots/wallet_keys_robot.dart b/integration_test/robots/wallet_keys_robot.dart index 189929737..38a605b9d 100644 --- a/integration_test/robots/wallet_keys_robot.dart +++ b/integration_test/robots/wallet_keys_robot.dart @@ -70,7 +70,10 @@ class WalletKeysAndSeedPageRobot { if (walletType == WalletType.bitcoin || walletType == WalletType.litecoin || walletType == WalletType.bitcoinCash) { - commonTestCases.hasText(appStore.wallet!.seed!); + final seedWords = appStore.wallet!.seed!.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has seeds properly displayed'); } @@ -78,10 +81,14 @@ class WalletKeysAndSeedPageRobot { walletType == WalletType.solana || walletType == WalletType.tron) { if (hasSeed) { - commonTestCases.hasText(appStore.wallet!.seed!); + final seedWords = appStore.wallet!.seed!.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has seeds properly displayed'); } if (hasPrivateKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(appStore.wallet!.privateKey!); tester.printToConsole('$walletName wallet has private key properly displayed'); } @@ -89,14 +96,19 @@ class WalletKeysAndSeedPageRobot { if (walletType == WalletType.nano || walletType == WalletType.banano) { if (hasSeed) { - commonTestCases.hasText(appStore.wallet!.seed!); + final seedWords = appStore.wallet!.seed!.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has seeds properly displayed'); } if (hasHexSeed) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(appStore.wallet!.hexSeed!); tester.printToConsole('$walletName wallet has hexSeed properly displayed'); } if (hasPrivateKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(appStore.wallet!.privateKey!); tester.printToConsole('$walletName wallet has private key properly displayed'); } @@ -129,35 +141,39 @@ class WalletKeysAndSeedPageRobot { final hasSeedLegacy = Polyseed.isValidSeed(seed); if (hasPublicSpendKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(keys.publicSpendKey); tester.printToConsole('$walletName wallet has public spend key properly displayed'); } if (hasPrivateSpendKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(keys.privateSpendKey); tester.printToConsole('$walletName wallet has private spend key properly displayed'); } if (hasPublicViewKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(keys.publicViewKey); tester.printToConsole('$walletName wallet has public view key properly displayed'); } if (hasPrivateViewKey) { + await commonTestCases.tapItemByKey('wallet_keys_page_keys'); commonTestCases.hasText(keys.privateViewKey); tester.printToConsole('$walletName wallet has private view key properly displayed'); } if (hasSeeds) { - await commonTestCases.dragUntilVisible( - '${walletName}_wallet_seed_item_key', - 'wallet_keys_page_credentials_list_view_key', - ); - commonTestCases.hasText(seed); + await commonTestCases.tapItemByKey('wallet_keys_page_seed'); + final seedWords = seed.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has seeds properly displayed'); } if (hasSeedLegacy) { - await commonTestCases.dragUntilVisible( - '${walletName}_wallet_seed_legacy_item_key', - 'wallet_keys_page_credentials_list_view_key', - ); - commonTestCases.hasText(legacySeed); + await commonTestCases.tapItemByKey('wallet_keys_page_seed_legacy'); + final seedWords = legacySeed.split(" "); + for (var seedWord in seedWords) { + commonTestCases.hasTextAtLestOnce(seedWord); + } tester.printToConsole('$walletName wallet has legacy seeds properly displayed'); } } diff --git a/lib/anonpay/anonpay_api.dart b/lib/anonpay/anonpay_api.dart index acab662d1..d68807b95 100644 --- a/lib/anonpay/anonpay_api.dart +++ b/lib/anonpay/anonpay_api.dart @@ -6,8 +6,8 @@ import 'package:cake_wallet/anonpay/anonpay_status_response.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:http/http.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -20,8 +20,9 @@ class AnonPayApi { final WalletBase wallet; static const anonpayRef = secrets.anonPayReferralCode; - static const onionApiAuthority = 'tqzngtf2hybjbexznel6dhgsvbynjzezoybvtv6iofomx7gchqfssgqd.onion'; static const clearNetAuthority = 'trocador.app'; + // static const onionApiAuthority = 'tqzngtf2hybjbexznel6dhgsvbynjzezoybvtv6iofomx7gchqfssgqd.onion'; + static const onionApiAuthority = clearNetAuthority; static const markup = secrets.trocadorExchangeMarkup; static const anonPayPath = '/anonpay'; static const anonPayStatus = '/anonpay/status'; @@ -29,8 +30,11 @@ class AnonPayApi { static const apiKey = secrets.trocadorApiKey; Future paymentStatus(String id) async { - final authority = await _getAuthority(); - final response = await get(Uri.https(authority, "$anonPayStatus/$id")); + final response = await ProxyWrapper().get( + clearnetUri: Uri.https(clearNetAuthority, "$anonPayStatus/$id"), + onionUri: Uri.https(onionApiAuthority, "$anonPayStatus/$id"), + ); + final responseJSON = json.decode(response.body) as Map; final status = responseJSON['Status'] as String; final fiatAmount = responseJSON['Fiat_Amount'] as double?; @@ -69,10 +73,11 @@ class AnonPayApi { if (request.fiatEquivalent != null) { body['fiat_equiv'] = request.fiatEquivalent; } - final authority = await _getAuthority(); - - final response = await get(Uri.https(authority, anonPayPath, body)); - + final response = await ProxyWrapper().get( + clearnetUri: Uri.https(clearNetAuthority, anonPayPath, body), + onionUri: Uri.https(onionApiAuthority, anonPayPath, body), + ); + final responseJSON = json.decode(response.body) as Map; final id = responseJSON['ID'] as String; final url = responseJSON['url'] as String; @@ -146,17 +151,16 @@ class AnonPayApi { 'name': cryptoCurrency.name, }; - final String apiAuthority = await _getAuthority(); - final uri = Uri.https(apiAuthority, coinPath, params); - - final response = await get(uri); - + final response = await ProxyWrapper().get( + clearnetUri: Uri.https(clearNetAuthority, coinPath, params), + onionUri: Uri.https(onionApiAuthority, coinPath, params), + ); + + final responseJSON = json.decode(response.body) as List; if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } - final responseJSON = json.decode(response.body) as List; - if (responseJSON.isEmpty) { throw Exception('No data'); } @@ -197,17 +201,4 @@ class AnonPayApi { return tag.toLowerCase(); } } - - Future _getAuthority() async { - try { - if (useTorOnly) { - return onionApiAuthority; - } - final uri = Uri.https(onionApiAuthority, '/anonpay'); - await get(uri); - return onionApiAuthority; - } catch (e) { - return clearNetAuthority; - } - } } diff --git a/lib/anypay/anypay_api.dart b/lib/anypay/anypay_api.dart index 0b81d24c2..20187484b 100644 --- a/lib/anypay/anypay_api.dart +++ b/lib/anypay/anypay_api.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/foundation.dart'; -import 'package:http/http.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/anypay/any_pay_payment.dart'; import 'package:cake_wallet/anypay/any_pay_trasnaction.dart'; @@ -53,8 +53,12 @@ class AnyPayApi { final body = { 'chain': chainByScheme(scheme), 'currency': currencyByScheme(scheme).title}; - final response = await post(url, headers: headers, body: utf8.encode(json.encode(body))); - + final response = await ProxyWrapper().post( + clearnetUri: url, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 200) { await ExceptionHandler.onError(FlutterErrorDetails(exception: response)); throw Exception('Unexpected response http code: ${response.statusCode}'); @@ -79,7 +83,12 @@ class AnyPayApi { 'chain': chain, 'currency': currency, 'transactions': transactions.map((tx) => {'tx': tx.tx, 'tx_hash': tx.id, 'tx_key': tx.key}).toList()}; - final response = await post(Uri.parse(uri), headers: headers, body: utf8.encode(json.encode(body))); + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(uri), + headers: headers, + body: json.encode(body), + ); + if (response.statusCode == 400) { final decodedBody = json.decode(response.body) as Map; throw Exception(decodedBody['message'] as String? ?? 'Unexpected response\nError code: 400'); diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index 150f731dc..eed527cf1 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -17,7 +18,6 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class DFXBuyProvider extends BuyProvider { @@ -100,11 +100,12 @@ class DFXBuyProvider extends BuyProvider { }); final uri = Uri.https(_baseUrl, _authPath); - var response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, headers: {'Content-Type': 'application/json'}, body: requestBody, ); + if (response.statusCode == 201) { final responseBody = jsonDecode(response.body); @@ -137,8 +138,10 @@ class DFXBuyProvider extends BuyProvider { final url = Uri.https(_baseUrl, '/v1/fiat'); try { - final response = await http.get(url, headers: {'accept': 'application/json'}); - + final response = await ProxyWrapper().get( + clearnetUri: url, + headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as List; for (final item in data) { @@ -160,8 +163,8 @@ class DFXBuyProvider extends BuyProvider { final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain}); try { - final response = await http.get(url, headers: {'accept': 'application/json'}); - + final response = await ProxyWrapper().get(clearnetUri: url, headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { final responseData = jsonDecode(response.body); @@ -271,7 +274,12 @@ class DFXBuyProvider extends BuyProvider { }); try { - final response = await http.put(url, headers: headers, body: body); + final response = await ProxyWrapper().put( + clearnetUri: url, + headers: headers, + body: body, + ); + final responseData = jsonDecode(response.body); if (response.statusCode == 200) { diff --git a/lib/buy/kryptonim/kryptonim.dart b/lib/buy/kryptonim/kryptonim.dart index 5e58ce190..1d2d10f58 100644 --- a/lib/buy/kryptonim/kryptonim.dart +++ b/lib/buy/kryptonim/kryptonim.dart @@ -9,10 +9,10 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; import 'dart:developer'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class KryptonimBuyProvider extends BuyProvider { @@ -74,9 +74,14 @@ class KryptonimBuyProvider extends BuyProvider { }); try { - final response = await http.post(url, headers: headers, body: body); + final response = await ProxyWrapper().post( + clearnetUri: url, + headers: headers, + body: body, + ); if (response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 401) { + return jsonDecode(response.body) as Map; } else { return {}; diff --git a/lib/buy/meld/meld_buy_provider.dart b/lib/buy/meld/meld_buy_provider.dart index db593d358..002ab0465 100644 --- a/lib/buy/meld/meld_buy_provider.dart +++ b/lib/buy/meld/meld_buy_provider.dart @@ -8,13 +8,13 @@ import 'package:cake_wallet/buy/payment_method.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; import 'dart:developer'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class MeldBuyProvider extends BuyProvider { @@ -71,8 +71,8 @@ class MeldBuyProvider extends BuyProvider { final url = Uri.https(_baseUrl, path, params); try { - final response = await http.get( - url, + final response = await ProxyWrapper().get( + clearnetUri: url, headers: { 'Authorization': _isProduction ? '' : _testApiKey, 'Meld-Version': '2023-12-19', @@ -80,6 +80,7 @@ class MeldBuyProvider extends BuyProvider { 'content-type': 'application/json', }, ); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as List; @@ -130,7 +131,12 @@ class MeldBuyProvider extends BuyProvider { }); try { - final response = await http.post(url, headers: headers, body: body); + final response = await ProxyWrapper().post( + clearnetUri: url, + headers: headers, + body: body, + ); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as Map; diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 47e8e34f9..1f9ab1bb1 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -16,12 +16,12 @@ import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/themes/core/material_base_theme.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart'; import 'package:url_launcher/url_launcher.dart'; class MoonPayProvider extends BuyProvider { @@ -98,9 +98,12 @@ class MoonPayProvider extends BuyProvider { Future getMoonpaySignature(String query) async { final uri = Uri.https(_cIdBaseUrl, "/api/moonpay"); - final response = await post(uri, - headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey}, - body: json.encode({'query': query})); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey}, + body: json.encode({'query': query}), + ); + if (response.statusCode == 200) { return (jsonDecode(response.body) as Map)['signature'] as String; @@ -120,7 +123,11 @@ class MoonPayProvider extends BuyProvider { final url = Uri.https(_baseUrl, path, params); try { - final response = await get(url, headers: {'accept': 'application/json'}); + final response = await ProxyWrapper().get( + clearnetUri: url, + headers: {'accept': 'application/json'}, + ); + if (response.statusCode == 200) { return jsonDecode(response.body) as Map; } else { @@ -191,8 +198,8 @@ class MoonPayProvider extends BuyProvider { final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath'; final url = Uri.https(_baseUrl, path, params); try { - final response = await get(url); - + final response = await ProxyWrapper().get(clearnetUri: url); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as Map; @@ -306,7 +313,8 @@ class MoonPayProvider extends BuyProvider { Future findOrderById(String id) async { final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey; final uri = Uri.parse(url); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Transaction $id is not found!'); diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 54283a612..80c9a6326 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -9,11 +9,12 @@ import 'package:cake_wallet/buy/payment_method.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/themes/core/theme_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class OnRamperBuyProvider extends BuyProvider { @@ -66,8 +67,10 @@ class OnRamperBuyProvider extends BuyProvider { final url = Uri.https(_baseApiUrl, '$supported$defaultsAll', params); try { - final response = - await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + final response = await ProxyWrapper().get( + clearnetUri: url, + headers: {'Authorization': _apiKey, 'accept': 'application/json'}, + ); if (response.statusCode == 200) { final Map data = jsonDecode(response.body) as Map; @@ -102,8 +105,8 @@ class OnRamperBuyProvider extends BuyProvider { try { final response = - await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); - + await ProxyWrapper().get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + if (response.statusCode == 200) { final Map data = jsonDecode(response.body) as Map; final List message = data['message'] as List; @@ -132,7 +135,8 @@ class OnRamperBuyProvider extends BuyProvider { try { final response = - await http.get(url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + await ProxyWrapper().get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); + if (response.statusCode == 200) { final Map data = jsonDecode(response.body) as Map; @@ -195,8 +199,8 @@ class OnRamperBuyProvider extends BuyProvider { final headers = {'Authorization': _apiKey, 'accept': 'application/json'}; try { - final response = await http.get(url, headers: headers); - + final response = await ProxyWrapper().get(clearnetUri: url, headers: headers); + if (response.statusCode == 200) { final data = jsonDecode(response.body) as List; if (data.isEmpty) return null; diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 4945529ef..4b9c1aa70 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -18,7 +19,6 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class RobinhoodBuyProvider extends BuyProvider { @@ -69,7 +69,8 @@ class RobinhoodBuyProvider extends BuyProvider { final uri = Uri.https(_apiBaseUrl, '$_assetsPath', {'applicationId': _applicationId}); try { - final response = await http.get(uri, headers: {'accept': 'application/json'}); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'accept': 'application/json'}); + if (response.statusCode == 200) { final responseData = jsonDecode(response.body) as Map; @@ -122,12 +123,14 @@ class RobinhoodBuyProvider extends BuyProvider { final uri = Uri.https(_cIdBaseUrl, "/api/robinhood"); - var response = await http.post(uri, - headers: {'Content-Type': 'application/json'}, - body: json - .encode({'valid_until': valid_until, 'wallet': walletAddress, 'signature': signature})); + var response = await ProxyWrapper().post( + clearnetUri: uri, + headers: {'Content-Type': 'application/json'}, + body: json.encode({'valid_until': valid_until, 'wallet': walletAddress, 'signature': signature}), + ); if (response.statusCode == 200) { + return (jsonDecode(response.body) as Map)['connectId'] as String; } else { throw Exception('Provider currently unavailable. Status: ${response.statusCode}'); @@ -219,7 +222,8 @@ class RobinhoodBuyProvider extends BuyProvider { Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams); try { - final response = await http.get(uri, headers: {'accept': 'application/json'}); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'accept': 'application/json'}); + final responseData = jsonDecode(response.body) as Map; if (response.statusCode == 200) { diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index 7fe6f4be3..3f9844df6 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -3,7 +3,7 @@ import 'package:cake_wallet/buy/buy_exception.dart'; import 'package:cake_wallet/buy/pairs_utils.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:http/http.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/buy/buy_amount.dart'; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/buy_provider_description.dart'; @@ -73,18 +73,21 @@ class WyreBuyProvider extends BuyProvider { 'referrerAccountId': _accountId, 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest'] }; - final response = await post(uri, - headers: { - 'Authorization': 'Bearer $_secretKey', - 'Content-Type': 'application/json', - 'cache-control': 'no-cache' - }, - body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: { + 'Authorization': 'Bearer $_secretKey', + 'Content-Type': 'application/json', + 'cache-control': 'no-cache' + }, + body: json.encode(body), + ); if (response.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Url $url is not found!'); } + final responseJSON = json.decode(response.body) as Map; final urlFromResponse = responseJSON['url'] as String; return urlFromResponse; @@ -101,18 +104,21 @@ class WyreBuyProvider extends BuyProvider { 'country': _countryCode }; final uri = Uri.parse(quoteUrl); - final response = await post(uri, - headers: { - 'Authorization': 'Bearer $_secretKey', - 'Content-Type': 'application/json', - 'cache-control': 'no-cache' - }, - body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: { + 'Authorization': 'Bearer $_secretKey', + 'Content-Type': 'application/json', + 'cache-control': 'no-cache' + }, + body: json.encode(body), + ); if (response.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Quote is not found!'); } + final responseJSON = json.decode(response.body) as Map; final sourceAmount = responseJSON['sourceAmount'] as double; final destAmount = responseJSON['destAmount'] as double; @@ -125,8 +131,7 @@ class WyreBuyProvider extends BuyProvider { Future findOrderById(String id) async { final orderUrl = baseApiUrl + _ordersSuffix + '/$id'; final orderUri = Uri.parse(orderUrl); - final orderResponse = await get(orderUri); - + final orderResponse = await ProxyWrapper().get(clearnetUri: orderUri); if (orderResponse.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Order $id is not found!'); } @@ -142,8 +147,7 @@ class WyreBuyProvider extends BuyProvider { final transferUrl = baseApiUrl + _transferSuffix + transferId + _trackSuffix; final transferUri = Uri.parse(transferUrl); - final transferResponse = await get(transferUri); - + final transferResponse = await ProxyWrapper().get(clearnetUri: transferUri); if (transferResponse.statusCode != 200) { throw BuyException(title: providerDescription, content: 'Transfer $transferId is not found!'); } diff --git a/lib/cake_pay/cake_pay_api.dart b/lib/cake_pay/cake_pay_api.dart index 5f1a350c0..a3c7f48d0 100644 --- a/lib/cake_pay/cake_pay_api.dart +++ b/lib/cake_pay/cake_pay_api.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:cake_wallet/cake_pay/cake_pay_order.dart'; import 'package:cake_wallet/cake_pay/cake_pay_user_credentials.dart'; import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cake_wallet/entities/country.dart'; -import 'package:http/http.dart' as http; class CakePayApi { static const testBaseUri = false; @@ -32,12 +32,17 @@ class CakePayApi { 'Content-Type': 'application/json', 'Authorization': 'Api-Key $apiKey', }; - final response = await http.post(uri, headers: headers, body: json.encode({'email': email})); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode({'email': email}), + ); if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } + final bodyJson = json.decode(response.body) as Map; if (bodyJson.containsKey('user') && bodyJson['user']['email'] != null) { @@ -64,12 +69,17 @@ class CakePayApi { }; final query = {'email': email, 'otp': code}; - final response = await http.post(uri, headers: headers, body: json.encode(query)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(query), + ); if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } + final bodyJson = json.decode(response.body) as Map; if (bodyJson.containsKey('error')) { @@ -116,9 +126,14 @@ class CakePayApi { }; try { - final response = await http.post(uri, headers: headers, body: json.encode(query)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(query), + ); if (response.statusCode != 201) { + final responseBody = json.decode(response.body); if (responseBody is List) { throw '${responseBody[0]}'; @@ -127,6 +142,7 @@ class CakePayApi { } } + final bodyJson = json.decode(response.body) as Map; return CakePayOrder.fromMap(bodyJson); } catch (e) { @@ -145,7 +161,8 @@ class CakePayApi { 'X-CSRFToken': CSRFToken, }; - final response = await http.get(uri, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + printV('Response: ${response.statusCode}'); @@ -168,7 +185,11 @@ class CakePayApi { }; try { - final response = await http.post(uri, headers: headers, body: json.encode({'email': email})); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode({'email': email}), + ); if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); @@ -187,8 +208,8 @@ class CakePayApi { 'Authorization': 'Api-Key $apiKey', }; - final response = await http.get(uri, headers: headers); - + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } @@ -234,14 +255,15 @@ class CakePayApi { 'Authorization': 'Api-Key $apiKey', }; - var response = await http.get(uri, headers: headers); + var response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + if (response.statusCode != 200) { throw Exception( 'Failed to fetch vendors: statusCode - ${response.statusCode}, queryParams -$queryParams, response - ${response.body}'); } - final bodyJson = json.decode(utf8.decode(response.bodyBytes)); + final bodyJson = json.decode(response.body); if (bodyJson is List && bodyJson.isEmpty) { return []; diff --git a/lib/core/background_sync.dart b/lib/core/background_sync.dart index 12eb81f99..a503f5c1b 100644 --- a/lib/core/background_sync.dart +++ b/lib/core/background_sync.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; +import 'package:cake_wallet/utils/tor.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cw_core/sync_status.dart'; @@ -15,6 +16,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter/foundation.dart'; class BackgroundSync { final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); @@ -90,6 +92,11 @@ class BackgroundSync { } Future sync() async { + final settingsStore = getIt.get(); + if (settingsStore.currentBuiltinTor) { + printV("Starting Tor"); + await ensureTorStarted(context: null); + } printV("Background sync started"); await _syncWallets(); printV("Background sync completed"); @@ -100,7 +107,6 @@ class BackgroundSync { final walletListViewModel = getIt.get(); final settingsStore = getIt.get(); - final List moneroWallets = walletListViewModel.wallets .where((element) => !element.isHardware) .where((element) => ![WalletType.haven, WalletType.decred].contains(element.type)) diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 8a37175b4..046c2b149 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -1,18 +1,15 @@ +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'dart:convert'; -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 _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion'; +const _fiatApiOnionAuthority = _fiatApiClearNetAuthority; const _fiatApiPath = '/v2/rates'; -Future _fetchPrice(Map args) async { - final crypto = args['crypto'] as String; - final fiat = args['fiat'] as String; - final torOnly = args['torOnly'] as bool; +Future _fetchPrice(String crypto, String fiat, bool torOnly) async { final Map queryParams = { 'interval_count': '1', @@ -24,14 +21,14 @@ Future _fetchPrice(Map args) async { num price = 0.0; try { - late final Uri uri; - if (torOnly) { - uri = Uri.http(_fiatApiOnionAuthority, _fiatApiPath, queryParams); - } else { - uri = Uri.https(_fiatApiClearNetAuthority, _fiatApiPath, queryParams); - } + final onionUri = Uri.http(_fiatApiOnionAuthority, _fiatApiPath, queryParams); + final clearnetUri = Uri.https(_fiatApiClearNetAuthority, _fiatApiPath, queryParams); - final response = await get(uri); + final response = await ProxyWrapper().get( + onionUri: onionUri, + clearnetUri: torOnly ? onionUri : clearnetUri, + ); + if (response.statusCode != 200) { return 0.0; @@ -50,18 +47,11 @@ Future _fetchPrice(Map args) async { } } -Future _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async => - compute(_fetchPrice, { - 'fiat': fiat.toString(), - 'crypto': crypto.toString(), - 'torOnly': torOnly, - }); - class FiatConversionService { static Future fetchPrice({ required CryptoCurrency crypto, required FiatCurrency fiat, required bool torOnly, }) async => - await _fetchPriceAsync(crypto, fiat, torOnly); + await _fetchPrice(crypto.toString(), fiat.toString(), torOnly); } diff --git a/lib/core/open_crypto_pay/open_cryptopay_service.dart b/lib/core/open_crypto_pay/open_cryptopay_service.dart index 6449bbf90..4b317d9c3 100644 --- a/lib/core/open_crypto_pay/open_cryptopay_service.dart +++ b/lib/core/open_crypto_pay/open_cryptopay_service.dart @@ -5,15 +5,13 @@ import 'package:cake_wallet/core/open_crypto_pay/exceptions.dart'; import 'package:cake_wallet/core/open_crypto_pay/lnurl.dart'; import 'package:cake_wallet/core/open_crypto_pay/models.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:http/http.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; class OpenCryptoPayService { static bool isOpenCryptoPayQR(String value) => value.toLowerCase().contains("lightning=lnurl") || value.toLowerCase().startsWith("lnurl"); - final Client _httpClient = Client(); - Future commitOpenCryptoPayRequest( String txHex, { required String txId, @@ -31,7 +29,8 @@ class OpenCryptoPayService { queryParams['tx'] = txId; final response = - await _httpClient.get(Uri.https(uri.authority, uri.path, queryParams)); + await ProxyWrapper().get(clearnetUri: Uri.https(uri.authority, uri.path, queryParams)); + if (response.statusCode == 200) { final body = jsonDecode(response.body) as Map; @@ -40,13 +39,13 @@ class OpenCryptoPayService { throw OpenCryptoPayException(body.toString()); } throw OpenCryptoPayException( - "Unexpected status code ${response.statusCode} ${response.body}"); + "Unexpected status code ${response.statusCode} ${response}"); } Future cancelOpenCryptoPayRequest(OpenCryptoPayRequest request) async { final uri = Uri.parse(request.callbackUrl.replaceAll("/cb/", "/cancel/")); - await _httpClient.delete(uri); + await ProxyWrapper().delete(clearnetUri: uri); } Future getOpenCryptoPayInvoice(String lnUrl) async { @@ -73,7 +72,8 @@ class OpenCryptoPayService { Future<(_OpenCryptoPayQuote, Map>)> _getOpenCryptoPayParams(Uri uri) async { - final response = await _httpClient.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 200) { final responseBody = jsonDecode(response.body) as Map; @@ -119,8 +119,8 @@ class OpenCryptoPayService { queryParams['asset'] = asset.title; queryParams['method'] = _getMethod(asset); - final response = - await _httpClient.get(Uri.https(uri.authority, uri.path, queryParams)); + final response = await ProxyWrapper().get(clearnetUri: Uri.https(uri.authority, uri.path, queryParams)); + if (response.statusCode == 200) { final responseBody = jsonDecode(response.body) as Map; diff --git a/lib/core/yat_service.dart b/lib/core/yat_service.dart index 92e81e5ef..84aac1626 100644 --- a/lib/core/yat_service.dart +++ b/lib/core/yat_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:cake_wallet/entities/yat_record.dart'; -import 'package:http/http.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; class YatService { static bool isDevMode = false; @@ -33,7 +33,8 @@ class YatService { final yatRecords = []; try { - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final resBody = json.decode(response.body) as Map; final results = resBody["result"] as Map; // Favour a subaddress over a standard address. @@ -42,7 +43,7 @@ class YatService { results[MONERO_STD_ADDRESS] ?? results[tag]) as Map; - if (yatRecord != null) { + if (yatRecord.isNotEmpty) { yatRecords.add(YatRecord.fromJson(yatRecord)); } diff --git a/lib/di.dart b/lib/di.dart index 46c453d61..5a7be0f1f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -33,10 +33,12 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/haven/cw_haven.dart'; import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; +import 'package:cake_wallet/src/screens/dev/network_requests.dart'; import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/savings_page.dart'; import 'package:cake_wallet/src/screens/settings/background_sync_page.dart'; +import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; @@ -48,6 +50,7 @@ import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; +import 'package:cake_wallet/view_model/start_tor_view_model.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; @@ -138,7 +141,6 @@ import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; -import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; @@ -690,7 +692,6 @@ Future setup({ return walletKitService; }); - getIt.registerFactory(() => NFTViewModel(appStore, getIt.get())); getIt.registerFactory(() => BalancePage( nftViewModel: getIt.get(), dashboardViewModel: getIt.get(), @@ -1495,7 +1496,7 @@ Future setup({ () => WalletConnectConnectionsView(walletKitService: getIt.get()), ); - getIt.registerFactory(() => TorPage(getIt.get())); + getIt.registerFactory(() => NFTViewModel(appStore, getIt.get())); getIt.registerFactory(() => SignViewModel(getIt.get().wallet!)); @@ -1512,7 +1513,11 @@ Future setup({ getIt.registerFactory(() => BackgroundSyncLogsViewModel()); getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get())); + + getIt.registerFactory(() => DevNetworkRequests()); + getIt.registerFactory(() => StartTorPage(StartTorViewModel(),)); + getIt.registerFactory(() => DEuroViewModel(getIt())); getIt.registerFactory(() => DEuroSavingsPage(getIt())); diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart index 512244c1b..78b0f4178 100644 --- a/lib/entities/ens_record.dart +++ b/lib/entities/ens_record.dart @@ -1,14 +1,16 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:ens_dart/ens_dart.dart'; -import 'package:http/http.dart'; import 'package:web3dart/web3dart.dart'; class EnsRecord { + static Future fetchEnsAddress(String name, {WalletBase? wallet}) async { + Web3Client? _client; if (wallet != null && wallet.type == WalletType.ethereum) { @@ -20,7 +22,9 @@ class EnsRecord { } if (_client == null) { - _client = Web3Client("https://ethereum-rpc.publicnode.com", Client()); + late final client = ProxyWrapper().getHttpIOClient(); + + _client = Web3Client("https://ethereum-rpc.publicnode.com", client); } try { diff --git a/lib/entities/fio_address_provider.dart b/lib/entities/fio_address_provider.dart index a88804c97..dcfd79c96 100644 --- a/lib/entities/fio_address_provider.dart +++ b/lib/entities/fio_address_provider.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class FioAddressProvider { static const apiAuthority = 'fio.blockpane.com'; @@ -13,13 +13,17 @@ class FioAddressProvider { final body = {"fio_name": fioAddress}; final uri = Uri.https(apiAuthority, availCheck); - final response = - await http.post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); if (response.statusCode != 200) { return isFioRegistered; } + final responseJSON = json.decode(response.body) as Map; isFioRegistered = responseJSON['is_registered'] as int == 1; @@ -35,9 +39,13 @@ class FioAddressProvider { }; final uri = Uri.https(apiAuthority, getAddress); - final response = - await http.post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error'] as String; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 9e384f462..0f3c1fe48 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -66,6 +66,7 @@ class PreferencesKey { static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; static const syncModeKey = 'sync_mode'; static const syncAllKey = 'sync_all'; + static const builtinTorKey = 'builtin_tor'; static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; static const sortBalanceBy = 'sort_balance_by'; diff --git a/lib/entities/unstoppable_domain_address.dart b/lib/entities/unstoppable_domain_address.dart index a047c85d9..cf6eb8945 100644 --- a/lib/entities/unstoppable_domain_address.dart +++ b/lib/entities/unstoppable_domain_address.dart @@ -1,15 +1,16 @@ import 'dart:convert'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; Future fetchUnstoppableDomainAddress(String domain, String ticker) async { var address = ''; try { final uri = Uri.parse("https://api.unstoppabledomains.com/profile/public/${Uri.encodeQueryComponent(domain)}?fields=records"); - final jsonString = await http.read(uri); - final jsonParsed = json.decode(jsonString) as Map; + final response = await ProxyWrapper().get(clearnetUri: uri); + + final jsonParsed = json.decode(response.body) as Map; if (jsonParsed["records"] == null) { throw Exception(".records response from $uri is empty"); }; diff --git a/lib/entities/wellknown_record.dart b/lib/entities/wellknown_record.dart index dbe808281..6dd94440b 100644 --- a/lib/entities/wellknown_record.dart +++ b/lib/entities/wellknown_record.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class WellKnownRecord { WellKnownRecord({ @@ -40,14 +40,15 @@ class WellKnownRecord { } // lookup domain/.well-known/nano-currency.json and check if it has a nano address: - final http.Response response = await http.get( - Uri.parse("https://$domain/.well-known/$jsonLocation.json?names=$name"), + final response = await ProxyWrapper().get( + clearnetUri: Uri.parse("https://$domain/.well-known/$jsonLocation.json?names=$name"), headers: {"Accept": "application/json"}, ); if (response.statusCode != 200) { return null; } + final Map decoded = json.decode(response.body) as Map; // Access the first element in the names array and retrieve its address diff --git a/lib/entities/zano_alias.dart b/lib/entities/zano_alias.dart index 1ddf95178..ec966e225 100644 --- a/lib/entities/zano_alias.dart +++ b/lib/entities/zano_alias.dart @@ -1,14 +1,14 @@ import 'dart:convert'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class ZanoAlias { static Future fetchZanoAliasAddress(String alias) async { try { final uri = Uri.parse("http://zano.cakewallet.com:11211/json_rpc"); - final response = await http.post( - uri, + final response = await ProxyWrapper().post( + clearnetUri: uri, body: json.encode({ "id": 0, "jsonrpc": "2.0", @@ -16,6 +16,7 @@ class ZanoAlias { "params": {"alias": alias} }), ); + final jsonParsed = json.decode(response.body) as Map; return jsonParsed['result']['alias_details']['address'] as String?; diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart index a2c27b745..c2e2f385c 100644 --- a/lib/exchange/provider/chainflip_exchange_provider.dart +++ b/lib/exchange/provider/chainflip_exchange_provider.dart @@ -12,7 +12,7 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class ChainflipExchangeProvider extends ExchangeProvider { ChainflipExchangeProvider({required this.tradesStore}) @@ -275,7 +275,8 @@ class ChainflipExchangeProvider extends ExchangeProvider { Future> _getRequest(String path, Map params) async { final uri = Uri.https(_baseURL, path, params); - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if ((response.statusCode != 200) || (response.body.contains('error'))) { throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); @@ -287,7 +288,8 @@ class ChainflipExchangeProvider extends ExchangeProvider { Future?> _getStatus(Map params) async { final uri = Uri.https(_baseURL, _txInfoPath, params); - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 404) return null; diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart index 79f8d70d4..2cbd97dd2 100644 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -13,11 +13,11 @@ import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/distribution_info.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; - +import 'package:cw_core/utils/proxy_wrapper.dart'; class ChangeNowExchangeProvider extends ExchangeProvider { ChangeNowExchangeProvider({required SettingsStore settingsStore}) : _settingsStore = settingsStore, @@ -73,7 +73,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { 'flow': _getFlow(isFixedRateMode) }; final uri = Uri.https(apiAuthority, rangePath, params); - final response = await get(uri, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -118,7 +119,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { params['fromAmount'] = amount.toString(); final uri = Uri.https(apiAuthority, estimatedAmountPath, params); - final response = await get(uri, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + final responseJSON = json.decode(response.body) as Map; final fromAmount = double.parse(responseJSON['fromAmount'].toString()); final toAmount = double.parse(responseJSON['toAmount'].toString()); @@ -177,7 +179,12 @@ class ChangeNowExchangeProvider extends ExchangeProvider { } final uri = Uri.https(apiAuthority, createTradePath); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -220,7 +227,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final headers = {apiHeaderKey: apiKey}; final params = {'id': id}; final uri = Uri.https(apiAuthority, findTradeByIdPath, params); - final response = await get(uri, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index 43f63b8ca..2d5011fe0 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -9,9 +9,9 @@ import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class ExolixExchangeProvider extends ExchangeProvider { ExolixExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -86,8 +86,9 @@ class ExolixExchangeProvider extends ExchangeProvider { // Maximum of 2 attempts to fetch limits for (int i = 0; i < 2; i++) { final uri = Uri.https(apiBaseUrl, ratePath, params); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + + if (response.statusCode == 200) { final responseJSON = json.decode(response.body) as Map; final minAmount = responseJSON['minAmount']; @@ -133,7 +134,8 @@ class ExolixExchangeProvider extends ExchangeProvider { params['amount'] = amount.toString(); final uri = Uri.https(apiBaseUrl, ratePath, params); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final responseJSON = json.decode(response.body) as Map; if (response.statusCode != 200) { @@ -172,7 +174,12 @@ class ExolixExchangeProvider extends ExchangeProvider { body['amount'] = request.fromAmount; final uri = Uri.https(apiBaseUrl, transactionsPath); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -214,8 +221,8 @@ class ExolixExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { final findTradeByIdPath = '$transactionsPath/$id'; final uri = Uri.https(apiBaseUrl, findTradeByIdPath); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); if (response.statusCode == 400) { diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart index d07297fcf..1fe1e9e35 100644 --- a/lib/exchange/provider/letsexchange_exchange_provider.dart +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -10,9 +10,9 @@ import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; class LetsExchangeExchangeProvider extends ExchangeProvider { LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -152,7 +152,11 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { final uri = Uri.https(_baseUrl, isFixedRateMode ? _createTransactionRevertPath : _createTransactionPath, tradeParams); - final response = await http.post(uri, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + ); + if (response.statusCode != 200) { throw Exception('LetsExchange create trade failed: ${response.body}'); @@ -218,7 +222,8 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { }; final url = Uri.https(_baseUrl, '$_getTransactionPath/$id'); - final response = await http.get(url, headers: headers); + final response = await ProxyWrapper().get(clearnetUri: url, headers: headers); + if (response.statusCode != 200) { throw Exception('LetsExchange fetch trade failed: ${response.body}'); @@ -266,7 +271,11 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { try { final uri = Uri.https(_baseUrl, isFixedRateMode ? _infoRevertPath : _infoPath, params); - final response = await http.post(uri, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + ); + if (response.statusCode != 200) { throw Exception('LetsExchange fetch info failed: ${response.body}'); } diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart index 12ec59100..3c9b2d951 100644 --- a/lib/exchange/provider/sideshift_exchange_provider.dart +++ b/lib/exchange/provider/sideshift_exchange_provider.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:developer'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; @@ -11,9 +10,9 @@ import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class SideShiftExchangeProvider extends ExchangeProvider { SideShiftExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -60,8 +59,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { Future checkIsAvailable() async { const url = apiBaseUrl + permissionPath; final uri = Uri.parse(url); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 500) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error']['message'] as String; @@ -90,7 +89,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { "$apiBaseUrl$rangePath/${fromCurrency.title.toLowerCase()}-$fromNetwork/${toCurrency.title.toLowerCase()}-$toNetwork"; final uri = Uri.parse(url); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 500) { final responseJSON = json.decode(response.body) as Map; @@ -137,7 +137,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { "$apiBaseUrl$rangePath/$fromCurrency-$depositNetwork/$toCurrency-$settleNetwork?amount=$amount"; final uri = Uri.parse(url); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final responseJSON = json.decode(response.body) as Map; if (response.statusCode == 500) { @@ -186,7 +187,12 @@ class SideShiftExchangeProvider extends ExchangeProvider { final headers = {'Content-Type': 'application/json'}; final uri = Uri.parse(url); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 201) { if (response.statusCode == 400) { @@ -227,8 +233,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { final url = apiBaseUrl + orderPath + '/' + id; final uri = Uri.parse(url); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 404) { throw TradeNotFoundException(id, provider: description); } @@ -281,7 +287,12 @@ class SideShiftExchangeProvider extends ExchangeProvider { 'depositNetwork': _networkFor(request.fromCurrency), }; final uri = Uri.parse(url); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 201) { if (response.statusCode == 400) { diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart index 5391d5f89..cc02ac799 100644 --- a/lib/exchange/provider/simpleswap_exchange_provider.dart +++ b/lib/exchange/provider/simpleswap_exchange_provider.dart @@ -11,8 +11,8 @@ import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:http/http.dart'; class SimpleSwapExchangeProvider extends ExchangeProvider { SimpleSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -48,7 +48,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { @override Future checkIsAvailable() async { final uri = Uri.https(apiAuthority, getEstimatePath, {'api_key': apiKey}); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); return !(response.statusCode == 403); } @@ -66,7 +66,8 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, rangePath, params); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 500) { final responseJSON = json.decode(response.body) as Map; @@ -104,10 +105,10 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { 'fixed': isFixedRateMode.toString() }; final uri = Uri.https(apiAuthority, getEstimatePath, params); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.body == "null") return 0.00; - final data = json.decode(response.body) as String; return double.parse(data) / amount; @@ -134,7 +135,12 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, createExchangePath, params); - final response = await post(uri, headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 200 && response.statusCode != 201) { if (response.statusCode == 400) { @@ -176,8 +182,9 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { final params = {'api_key': apiKey, 'id': id}; final uri = Uri.https(apiAuthority, getExchangePath, params); - final response = await get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + + if (response.statusCode == 404) { throw TradeNotFoundException(id, provider: description); } diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 11b8d768a..8c7efae65 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -10,8 +10,8 @@ import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:http/http.dart' as http; class StealthExExchangeProvider extends ExchangeProvider { StealthExExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -63,8 +63,12 @@ class StealthExExchangeProvider extends ExchangeProvider { }; try { - final response = await http.post(Uri.parse(_baseUrl + _rangePath), - headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(_baseUrl + _rangePath), + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 200) { throw Exception('StealthEx fetch limits failed: ${response.body}'); } @@ -134,8 +138,12 @@ class StealthExExchangeProvider extends ExchangeProvider { 'additional_fee_percent': _additionalFeePercent, }; - final response = await http.post(Uri.parse(_baseUrl + _exchangesPath), - headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(_baseUrl + _exchangesPath), + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 201) { throw Exception('StealthEx create trade failed: ${response.body}'); @@ -202,8 +210,9 @@ class StealthExExchangeProvider extends ExchangeProvider { final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; final uri = Uri.parse('$_baseUrl$_exchangesPath/$id'); - final response = await http.get(uri, headers: headers); - + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); + + if (response.statusCode != 200) { throw Exception('StealthEx fetch trade failed: ${response.body}'); } @@ -260,8 +269,12 @@ class StealthExExchangeProvider extends ExchangeProvider { }; try { - final response = await http.post(Uri.parse(_baseUrl + _amountPath), - headers: headers, body: json.encode(body)); + final response = await ProxyWrapper().post( + clearnetUri: Uri.parse(_baseUrl + _amountPath), + headers: headers, + body: json.encode(body), + ); + if (response.statusCode != 200) return {}; final responseJSON = json.decode(response.body) as Map; final rate = responseJSON['rate'] as Map?; diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart index d3f64b712..f6c8332db 100644 --- a/lib/exchange/provider/swaptrade_exchange_provider.dart +++ b/lib/exchange/provider/swaptrade_exchange_provider.dart @@ -10,9 +10,9 @@ import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class SwapTradeExchangeProvider extends ExchangeProvider { SwapTradeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -69,7 +69,8 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }) async { try { final uri = Uri.https(apiAuthority, getCoins); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final responseJSON = json.decode(response.body) as Map; @@ -116,7 +117,12 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, getRate, params); - final response = await post(uri, body: body, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + body: json.encode(body), + headers: headers, + ); + final responseBody = json.decode(response.body) as Map; if (response.statusCode != 200) @@ -153,7 +159,12 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, createOrder, params); - final response = await post(uri, body: body, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + body: json.encode(body), + headers: headers, + ); + final responseBody = json.decode(response.body) as Map; if (response.statusCode == 400 || responseBody["success"] == false) { @@ -196,7 +207,12 @@ class SwapTradeExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, order, params); - final response = await post(uri, body: body, headers: headers); + final response = await ProxyWrapper().post( + clearnetUri: uri, + body: json.encode(body), + headers: headers, + ); + final responseBody = json.decode(response.body) as Map; if (response.statusCode == 400 || responseBody["success"] == false) { diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart index aa7ab2d27..0cdb2a423 100644 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -7,10 +7,10 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart' as http; class ThorChainExchangeProvider extends ExchangeProvider { ThorChainExchangeProvider({required this.tradesStore}) @@ -164,7 +164,8 @@ class ThorChainExchangeProvider extends ExchangeProvider { if (id.isEmpty) throw Exception('Trade id is empty'); final formattedId = id.startsWith('0x') ? id.substring(2) : id; final uri = Uri.https(_baseNodeURL, '$_txInfoPath$formattedId'); - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode == 404) { throw Exception('Trade not found for id: $formattedId'); @@ -217,8 +218,8 @@ class ThorChainExchangeProvider extends ExchangeProvider { static Future?>? lookupAddressByName(String name) async { final uri = Uri.https(_baseURL, '$_nameLookUpPath$name'); - final response = await http.get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { return null; } @@ -244,8 +245,8 @@ class ThorChainExchangeProvider extends ExchangeProvider { Future> _getSwapQuote(Map params) async { Uri uri = Uri.https(_baseNodeURL, _quotePath, params); - final response = await http.get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { throw Exception('Unexpected HTTP status: ${response.statusCode}'); } diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 170c53627..f3a70b912 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -8,9 +8,9 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart'; class TrocadorExchangeProvider extends ExchangeProvider { TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}}) @@ -52,8 +52,9 @@ class TrocadorExchangeProvider extends ExchangeProvider { ]; static const apiKey = secrets.trocadorApiKey; - static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; static const clearNetAuthority = 'api.trocador.app'; + static const onionApiAuthority = clearNetAuthority; + // static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; static const markup = secrets.trocadorExchangeMarkup; static const newRatePath = '/new_rate'; static const createTradePath = '/new_trade'; @@ -97,7 +98,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { }; final uri = await _getUri(coinPath, params); - final response = await get(uri, headers: {'API-Key': apiKey}); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); @@ -138,12 +140,10 @@ class TrocadorExchangeProvider extends ExchangeProvider { }; final uri = await _getUri(newRatePath, params); - final response = await get(uri, headers: {'API-Key': apiKey}); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); + final responseJSON = json.decode(response.body) as Map; - - if (responseJSON['error'] != null) throw Exception(responseJSON['error']); - final fromAmount = double.parse(responseJSON['amount_from'].toString()); final toAmount = double.parse(responseJSON['amount_to'].toString()); final rateId = responseJSON['trade_id'] as String? ?? ''; @@ -206,8 +206,9 @@ class TrocadorExchangeProvider extends ExchangeProvider { params['provider'] = _provider.first as String; final uri = await _getUri(createTradePath, params); - final response = await get(uri, headers: {'API-Key': apiKey}); - + final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); + + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error'] as String; @@ -255,9 +256,10 @@ class TrocadorExchangeProvider extends ExchangeProvider { @override Future findTradeById({required String id}) async { final uri = await _getUri(tradePath, {'id': id}); - return get(uri, headers: {'API-Key': apiKey}).then((response) { + return ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}).then((response) async { if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); + final responseListJson = json.decode(response.body) as List; final responseJSON = responseListJson.first; @@ -292,7 +294,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { Future> fetchProviders() async { final uri = await _getUri(providersListPath, {'api_key': apiKey}); - final response = await get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); @@ -355,7 +358,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { if (useTorOnly) return uri; try { - await get(uri); + await ProxyWrapper().get(clearnetUri: uri); return uri; } catch (e) { diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart index 5611d6855..e70eab568 100644 --- a/lib/exchange/provider/xoswap_exchange_provider.dart +++ b/lib/exchange/provider/xoswap_exchange_provider.dart @@ -10,8 +10,7 @@ import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; - +import 'package:cw_core/utils/proxy_wrapper.dart'; class XOSwapExchangeProvider extends ExchangeProvider { XOSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported)); @@ -72,7 +71,8 @@ class XOSwapExchangeProvider extends ExchangeProvider { final uri = Uri.https(_apiAuthority, _apiPath + _assets, {'networks': normalizedNetwork, 'query': currency.title}); - final response = await http.get(uri, headers: _headers); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { throw Exception('Failed to fetch assets for ${currency.title} on ${currency.tag}'); } @@ -102,7 +102,8 @@ class XOSwapExchangeProvider extends ExchangeProvider { if (curFrom == null || curTo == null) return []; final pairId = curFrom + '_' + curTo; final uri = Uri.https(_apiAuthority, '$_apiPath$_pairsPath/$pairId$_ratePath'); - final response = await http.get(uri, headers: _headers); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) return []; return json.decode(response.body) as List; } catch (e) { @@ -205,7 +206,12 @@ class XOSwapExchangeProvider extends ExchangeProvider { 'pairId': pairId, }; - final response = await http.post(uri, headers: _headers, body: json.encode(payload)); + final response = await ProxyWrapper().post( + clearnetUri: uri, + headers: _headers, + body: json.encode(payload), + ); + if (response.statusCode != 201) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error'] ?? 'Unknown error'; @@ -254,7 +260,8 @@ class XOSwapExchangeProvider extends ExchangeProvider { Future findTradeById({required String id}) async { try { final uri = Uri.https(_apiAuthority, '$_apiPath$_orders/$id'); - final response = await http.get(uri, headers: _headers); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { final responseJSON = json.decode(response.body) as Map; if (responseJSON.containsKey('code') && responseJSON['code'] == 'NOT_FOUND') { diff --git a/lib/main.dart b/lib/main.dart index 6795b7ff9..709304939 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,9 +25,12 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/root/root.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/themes/core/material_base_theme.dart'; import 'package:cake_wallet/themes/utils/theme_provider.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/address_info.dart'; @@ -38,6 +41,8 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; @@ -87,6 +92,9 @@ Future runAppWithZone({Key? topLevelKey}) async { ledgerFile.writeAsStringSync("$content\n${event.message}"); }); } + if (FeatureFlag.hasDevOptions) { + ProxyWrapper.logger = MemoryProxyLogger(); + } runApp(App(key: topLevelKey)); isAppRunning = true; @@ -194,8 +202,8 @@ Future initializeAppConfigs({bool loadWallet = true}) async { final powNodes = await CakeHive.openBox(Node.boxName + "pow"); // must be different from Node.boxName final transactionDescriptions = await CakeHive.openBox( - TransactionDescription.boxName, - encryptionKey: transactionDescriptionsBoxKey); + TransactionDescription.boxName, + encryptionKey: transactionDescriptionsBoxKey); final trades = await CakeHive.openBox(Trade.boxName, encryptionKey: tradesBoxKey); final orders = await CakeHive.openBox(Order.boxName, encryptionKey: ordersBoxKey); final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); @@ -279,7 +287,11 @@ Future initialSetup({ navigatorKey: navigatorKey, secureStorage: secureStorage, ); - await bootstrap(navigatorKey, loadWallet: loadWallet); + await bootstrapOffline(); + final settingsStore = getIt(); + if (!settingsStore.currentBuiltinTor) { + bootstrapOnline(navigatorKey, loadWallet: loadWallet); + } } class App extends StatefulWidget { @@ -293,20 +305,24 @@ class App extends StatefulWidget { class AppState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { - return Observer( - builder: (BuildContext context) { + return Observer(builder: (BuildContext context) { final appStore = getIt.get(); final authService = getIt.get(); final linkViewModel = getIt.get(); final tradeMonitor = getIt.get(); + final settingsStore = appStore.settingsStore; final statusBarColor = Colors.transparent; final authenticationStore = getIt.get(); final initialRoute = authenticationStore.state == AuthenticationState.uninitialized - ? Routes.welcome - : Routes.login; + ? Routes.welcome + : settingsStore.currentBuiltinTor ? Routes.startTor : Routes.login; final currentTheme = appStore.themeStore.currentTheme; - final statusBarBrightness = currentTheme.isDark ? Brightness.light : Brightness.dark; - final statusBarIconBrightness = currentTheme.isDark ? Brightness.light : Brightness.dark; + final statusBarBrightness = currentTheme.type == currentTheme.isDark + ? Brightness.light + : Brightness.dark; + final statusBarIconBrightness = currentTheme.type == currentTheme.isDark + ? Brightness.light + : Brightness.dark; SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( statusBarColor: statusBarColor, statusBarBrightness: statusBarBrightness, @@ -322,7 +338,8 @@ class AppState extends State with SingleTickerProviderStateMixin { tradeMonitor: tradeMonitor, child: ThemeProvider( themeStore: appStore.themeStore, - materialAppBuilder: (context, theme, darkTheme, themeMode) => MaterialApp( + materialAppBuilder: (context, theme, darkTheme, themeMode) => + MaterialApp( navigatorObservers: [routeObserver], navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, @@ -365,8 +382,10 @@ class _HomeState extends State<_Home> { SystemChrome.setPreferredOrientations( [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); } else { - SystemChrome.setPreferredOrientations( - [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight + ]); } } } diff --git a/lib/mastodon/mastodon_api.dart b/lib/mastodon/mastodon_api.dart index a2fdc97bd..58767eea7 100644 --- a/lib/mastodon/mastodon_api.dart +++ b/lib/mastodon/mastodon_api.dart @@ -1,6 +1,6 @@ import 'dart:convert'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:http/http.dart' as http; import 'package:cake_wallet/mastodon/mastodon_user.dart'; class MastodonAPI { @@ -20,7 +20,8 @@ class MastodonAPI { queryParameters: queryParams, ); - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) return null; @@ -47,7 +48,8 @@ class MastodonAPI { queryParameters: queryParams, ); - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + if (response.statusCode != 200) { throw Exception('Unexpected HTTP status: ${response.statusCode}'); diff --git a/lib/reactions/bootstrap.dart b/lib/reactions/bootstrap.dart index e767433aa..6fb4c5ab2 100644 --- a/lib/reactions/bootstrap.dart +++ b/lib/reactions/bootstrap.dart @@ -15,24 +15,29 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; -Future bootstrap(GlobalKey navigatorKey, {required bool loadWallet}) async { - final appStore = getIt.get(); +Future bootstrapOffline() async { final authenticationStore = getIt.get(); - final settingsStore = getIt.get(); - final fiatConversionStore = getIt.get(); final currentWalletName = getIt.get().getString(PreferencesKey.currentWalletName); if (currentWalletName != null) { authenticationStore.installed(); } +} + +void bootstrapOnline(GlobalKey navigatorKey, {required bool loadWallet}) { + final appStore = getIt.get(); + final authenticationStore = getIt.get(); + final settingsStore = getIt.get(); + final fiatConversionStore = getIt.get(); if (loadWallet) { startAuthenticationStateChange(authenticationStore, navigatorKey); } + startCurrentWalletChangeReaction(appStore, settingsStore, fiatConversionStore); startCurrentFiatChangeReaction(appStore, settingsStore, fiatConversionStore); startCurrentFiatApiModeChangeReaction(appStore, settingsStore, fiatConversionStore); startOnCurrentNodeChangeReaction(appStore); startFiatRateUpdate(appStore, settingsStore, fiatConversionStore); -} +} \ No newline at end of file diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index 1e8fa88fa..4591bb9ae 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -1,11 +1,15 @@ import 'dart:async'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/utils/tor.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; Timer? _checkConnectionTimer; @@ -36,6 +40,10 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore final alive = await settingsStore.getCurrentNode(wallet.type).requestNode(); if (alive) { + if (settingsStore.currentBuiltinTor) { + await ensureTorStarted(context: null); + } + await wallet.connectToNode(node: settingsStore.getCurrentNode(wallet.type)); } } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 08f1a25ee..7b7199b14 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/utils/tor.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; @@ -83,6 +84,10 @@ void startCurrentWalletChangeReaction( bitcoin!.updatePayjoinState(wallet, settingsStore.usePayjoin); } + if (settingsStore.currentBuiltinTor) { + await ensureTorStarted(context: null); + } + await wallet.connectToNode(node: node); if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) { final powNode = settingsStore.getCurrentPowNode(wallet.type); diff --git a/lib/router.dart b/lib/router.dart index afb1b46f8..00b0c5bb9 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -38,6 +38,7 @@ import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/sign_page.dart'; import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart'; import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart'; +import 'package:cake_wallet/src/screens/dev/network_requests.dart'; import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart'; @@ -93,7 +94,6 @@ import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; -import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; @@ -101,6 +101,7 @@ import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_info_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_qr_page.dart'; import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; +import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart'; import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; @@ -857,9 +858,6 @@ Route createRoute(RouteSettings settings) { ), ); - case Routes.torPage: - return MaterialPageRoute(builder: (_) => getIt.get()); - case Routes.signPage: return MaterialPageRoute( builder: (_) => SignPage( @@ -910,7 +908,12 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); - + + case Routes.devNetworkRequests: + return MaterialPageRoute( + builder: (_) => getIt.get(), + ); + case Routes.devMoneroCallProfiler: return MaterialPageRoute( builder: (_) => getIt.get(), @@ -920,6 +923,11 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get(), ); + + case Routes.startTor: + return MaterialPageRoute( + builder: (_) => getIt.get(), + ); case Routes.dEuroSavings: return MaterialPageRoute( diff --git a/lib/routes.dart b/lib/routes.dart index addaec1d9..6ff083bda 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -110,14 +110,16 @@ class Routes { static const walletConnectConnectionsListing = '/wallet-connect-connections-listing'; static const nftDetailsPage = '/nft_details_page'; static const importNFTPage = '/import_nft_page'; - static const torPage = '/tor_page'; static const backgroundSync = '/background_sync'; + static const startTor = '/start_tor'; static const devMoneroBackgroundSync = '/dev/monero_background_sync'; static const devMoneroCallProfiler = '/dev/monero_call_profiler'; + static const devSharedPreferences = '/dev/shared_preferences'; static const devSecurePreferences = '/dev/secure_preferences'; static const devBackgroundSyncLogs = '/dev/background_sync_logs'; + static const devNetworkRequests = '/dev/network_requests'; static const signPage = '/sign_page'; static const connectDevices = '/device/connect'; diff --git a/lib/src/screens/dev/network_requests.dart b/lib/src/screens/dev/network_requests.dart new file mode 100644 index 000000000..912c79da7 --- /dev/null +++ b/lib/src/screens/dev/network_requests.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/view_model/dev/network_requests_view_model.dart'; +import 'package:cw_core/utils/proxy_logger/abstract.dart'; +import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class DevNetworkRequests extends BasePage { + final NetworkRequestsViewModel viewModel = NetworkRequestsViewModel(); + + DevNetworkRequests() { + viewModel.loadLogs(); + } + + @override + String? get title => "[dev] network requests"; + + @override + Widget? trailing(BuildContext context) { + return IconButton( + icon: Icon(Icons.refresh), + onPressed: () => viewModel.loadLogs(), + ); + } + + @override + Widget body(BuildContext context) { + return Observer( + builder: (_) { + if (viewModel.logs.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("No logs loaded"), + SizedBox(height: 16), + ElevatedButton( + onPressed: () => viewModel.loadLogs(), + child: Text("Load Logs"), + ), + ], + ), + ); + } else { + return ListView.builder( + itemCount: viewModel.logs.length, + itemBuilder: (BuildContext context, int i) { + final item = viewModel.logs[i]; + return ListTile( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return DevRequestDetails(item); + }, + ), + ); + }, + leading: switch (item.network) { + RequestNetwork.clearnet => Text("C"), + RequestNetwork.tor => Text("T"), + }, + trailing: switch (item.method) { + RequestMethod.get => Text("GET"), + RequestMethod.post => Text("POST"), + RequestMethod.put => Text("PUT"), + RequestMethod.delete => Text("DELETE"), + RequestMethod.newHttpClient || + RequestMethod.newHttpIOClient || + RequestMethod.newProxySocket => null, + }, + title: Text(item.time.toIso8601String()), + subtitle: switch (item.method) { + RequestMethod.get || + RequestMethod.post || + RequestMethod.put || + RequestMethod.delete => Text("${item.uri}"), + RequestMethod.newHttpClient => Text("newHttpClient"), + RequestMethod.newHttpIOClient => Text("newHttpIOClient"), + RequestMethod.newProxySocket => Text("newProxySocket"), + }, + tileColor: item.error != null ? Colors.red : null, + ); + }, + ); + } + }, + ); + } +} + +class DevRequestDetails extends BasePage { + final MemoryProxyLoggerEntry req; + DevRequestDetails(this.req); + + @override + Widget body(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: _body(context), + ); + } + + Widget _body(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle("Time"), + SelectableText(req.time.toString()), + + _sectionTitle("Method"), + SelectableText(req.method.toString()), + + _sectionTitle("URI"), + SelectableText(req.uri?.toString() ?? "null"), + + _sectionTitle("Network"), + SelectableText(req.network.toString()), + + _sectionTitle("Body (as UTF-8)"), + SelectableText(_tryDecodeBody(req.body)), + + _sectionTitle("Response"), + SelectableText(req.response?.body ?? "null"), + + _sectionTitle("Error"), + SelectableText(req.error ?? "No error"), + + _sectionTitle("Stack Trace"), + SelectableText(req.trace.toString()), + ], + ); + } + + Widget _sectionTitle(String title) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + ); + } + + String _tryDecodeBody(Uint8List body) { + try { + return utf8.decode(body); + } catch (_) { + return 'Binary data (${body.length} bytes)'; + } + } +} diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 381a9fec9..11bfea20a 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -196,8 +196,27 @@ class NodeForm extends StatelessWidget { Observer( builder: (_) => Column( children: [ + if (nodeViewModel.usesEmbeddedProxy) ...[ + Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + StandardCheckbox( + value: nodeViewModel.usesEmbeddedProxy, + gradientBackground: false, + borderColor: Theme.of(context).dividerColor, + iconColor: Theme.of(context).colorScheme.primary, + onChanged: null, + caption: 'Embedded Tor SOCKS Proxy', + ), + ], + ), + ), + ], Padding( - padding: EdgeInsets.only(top: 20), + padding: EdgeInsets.only(top: 10), child: Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index 901b96e42..61a88cf12 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/wallet_connect_button.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; @@ -71,10 +72,28 @@ class ConnectionSyncPage extends BasePage { ), ], if (FeatureFlag.isInAppTorEnabled) - SettingsCellWithArrow( - title: S.current.tor_connection, - handler: (context) => Navigator.of(context).pushNamed(Routes.torPage), - ), + Observer(builder: (context) { + return SettingsSwitcherCell( + leading: Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: Colors.red.shade100, + borderRadius: BorderRadius.circular(3), + ), + child: Text( + 'Alpha', + style: TextStyle( + color: Colors.red.shade700, + fontSize: 8, + fontWeight: FontWeight.bold, + ), + ), + ), + title: S.current.enable_builtin_tor, + value: dashboardViewModel.builtinTor, + onValueChange: (_, bool value) => dashboardViewModel.setBuiltinTor(value, context), + ); + }), ], ), ); diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index a52f9ae06..2ff9dcdaf 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -93,6 +93,12 @@ class OtherSettingsPage extends BasePage { handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.devBackgroundSyncLogs), ), + if (FeatureFlag.hasDevOptions) + SettingsCellWithArrow( + title: '[dev] network requests logs', + handler: (BuildContext context) => + Navigator.of(context).pushNamed(Routes.devNetworkRequests), + ), Spacer(), SettingsVersionCell( title: S.of(context).version(_otherSettingsViewModel.currentVersion)), diff --git a/lib/src/screens/settings/tor_page.dart b/lib/src/screens/settings/tor_page.dart deleted file mode 100644 index 55d96bb60..000000000 --- a/lib/src/screens/settings/tor_page.dart +++ /dev/null @@ -1,269 +0,0 @@ -import 'dart:async'; - -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/themes/utils/custom_theme_colors.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:flutter/material.dart'; -// import 'package:tor/tor.dart'; - -class TorPage extends BasePage { - final AppStore appStore; - - TorPage(this.appStore); - - @override - Widget body(BuildContext context) { - return TorPageBody(appStore); - } -} - -class TorPageBody extends StatefulWidget { - final AppStore appStore; - - const TorPageBody(this.appStore, {Key? key}) : super(key: key); - - @override - State createState() => _TorPageBodyState(); -} - -class _TorPageBodyState extends State { - bool torEnabled = false; - bool connecting = false; - - // Set the default text for the host input field. - final hostController = TextEditingController(text: 'https://icanhazip.com/'); - - // https://check.torproject.org is another good option. - - Future startTor() async { - setState(() { - connecting = true; // Update flag - }); - - // await Tor.init(); - // - // // Start the proxy - // await Tor.instance.start(); - // - // // Toggle started flag. - // setState(() { - // torEnabled = Tor.instance.enabled; // Update flag - // connecting = false; - // }); - // - // final node = widget.appStore.settingsStore.getCurrentNode(widget.appStore.wallet!.type); - // if (node.socksProxyAddress?.isEmpty ?? true) { - // node.socksProxyAddress = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; - // } - // widget.appStore.wallet!.connectToNode(node: node); - - printV('Done awaiting; tor should be running'); - } - - Future endTor() async { - // // Start the proxy - // Tor.instance.disable(); - // - // // Toggle started flag. - // setState(() { - // torEnabled = Tor.instance.enabled; // Update flag - // }); - // - // printV('Done awaiting; tor should be stopped'); - } - // - // @override - // void initState() { - // super.initState(); - // - // torEnabled = Tor.instance.enabled; - // } - - @override - void dispose() { - // Clean up the controller when the widget is disposed. - hostController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Container( - padding: const EdgeInsets.all(10), - child: connecting - ? ConnectingScreen() - : torEnabled - ? DisconnectScreen(disconnect: endTor) - : ConnectScreen(connect: startTor), - ), - ); - } -} - -class ConnectScreen extends StatelessWidget { - final Function() connect; - - const ConnectScreen({super.key, required this.connect}); - - @override - Widget build(BuildContext context) { - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 200, - height: 200, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.primary, - ), - child: Icon( - Icons.lock, - color: Theme.of(context).colorScheme.onPrimary, - size: 100, - ), - ), - SizedBox(height: 48), - Text( - 'Connect to Tor', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 16), - Text( - 'Your connection to the Tor network ensures privacy and security.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 48), - ElevatedButton( - onPressed: connect, - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - 'Connect', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 18, - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - ), - ], - ), - ), - ); - } -} - -class DisconnectScreen extends StatelessWidget { - final Function() disconnect; - - const DisconnectScreen({super.key, required this.disconnect}); - - @override - Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 200, - height: 200, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: CustomThemeColors.syncGreen, - ), - child: Icon( - Icons.check, - color: Theme.of(context).colorScheme.onPrimary, - size: 100, - ), - ), - SizedBox(height: 20), - Text( - 'Connected to Tor', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 10), - Text( - 'You are currently connected to the Tor network.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 30), - ElevatedButton( - onPressed: disconnect, - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - 'Disconnect', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 18, - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - ), - ], - ), - ); - } -} - -class ConnectingScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 200, - height: 200, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: CustomThemeColors.syncYellow, - ), - child: Icon( - Icons.hourglass_bottom, - color: Theme.of(context).colorScheme.onPrimary, - size: 100, - ), - ), - SizedBox(height: 20), - Text( - 'Connecting...', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 10), - ], - ), - ); - } -} diff --git a/lib/src/screens/start_tor/start_tor_page.dart b/lib/src/screens/start_tor/start_tor_page.dart new file mode 100644 index 000000000..97c5c6599 --- /dev/null +++ b/lib/src/screens/start_tor/start_tor_page.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/view_model/start_tor_view_model.dart'; + +class StartTorPage extends BasePage { + StartTorPage(this.startTorViewModel); + + final StartTorViewModel startTorViewModel; + + @override + String get title => S.current.tor_connection; + + @override + Widget leading(BuildContext context) { + return Container(); + } + + @override + Widget body(BuildContext context) { + startTorViewModel.startTor(context); + return Container( + padding: EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + SizedBox(width: double.maxFinite), + if (startTorViewModel.isLoading) ...[ + CircularProgressIndicator(), + SizedBox(height: 20), + _buildWaitingText(context), + ], + if (startTorViewModel.showOptions) ...[ + _buildOptionsButtons(context), + ], + ], + ), + ], + ), + ); + } + + Widget _buildWaitingText(BuildContext context) { + return Observer( + builder: (_) { + return Column( + children: [ + Text( + S.current.establishing_tor_connection, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + if (startTorViewModel.showOptions) _buildOptionsButtons(context), + ], + ); + }, + ); + } + + Widget _buildOptionsButtons(BuildContext context) { + return Column( + children: [ + Text( + S.current.tor_connection_timeout, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 24), + PrimaryButton( + onPressed: () => startTorViewModel.disableTor(context), + text: S.current.disable_tor, + color: Theme.of(context).colorScheme.primary, + textColor: Colors.white, + ), + SizedBox(height: 16), + PrimaryButton( + onPressed: () => startTorViewModel.ignoreAndLaunchApp(context), + text: S.current.ignor, + color: Theme.of(context).colorScheme.secondary, + textColor: Colors.white, + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart index 05c2a1f55..426c1580f 100644 --- a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart +++ b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:reown_walletkit/reown_walletkit.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; @@ -41,7 +41,7 @@ class EvmChainServiceImpl { }) : ethClient = web3Client ?? Web3Client( appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(), - http.Client(), + ProxyWrapper().getHttpIOClient(), ) { for (final event in EventsConstants.allEvents) { walletKit.registerEventEmitter( @@ -563,14 +563,15 @@ class EvmChainServiceImpl { }, ); - final response = await http.get( - uri, + final response = await ProxyWrapper().get( + clearnetUri: uri, headers: { "Accept": "application/json", "X-API-Key": secrets.moralisApiKey, }, ); + final decodedResponse = jsonDecode(response.body)[0] as Map; final symbol = (decodedResponse['symbol'] ?? '') as String; diff --git a/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart b/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart index e60b452eb..a109fa9f3 100644 --- a/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart +++ b/lib/src/screens/wallet_connect/wc_pairing_detail_page.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_servic import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:reown_walletkit/reown_walletkit.dart'; @@ -153,7 +154,10 @@ class WCCDetailsWidget extends BasePage { child: CircleAvatar( backgroundImage: (pairing.peerMetadata!.icons.isNotEmpty ? NetworkImage(pairing.peerMetadata!.icons[0]) - : const AssetImage('assets/images/app_logo.png')) as ImageProvider, + : AssetImage( + CakeTor.instance.enabled + ? 'assets/images/tor_logo.svg' + : 'assets/images/app_logo.png')) as ImageProvider, ), ), const SizedBox(height: 20.0), diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 1c4b73bf8..917891854 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -126,9 +126,9 @@ class _WalletKeysPageBodyState extends State dividerColor: Colors.transparent, padding: EdgeInsets.zero, tabs: [ - Tab(text: S.of(context).widgets_seed), - if (showKeyTab) Tab(text: S.of(context).keys), - if (showLegacySeedTab) Tab(text: S.of(context).legacy), + Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')), + if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),), + if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')), ], ), ), diff --git a/lib/src/widgets/provider_optoin_tile.dart b/lib/src/widgets/provider_optoin_tile.dart index 7d517bfa4..337e50c9e 100644 --- a/lib/src/widgets/provider_optoin_tile.dart +++ b/lib/src/widgets/provider_optoin_tile.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -265,7 +266,11 @@ class Badge extends StatelessWidget { } Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) { - final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + if (CakeTor.instance.enabled && isNetworkImage) { + imagePath = "assets/images/tor_logo.svg"; + isNetworkImage = false; + } final bool isSvg = imagePath.endsWith('.svg'); final double imageHeight = height ?? 35; final double imageWidth = width ?? 35; diff --git a/lib/src/widgets/standard_checkbox.dart b/lib/src/widgets/standard_checkbox.dart index 946942aa5..64de5337d 100644 --- a/lib/src/widgets/standard_checkbox.dart +++ b/lib/src/widgets/standard_checkbox.dart @@ -16,7 +16,7 @@ class StandardCheckbox extends StatelessWidget { final Color? borderColor; final Color? iconColor; final Color? captionColor; - final Function(bool) onChanged; + final Function(bool)? onChanged; @override Widget build(BuildContext context) { @@ -40,7 +40,7 @@ class StandardCheckbox extends StatelessWidget { BoxDecoration(border: boxBorder, borderRadius: BorderRadius.all(Radius.circular(8.0))); return GestureDetector( - onTap: () => onChanged(!value), + onTap: onChanged == null ? null : () => onChanged!(!value), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index c88590475..958f682fe 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -80,6 +80,7 @@ abstract class SettingsStoreBase with Store { required String initialLanguageCode, required SyncMode initialSyncMode, required bool initialSyncAll, + required bool initialBuiltinTor, // required String initialCurrentLocale, required this.appVersion, required this.deviceName, @@ -184,6 +185,7 @@ abstract class SettingsStoreBase with Store { initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, currentSyncMode = initialSyncMode, currentSyncAll = initialSyncAll, + currentBuiltinTor = initialBuiltinTor, priority = ObservableMap() { //this.nodes = ObservableMap.of(nodes); @@ -404,6 +406,11 @@ abstract class SettingsStoreBase with Store { sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); }); + reaction((_) => currentBuiltinTor, (bool builtinTor) { + sharedPreferences.setBool(PreferencesKey.builtinTorKey, builtinTor); + }); + + reaction( (_) => exchangeStatus, (ExchangeApiMode mode) => @@ -821,6 +828,9 @@ abstract class SettingsStoreBase with Store { @observable bool currentSyncAll; + @observable + bool currentBuiltinTor; + String appVersion; String deviceName; @@ -1168,6 +1178,7 @@ abstract class SettingsStoreBase with Store { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 2); // default to 2 - daily sync }); final savedSyncAll = sharedPreferences.getBool(PreferencesKey.syncAllKey) ?? true; + final builtinTor = sharedPreferences.getBool(PreferencesKey.builtinTorKey) ?? false; // migrated to secure: final timeOutDuration = await SecureKey.getInt( @@ -1353,6 +1364,7 @@ abstract class SettingsStoreBase with Store { initialSyncAll: savedSyncAll, shouldShowYatPopup: shouldShowYatPopup, shouldShowRepWarning: shouldShowRepWarning, + initialBuiltinTor: builtinTor, ); } diff --git a/lib/store/yat/yat_store.dart b/lib/store/yat/yat_store.dart index 964b96db3..6b73c6211 100644 --- a/lib/store/yat/yat_store.dart +++ b/lib/store/yat/yat_store.dart @@ -5,12 +5,8 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_type.dart'; -import 'dart:convert'; -import 'package:cake_wallet/store/yat/yat_exception.dart'; -import 'package:http/http.dart'; import 'dart:async'; part 'yat_store.g.dart'; diff --git a/lib/twitter/twitter_api.dart b/lib/twitter/twitter_api.dart index 5acb00e2a..cccf769ad 100644 --- a/lib/twitter/twitter_api.dart +++ b/lib/twitter/twitter_api.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/twitter/twitter_user.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; class TwitterApi { static const twitterBearerToken = secrets.twitterBearerToken; @@ -23,13 +23,14 @@ class TwitterApi { path: userPath + userName, queryParameters: queryParams); - final response = await http.get(uri, headers: headers).catchError((error) { + final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers).catchError((error) { throw Exception('HTTP request failed: $error'); }); if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } + final Map responseJSON = jsonDecode(response.body) as Map; if (responseJSON['errors'] != null && diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index c968c5d75..b9314bc6e 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -1,10 +1,11 @@ import 'package:flutter/foundation.dart'; +import 'dart:io'; class FeatureFlag { static const bool isCakePayEnabled = false; static const bool isExolixEnabled = true; - static const bool isInAppTorEnabled = false; static const bool isBackgroundSyncEnabled = true; + static final bool isInAppTorEnabled = (Platform.isAndroid); static const int verificationWordsCount = kDebugMode ? 0 : 2; static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode); } \ No newline at end of file diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart index 746fde453..994a3957b 100644 --- a/lib/utils/image_utill.dart +++ b/lib/utils/image_utill.dart @@ -1,13 +1,17 @@ +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; class ImageUtil { static Widget getImageFromPath({required String imagePath, double? height, double? width}) { - final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + if (CakeTor.instance.enabled && isNetworkImage) { + imagePath = "assets/images/tor_logo.svg"; + isNetworkImage = false; + } final bool isSvg = imagePath.endsWith('.svg'); final double _height = height ?? 35; final double _width = width ?? 35; - if (isNetworkImage) { return isSvg ? SvgPicture.network( diff --git a/lib/utils/tor.dart b/lib/utils/tor.dart new file mode 100644 index 000000000..233e998eb --- /dev/null +++ b/lib/utils/tor.dart @@ -0,0 +1,80 @@ +import 'dart:async'; + +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:flutter/material.dart'; + +bool didTorStart = false; +Future ensureTorStopped({required BuildContext? context}) async { + if (!didTorStart) { + printV("Tor hasn't been initialized yet, so it can't be stopped."); + return; + } + BuildContext? dialogContext; + if (context != null) dialogContext = await showFullscreenDialog(context); + didTorStart = false; + printV("Stopping tor"); + await CakeTor.instance.stop(); + printV("Tor stopped"); + if (context != null) dismissFullscreenDialog(dialogContext!); +} + +Future ensureTorStarted({required BuildContext? context}) async { + if (didTorStart) { + printV("Tor has already started"); + return; + } + BuildContext? dialogContext; + if (context != null) dialogContext = await showFullscreenDialog(context); + didTorStart = true; + printV("Starting tor"); + // var rootToken = RootIsolateToken.instance!; + // await Isolate.run(() async { + // BackgroundIsolateBinaryMessenger.ensureInitialized(rootToken); + // await CakeTor.instance.start(); + // }); + // second start is fast but populates the values on current thread + await CakeTor.instance.start(); + printV("Tor started"); + while (!CakeTor.instance.started) { + printV("Waiting for tor to start (part 1)"); + await Future.delayed(const Duration(seconds: 1)); + } + while (CakeTor.instance.port == -1) { + printV("Waiting for tor to start (listening on port)"); + await Future.delayed(const Duration(seconds: 1)); + } + printV("Tor started on port ${CakeTor.instance.port}"); + if (context != null) dismissFullscreenDialog(dialogContext!); +} + +Future showFullscreenDialog(BuildContext context) async { + BuildContext? dialogContext; + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + dialogContext = context; + return PopScope( + canPop: false, + child: Container( + color: Colors.transparent, + child: Center( + child: CircularProgressIndicator( + color: Colors.white, + ), + ), + ), + ); + }, + ), + ); + await Future.delayed(const Duration(seconds: 1)); + return dialogContext!; +} + +Future dismissFullscreenDialog(BuildContext context) async { + await Future.delayed(const Duration(seconds: 1)); + Navigator.of(context).pop(); +} \ No newline at end of file diff --git a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart index 442bd51b4..e777813c7 100644 --- a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; part 'cake_pay_cards_list_view_model.g.dart'; @@ -135,7 +136,11 @@ abstract class CakePayCardsListViewModelBase with Store { Future getCountries() async { - availableCountries = await cakePayService.getCountries(); + try { + availableCountries = await cakePayService.getCountries(); + } catch (e) { + printV(e); + } } @action @@ -144,17 +149,21 @@ abstract class CakePayCardsListViewModelBase with Store { int? currentPage, }) async { vendorsState = CakePayVendorLoadingState(); - searchString = text ?? ''; - var newVendors = await cakePayService.getVendors( - country: Country.getCakePayName(selectedCountry), - page: currentPage ?? page, - search: searchString, - giftCards: displayGiftCards, - prepaidCards: displayPrepaidCards, - custom: displayCustomValueCards, - onDemand: displayDenominationsCards); + try { + searchString = text ?? ''; + var newVendors = await cakePayService.getVendors( + country: Country.getCakePayName(selectedCountry), + page: currentPage ?? page, + search: searchString, + giftCards: displayGiftCards, + prepaidCards: displayPrepaidCards, + custom: displayCustomValueCards, + onDemand: displayDenominationsCards); - cakePayVendors = CakePayVendorList = newVendors; + cakePayVendors = CakePayVendorList = newVendors; + } catch (e) { + printV(e); + } vendorsState = CakePayVendorLoadedState(); } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 30bd1c8b3..db359a3f5 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -13,6 +13,11 @@ import 'package:cake_wallet/entities/service_status.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:cake_wallet/utils/tor.dart'; +import 'package:cake_wallet/wownero/wownero.dart' as wow; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -47,10 +52,9 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_daemon/flutter_daemon.dart'; -import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -1027,6 +1031,39 @@ abstract class DashboardViewModelBase with Store { @computed bool get syncAll => settingsStore.currentSyncAll; + @computed + bool get builtinTor => settingsStore.currentBuiltinTor; + + @action + void setBuiltinTor(bool value, BuildContext context) { + if (value) { + unawaited(showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).tor_connection, + alertContent: S.of(context).tor_experimental, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(true), + ); + }, + ), + ); + } + settingsStore.currentBuiltinTor = value; + if (value) { + unawaited(ensureTorStarted(context: context).then((_) async { + if (settingsStore.currentBuiltinTor == false) return; // return when tor got disabled in the meantime; + await wallet.connectToNode(node: appStore.settingsStore.getCurrentNode(wallet.type)); + })); + } else { + unawaited(ensureTorStopped(context: context).then((_) async { + if (settingsStore.currentBuiltinTor == true) return; // return when tor got enabled in the meantime; + await wallet.connectToNode(node: appStore.settingsStore.getCurrentNode(wallet.type)); + })); + } + } + @action void setSyncAll(bool value) => settingsStore.currentSyncAll = value; @@ -1073,7 +1110,17 @@ abstract class DashboardViewModelBase with Store { } } + static ServicesResponse? cachedServicesResponse; + Future getServicesStatus() async { + if (cachedServicesResponse != null) { + return cachedServicesResponse!; + } + cachedServicesResponse = await _getServicesStatus(); + return cachedServicesResponse!; + } + + Future _getServicesStatus() async { try { if (isEnabledBulletinAction) { final uri = Uri.https( @@ -1082,8 +1129,7 @@ abstract class DashboardViewModelBase with Store { {'key': secrets.fiatApiKey}, ); - final res = await http.get(uri); - + final res = await ProxyWrapper().get(clearnetUri: uri); if (res.statusCode < 200 || res.statusCode >= 300) { throw res.body; } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 4794746c4..f16d2aa95 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -19,7 +20,6 @@ import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; -import 'package:http/http.dart' as http; import 'package:cake_wallet/.secrets.g.dart' as secrets; part 'home_settings_view_model.g.dart'; @@ -240,14 +240,14 @@ abstract class HomeSettingsViewModelBase with Store { ); try { - final response = await http.get( - uri, + final response = await ProxyWrapper().get( + clearnetUri: uri, headers: { "Accept": "application/json", "X-API-Key": secrets.moralisApiKey, }, ); - + final decodedResponse = jsonDecode(response.body); final tokenInfo = Erc20TokenInfoMoralis.fromJson(decodedResponse[0] as Map); @@ -309,8 +309,8 @@ abstract class HomeSettingsViewModelBase with Store { ); try { - final response = await http.get(uri); - + final response = await ProxyWrapper().get(clearnetUri: uri); + final decodedResponse = jsonDecode(response.body) as Map; if (decodedResponse['status'] != '1') { @@ -351,7 +351,8 @@ abstract class HomeSettingsViewModelBase with Store { ); try { - final response = await http.get(uri); + final response = await ProxyWrapper().get(clearnetUri: uri); + final decodedResponse = jsonDecode(response.body) as Map; diff --git a/lib/view_model/dashboard/nft_view_model.dart b/lib/view_model/dashboard/nft_view_model.dart index 7da63d399..733cb4f47 100644 --- a/lib/view_model/dashboard/nft_view_model.dart +++ b/lib/view_model/dashboard/nft_view_model.dart @@ -7,7 +7,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:http/http.dart' as http; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -80,15 +80,16 @@ abstract class NFTViewModelBase with Store { isLoading = true; - final response = await http.get( - uri, + final response = await ProxyWrapper().get( + clearnetUri: uri, headers: { "Accept": "application/json", "X-API-Key": secrets.moralisApiKey, }, ); + - final decodedResponse = jsonDecode(response.body); + final decodedResponse = jsonDecode(response.body) as Map; if (walletType == WalletType.solana) { final results = await Future.wait( @@ -131,14 +132,14 @@ abstract class NFTViewModelBase with Store { '/nft/$chainName/$address/metadata', ); - final response = await http.get( - uri, + final response = await ProxyWrapper().get( + clearnetUri: uri, headers: { "Accept": "application/json", "X-API-Key": secrets.moralisApiKey, }, ); - + final decodedResponse = jsonDecode(response.body) as Map; return SolanaNFTAssetModel.fromJson(decodedResponse); @@ -171,15 +172,14 @@ abstract class NFTViewModelBase with Store { "normalizeMetadata": "true", }, ); - - final response = await http.get( - uri, + final response = await ProxyWrapper().get( + clearnetUri: uri, headers: { "Accept": "application/json", "X-API-Key": secrets.moralisApiKey, }, ); - + final decodedResponse = jsonDecode(response.body) as Map; final nftAsset = NFTAssetModel.fromJson(decodedResponse); diff --git a/lib/view_model/dev/network_requests_view_model.dart b/lib/view_model/dev/network_requests_view_model.dart new file mode 100644 index 000000000..401ddc6d2 --- /dev/null +++ b/lib/view_model/dev/network_requests_view_model.dart @@ -0,0 +1,16 @@ +import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart'; +import 'package:mobx/mobx.dart'; + +part 'network_requests_view_model.g.dart'; + +class NetworkRequestsViewModel = NetworkRequestsViewModelBase with _$NetworkRequestsViewModel; + +abstract class NetworkRequestsViewModelBase with Store { + @observable + List logs = MemoryProxyLogger.logs; + + @action + Future loadLogs() async { + logs = MemoryProxyLogger.logs; + } +} \ No newline at end of file diff --git a/lib/view_model/dev/send_network_requests_view_model.dart b/lib/view_model/dev/send_network_requests_view_model.dart new file mode 100644 index 000000000..1eb3bffc9 --- /dev/null +++ b/lib/view_model/dev/send_network_requests_view_model.dart @@ -0,0 +1,16 @@ +import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart'; +import 'package:mobx/mobx.dart'; + +part 'send_network_requests_view_model.g.dart'; + +class SendNetworkRequestsViewModel = SendNetworkRequestsViewModelBase with _$SendNetworkRequestsViewModel; + +abstract class SendNetworkRequestsViewModelBase with Store { + @observable + List logs = MemoryProxyLogger.logs; + + @action + Future loadLogs() async { + logs = MemoryProxyLogger.logs; + } +} \ No newline at end of file diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 58b5e5756..91cffe367 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/core/create_trade_result.dart'; import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/view_model/send/fees_view_model.dart'; import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -16,7 +17,6 @@ import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -939,8 +939,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } Future _isContractAddress(String chainName, String contractAddress) async { - final httpClient = http.Client(); - final uri = Uri.https( 'deep-index.moralis.io', '/api/v2.2/erc20/metadata', @@ -951,13 +949,14 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ); try { - final response = await httpClient.get( - uri, + final response = await ProxyWrapper().get( + clearnetUri: uri, headers: { "Accept": "application/json", "X-API-Key": secrets.moralisApiKey, }, ); + final decodedResponse = jsonDecode(response.body)[0] as Map; diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 7e4e73915..78544aa82 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:flutter/cupertino.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -58,6 +59,9 @@ abstract class NodeCreateOrEditViewModelBase with Store { @observable bool useSocksProxy; + @computed + bool get usesEmbeddedProxy => CakeTor.instance.started; + @observable String socksProxyAddress; diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 9df5f2980..9cd9ce959 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; @@ -38,7 +39,7 @@ abstract class NodeListViewModelBase with Store { String getAlertContent(String uri) => S.current.change_current_node(uri) + - '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + S.current.orbot_running_alert : ''}'; + '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance.enabled ? '' : S.current.orbot_running_alert) : ''}'; final ObservableList nodes; final SettingsStore settingsStore; diff --git a/lib/view_model/node_list/pow_node_list_view_model.dart b/lib/view_model/node_list/pow_node_list_view_model.dart index 5467be31a..5100676f4 100644 --- a/lib/view_model/node_list/pow_node_list_view_model.dart +++ b/lib/view_model/node_list/pow_node_list_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; @@ -38,7 +39,7 @@ abstract class PowNodeListViewModelBase with Store { String getAlertContent(String uri) => S.current.change_current_node(uri) + - '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + S.current.orbot_running_alert : ''}'; + '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance.enabled ? '' : S.current.orbot_running_alert) : ''}'; final ObservableList nodes; final SettingsStore settingsStore; diff --git a/lib/view_model/start_tor_view_model.dart b/lib/view_model/start_tor_view_model.dart new file mode 100644 index 000000000..1ef819d5f --- /dev/null +++ b/lib/view_model/start_tor_view_model.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/reactions/bootstrap.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/tor.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; + +part 'start_tor_view_model.g.dart'; + +class StartTorViewModel = StartTorViewModelBase with _$StartTorViewModel; + +abstract class StartTorViewModelBase with Store { + StartTorViewModelBase() { + _startTimer(); + } + + Timer? _timer; + final int waitTimeInSeconds = 15; + + @observable + bool isLoading = true; + + @observable + bool timeoutReached = false; + + @observable + int remainingSeconds = 15; + + @computed + bool get showOptions => timeoutReached; + + @action + void _startTimer() { + remainingSeconds = waitTimeInSeconds; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + remainingSeconds -= 1; + + if (remainingSeconds <= 0) { + timer.cancel(); + timeoutReached = true; + } + }); + } + + @observable + bool didStartTor = false; + + @action + Future startTor(BuildContext context) async { + if (didStartTor) { + return; + } + await ensureTorStarted(context: null); + while (true) { + await Future.delayed(Duration(milliseconds: 250)); + if (CakeTor.instance.port != -1 && CakeTor.instance.started) { + break; + } + } + didStartTor = true; + final appStore = getIt.get(); + bootstrapOnline(navigatorKey, loadWallet: true); + appStore.wallet?.connectToNode(node: appStore.settingsStore.getCurrentNode(appStore.wallet!.type)); + Navigator.pushReplacementNamed(context, Routes.login); + } + + @action + void disableTor(BuildContext context) { + final settingsStore = getIt.get(); + settingsStore.currentBuiltinTor = false; + bootstrapOnline(navigatorKey, loadWallet: true); + final appStore = getIt.get(); + appStore.wallet?.connectToNode(node: appStore.settingsStore.getCurrentNode(appStore.wallet!.type)); + Navigator.pushReplacementNamed(context, Routes.login); + } + + @action + void ignoreAndLaunchApp(BuildContext context) { + bootstrapOnline(navigatorKey, loadWallet: true); + final appStore = getIt.get(); + appStore.wallet?.connectToNode(node: appStore.settingsStore.getCurrentNode(appStore.wallet!.type)); + Navigator.pushReplacementNamed(context, Routes.login); + } + + void dispose() { + _timer?.cancel(); + _timer = null; + } +} \ No newline at end of file diff --git a/pubspec_base.yaml b/pubspec_base.yaml index e11703607..9f7f17ae4 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -100,13 +100,17 @@ dependencies: # git: # url: https://github.com/cake-tech/tor.git # ref: main - socks5_proxy: ^1.0.4 + socks5_proxy: + git: + url: https://github.com/LacticWhale/socks_dart + ref: 27ad7c2efae8d7460325c74b90f660085cbd0685 flutter_svg: ^2.0.9 polyseed: ^0.0.7 nostr_tools: git: url: https://github.com/MrCyjaneK/nostr_tools.git ref: 089d5a2dd751429a040ba10fb24fcbae564053e5 + solana: ^0.31.0+1 ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus @@ -166,6 +170,14 @@ dependency_overrides: git: url: https://github.com/vespr-wallet/ledger-flutter-plus ref: 60817d4b20144f9da9029f5034790272795b9d38 + socks5_proxy: + git: + url: https://github.com/LacticWhale/socks_dart.git + ref: 27ad7c2efae8d7460325c74b90f660085cbd0685 + tor_binary: + git: + url: https://github.com/MrCyjaneK/flutter-tor_binary + ref: cb811c610871a9517d47134b87c2f590c15c96c5 web_socket_channel: ^3.0.2 freezed_annotation: 2.4.4 diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index f12171f31..a5b287384 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "من خلال إيقاف تشغيل هذا ، قد تكون معدلات الرسوم غير دقيقة في بعض الحالات ، لذلك قد ينتهي بك الأمر إلى دفع مبالغ زائدة أو دفع رسوم المعاملات الخاصة بك", "disable_fiat": "تعطيل fiat", "disable_sell": "قم بتعطيل إجراء البيع", + "disable_tor": "تعطيل تور", "disable_trade_option": "تعطيل خيار التجارة", "disableBatteryOptimization": "تعطيل تحسين البطارية", "disableBatteryOptimizationDescription": "هل تريد تعطيل تحسين البطارية من أجل جعل الخلفية مزامنة تعمل بحرية وسلاسة؟", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "نقوم بإنشاء عناوين جديدة في كل مرة تستخدم فيها عنوانًا ، لكن العناوين السابقة تستمر في العمل", "email_address": "عنوان البريد الالكترونى", "enable": "يُمكَِن", + "enable_builtin_tor": "تمكين بنيت في تور", "enable_mempool_api": "MEMPOOL API للحصول على رسوم وتواريخ دقيقة", "enable_replace_by_fee": "تمكين الاستبدال", "enable_silent_payments_scanning": "ابدأ في مسح المدفوعات الصامتة ، حتى يتم الوصول إلى الطرف", @@ -334,6 +336,7 @@ "error_while_processing": "حدث خطأ أثناء التنقل", "errorGettingCredentials": "ﺩﺎﻤﺘﻋﻻﺍ ﺕﺎﻧﺎﻴﺑ ﻰﻠﻋ ﻝﻮﺼﺤﻟﺍ ءﺎﻨﺛﺃ ﺄﻄﺧ ﺙﺪﺣ :ﻞﺸﻓ", "errorSigningTransaction": "ﺔﻠﻣﺎﻌﻤﻟﺍ ﻊﻴﻗﻮﺗ ءﺎﻨﺛﺃ ﺄﻄﺧ ﺙﺪﺣ", + "establishing_tor_connection": "ارتياح توصيل تور", "estimated": "مُقدَّر", "estimated_new_fee": "رسوم جديدة مقدرة", "estimated_receive_amount": "مقدرة المبلغ الاستقبال", @@ -860,6 +863,7 @@ "sort_by": "ترتيب حسب", "spend_key_private": "مفتاح الإنفاق (خاص)", "spend_key_public": "مفتاح الإنفاق (عام)", + "starting_tor_proxy": "بدء الوكيل Tor", "status": "الحالة:", "step": "خطوة", "string_default": "تقصير", @@ -916,6 +920,8 @@ "tokenID": "ﻒﻳﺮﻌﺗ ﺔﻗﺎﻄﺑ", "ton_extra_info": "يرجى عدم نسيان تحديد معرف المذكرة أثناء إرسال معاملة TON للتبادل", "tor_connection": "ﺭﻮﺗ ﻝﺎﺼﺗﺍ", + "tor_connection_timeout": "Tor Torn Connection Timeout", + "tor_experimental": "هذه ميزة تجريبية ، وليس جميع الاتصالات المصنوعة من توجيه Cake Wallet Support - قد تستخدم بعض الاتصالات ClearNet ، من أجل توجيه حركة المرور بشكل كامل باستخدام TOR مع وضع VPN.", "tor_only": "Tor فقط", "total": "المجموع", "total_saving": "إجمالي المدخرات", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index e1d364b95..805fd9273 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Като изключите това, таксите могат да бъдат неточни в някои случаи, така че може да се препланите или да не плащате таксите за вашите транзакции", "disable_fiat": "Деактивиране на fiat", "disable_sell": "Деактивирайте действието за продажба", + "disable_tor": "Деактивирайте Tor", "disable_trade_option": "Деактивирайте опцията за търговия", "disableBatteryOptimization": "Деактивирайте оптимизацията на батерията", "disableBatteryOptimizationDescription": "Искате ли да деактивирате оптимизацията на батерията, за да направите синхронизирането на фона да работи по -свободно и гладко?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Нови адреси се генерират всеки път, когато използвате този, но и предишните продължават да работят", "email_address": "Имейл адрес", "enable": "Активиране", + "enable_builtin_tor": "Активирайте вграден Tor", "enable_mempool_api": "Mempool API за точни такси и дати", "enable_replace_by_fee": "Активиране на замяна по забрана", "enable_silent_payments_scanning": "Започнете да сканирате безшумните плащания, докато се достигне съветът", @@ -334,6 +336,7 @@ "error_while_processing": "Възникна грешка при приемане", "errorGettingCredentials": "Неуспешно: Грешка при получаване на идентификационни данни", "errorSigningTransaction": "Възникна грешка при подписване на транзакция", + "establishing_tor_connection": "Естибилиране на TOR връзка", "estimated": "Изчислено", "estimated_new_fee": "Прогнозна нова такса", "estimated_receive_amount": "Прогнозна сума за получаване", @@ -860,6 +863,7 @@ "sort_by": "Сортирай по", "spend_key_private": "Spend key (таен)", "spend_key_public": "Spend key (публичен)", + "starting_tor_proxy": "Стартиране на прокси", "status": "Статус: ", "step": "Стъпка", "string_default": "По подразбиране", @@ -916,6 +920,8 @@ "tokenID": "документ за самоличност", "ton_extra_info": "Моля, не забравяйте да посочите идентификационния номер на бележката, докато изпращате транзакцията TON за борсата", "tor_connection": "Tor връзка", + "tor_connection_timeout": "TOR ВРЕМЕ НА ВРЕМЕТО ВРЕМЕ", + "tor_experimental": "Това е експериментална функция, не всички връзки, направени от маршрутизиране на тортата за портфейл за торта - някои връзки могат да използват ClearNet, за да се насочи напълно да използва трафик на приложения TOR с VPN режим.", "tor_only": "Само чрез Tor", "total": "Обща сума", "total_saving": "Общо спестявания", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 0a6693316..658bcdf6e 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Tímto vypnutím by sazby poplatků mohly být v některých případech nepřesné, takže byste mohli skončit přepláváním nebo nedoplatkem poplatků za vaše transakce", "disable_fiat": "Zakázat fiat", "disable_sell": "Zakázat akci prodeje", + "disable_tor": "Zakázat tor", "disable_trade_option": "Zakázat možnost TRADE", "disableBatteryOptimization": "Zakázat optimalizaci baterie", "disableBatteryOptimizationDescription": "Chcete deaktivovat optimalizaci baterie, aby se synchronizovala pozadí volně a hladce?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Po každém použití je generována nová adresa, ale předchozí adresy také stále fungují", "email_address": "E-mailová adresa", "enable": "Umožnit", + "enable_builtin_tor": "Povolte vestavěné tor", "enable_mempool_api": "Mempool API pro přesné poplatky a data", "enable_replace_by_fee": "Povolit výměnu podle poplatku", "enable_silent_payments_scanning": "Začněte skenovat tiché platby, dokud není dosaženo špičky", @@ -334,6 +336,7 @@ "error_while_processing": "Při převádění došlo k chybě", "errorGettingCredentials": "Selhalo: Chyba při získávání přihlašovacích údajů", "errorSigningTransaction": "Při podepisování transakce došlo k chybě", + "establishing_tor_connection": "Estabilizující připojení Tor", "estimated": "Odhadováno", "estimated_new_fee": "Odhadovaný nový poplatek", "estimated_receive_amount": "Odhadovaná částka přijímání", @@ -860,6 +863,7 @@ "sort_by": "Seřazeno podle", "spend_key_private": "Klíč pro platby (soukromý)", "spend_key_public": "Klíč pro platby (veřejný)", + "starting_tor_proxy": "Zahájení proxy Tor", "status": "Status: ", "step": "Krok", "string_default": "Výchozí", @@ -916,6 +920,8 @@ "tokenID": "ID", "ton_extra_info": "Při odeslání TON transakce pro výměnu nezapomeňte zadat ID Memo ID", "tor_connection": "Připojení Tor", + "tor_connection_timeout": "Časový limit připojení", + "tor_experimental": "Jedná se o experimentální funkci, ne všechna připojení vyrobená z podpory dortů na dort Tor - některá připojení mohou používat ClearNet, aby se plně směrovaly používání provozu aplikací s režimem VPN.", "tor_only": "Pouze Tor", "total": "Celkový", "total_saving": "Celkem ušetřeno", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 5e9c2209c..b35f79cd3 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Wenn dies ausgeschaltet wird, sind die Gebührenquoten in einigen Fällen möglicherweise ungenau, sodass Sie die Gebühren für Ihre Transaktionen möglicherweise überbezahlt oder unterzahlt", "disable_fiat": "Fiat deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", + "disable_tor": "Tor deaktivieren", "disable_trade_option": "Handelsoption deaktivieren", "disableBatteryOptimization": "Batterieoptimierung deaktivieren", "disableBatteryOptimizationDescription": "Möchten Sie die Batterieoptimierung deaktivieren, um die Hintergrundsynchronisierung reibungsloser zu gestalten?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Wir generieren jedes Mal neue Adressen, wenn Sie eine verwenden, aber vorherige Adressen funktionieren weiterhin", "email_address": "E-Mail-Adresse", "enable": "Aktivieren", + "enable_builtin_tor": "Aktivieren Sie den bau-in tor", "enable_mempool_api": "Mempool-API für genaue Gebühren und Daten", "enable_replace_by_fee": "Aktivieren Sie Ersatz für Fee", "enable_silent_payments_scanning": "Scannen Sie nach Silent Payments ihrer Adresse", @@ -334,6 +336,7 @@ "error_while_processing": "Ein Fehler beim Proceessing trat ein Fehler auf", "errorGettingCredentials": "Fehlgeschlagen: Fehler beim Abrufen der Anmeldeinformationen", "errorSigningTransaction": "Beim Signieren der Transaktion ist ein Fehler aufgetreten", + "establishing_tor_connection": "Einstellung der TOR -Verbindung", "estimated": "Geschätzt", "estimated_new_fee": "Geschätzte neue Gebühr", "estimated_receive_amount": "Geschätzter Empfangsbetrag", @@ -861,6 +864,7 @@ "sort_by": "Sortiere nach", "spend_key_private": "Spend Key (geheim)", "spend_key_public": "Spend Key (öffentlich)", + "starting_tor_proxy": "TOR -Proxy beginnen", "status": "Status: ", "step": "Schritt", "string_default": "Standard", @@ -917,6 +921,8 @@ "tokenID": "AUSWEIS", "ton_extra_info": "Bitte vergessen Sie nicht, die Memo -ID anzugeben, während Sie die TON -Transaktion für den Austausch senden", "tor_connection": "Tor-Verbindung", + "tor_connection_timeout": "TOR -Verbindungs ​​-Zeitüberschreitung", + "tor_experimental": "Dies ist eine experimentelle Funktion, nicht alle Verbindungen, die aus Kuchen -Wallet -Stütze für die Routing hergestellt werden. Einige Verbindungen können ClearNET verwenden, um den App -Traffic -App -TOR mit dem VPN -Modus vollständig zu verwenden.", "tor_only": "Nur Tor", "total": "Gesamt", "total_saving": "Gesamteinsparungen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index ad4894238..b6e655674 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "By turning this off, the fee rates might be inaccurate in some cases, so you might end up overpaying or underpaying the fees for your transactions", "disable_fiat": "Disable fiat", "disable_sell": "Disable sell action", + "disable_tor": "Disable Tor", "disable_trade_option": "Disable trade option", "disableBatteryOptimization": "Disable Battery Optimization", "disableBatteryOptimizationDescription": "Do you want to disable battery optimization in order to make background sync run more freely and smoothly?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work", "email_address": "Email Address", "enable": "Enable", + "enable_builtin_tor": "Enable built-in Tor", "enable_mempool_api": "Mempool API for accurate fees and dates", "enable_replace_by_fee": "Enable Replace-By-Fee", "enable_silent_payments_scanning": "Start scanning for transactions sent to your Silent Payment address.", @@ -334,6 +336,7 @@ "error_while_processing": "An error occurred while proceessing", "errorGettingCredentials": "Failed: Error while getting credentials", "errorSigningTransaction": "An error has occured while signing transaction", + "establishing_tor_connection": "Estabilishing Tor connection", "estimated": "Estimated", "estimated_new_fee": "Estimated new fee", "estimated_receive_amount": "Estimated receive amount", @@ -861,6 +864,7 @@ "sort_by": "Sort by", "spend_key_private": "Spend key (private)", "spend_key_public": "Spend key (public)", + "starting_tor_proxy": "Starting Tor proxy", "status": "Status: ", "step": "Step", "string_default": "Default", @@ -917,6 +921,8 @@ "tokenID": "ID", "ton_extra_info": "Please don’t forget to specify the Memo ID while sending the TON transaction for the exchange", "tor_connection": "Tor connection", + "tor_connection_timeout": "Tor connection timeout", + "tor_experimental": "This is an experimental feature, not all connections made from Cake Wallet support Tor routing - some connections may use clearnet, in order to fully route app traffic use Tor with VPN mode.", "tor_only": "Tor only", "total": "Total", "total_saving": "Total Savings", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 3f8657ec4..6a75f6e62 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Al apagar esto, las tasas de tarifas pueden ser inexactas en algunos casos, por lo que puede terminar pagando en exceso o pagando menos las tarifas por sus transacciones", "disable_fiat": "Deshabilitar fiat", "disable_sell": "Desactivar acción de venta", + "disable_tor": "Deshabilitar tor", "disable_trade_option": "Deshabilitar la opción de comercio", "disableBatteryOptimization": "Deshabilitar la optimización de la batería", "disableBatteryOptimizationDescription": "¿Desea deshabilitar la optimización de la batería para que la sincronización de fondo se ejecute más libremente y sin problemas?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Generamos nuevas direcciones cada vez que usa una, pero las direcciones anteriores siguen funcionando", "email_address": "Dirección de correo electrónico", "enable": "Permitir", + "enable_builtin_tor": "Habilitar buildi-in tor", "enable_mempool_api": "API de Mempool para tarifas y fechas precisas", "enable_replace_by_fee": "Habilitar reemplazar por tarea", "enable_silent_payments_scanning": "Comienza a escanear pagos silenciosos, hasta que se alcance la altura actual", @@ -334,6 +336,7 @@ "error_while_processing": "Se produjo un error mientras procesaba", "errorGettingCredentials": "Error: error al obtener las credenciales", "errorSigningTransaction": "Se ha producido un error al firmar la transacción.", + "establishing_tor_connection": "Estabilización de la conexión para", "estimated": "Estimado", "estimated_new_fee": "Nueva tarifa estimada", "estimated_receive_amount": "Cantidad de recepción estimada", @@ -861,6 +864,7 @@ "sort_by": "Ordenar por", "spend_key_private": "Llave de gasto (privada)", "spend_key_public": "Llave de gasto (pública)", + "starting_tor_proxy": "Inicio Tor Proxy", "status": "Estado: ", "step": "Paso", "string_default": "Por defecto", @@ -917,6 +921,8 @@ "tokenID": "IDENTIFICACIÓN", "ton_extra_info": "No olvide especificar el ID de memo mientras envía la transacción TON para el intercambio", "tor_connection": "conexión tor", + "tor_connection_timeout": "Tiempo de espera de conexión de Tor", + "tor_experimental": "Esta es una característica experimental, no todas las conexiones hechas con el enrutamiento de soporte para la billetera de pastel: algunas conexiones pueden usar ClearNet, para enrutar completamente el tráfico de aplicaciones Uso Tor con el modo VPN.", "tor_only": "solo Tor", "total": "Total", "total_saving": "Ahorro Total", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index ba3ae4859..d2f8b4e6b 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "En désactivant cela, les taux de frais peuvent être inexacts dans certains cas, vous pourriez donc finir par payer trop ou sous-paiement les frais pour vos transactions", "disable_fiat": "Désactiver les montants en fiat", "disable_sell": "Désactiver l'action de vente", + "disable_tor": "Désactiver Tor", "disable_trade_option": "Désactiver l'option de commerce", "disableBatteryOptimization": "Désactiver l'optimisation de la batterie", "disableBatteryOptimizationDescription": "Voulez-vous désactiver l'optimisation de la batterie afin de faire fonctionner la synchronisation d'arrière-plan plus librement et en douceur?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Nous générons de nouvelles adresses à chaque fois que vous en utilisez une, mais les adresses précédentes continuent à fonctionner", "email_address": "Adresse e-mail", "enable": "Activer", + "enable_builtin_tor": "Activer le Tor construit", "enable_mempool_api": "API Mempool pour les frais et dates précis", "enable_replace_by_fee": "Activer Remplace-by-Fee", "enable_silent_payments_scanning": "Commencez à analyser les transactions envoyées à votre adresse de paiement silencieux.", @@ -334,6 +336,7 @@ "error_while_processing": "Une erreur s'est produite lors de la procédure", "errorGettingCredentials": "Échec : erreur lors de l'obtention des informations d'identification", "errorSigningTransaction": "Une erreur s'est produite lors de la signature de la transaction", + "establishing_tor_connection": "Établir la connexion TOR", "estimated": "Estimé", "estimated_new_fee": "De nouveaux frais estimés", "estimated_receive_amount": "Recevoir estimé le montant", @@ -860,6 +863,7 @@ "sort_by": "Trier par", "spend_key_private": "Clef de dépense (spend key) (privée)", "spend_key_public": "Clef de dépense (spend key) (publique)", + "starting_tor_proxy": "Démarrer le proxy Tor", "status": "Statut: ", "step": "Étape", "string_default": "Défaut", @@ -916,6 +920,8 @@ "tokenID": "IDENTIFIANT", "ton_extra_info": "N'oubliez pas de spécifier l'identification de la note lors de l'envoi de la transaction TON pour l'échange", "tor_connection": "Connexion Tor", + "tor_connection_timeout": "Tor Connection Timeout", + "tor_experimental": "Il s'agit d'une caractéristique expérimentale, toutes les connexions faites à partir du portefeuille du portefeuille de gâteaux TOR ROUTING - Certaines connexions peuvent utiliser ClearNet, afin d'achever entièrement le trafic d'application Utiliser TOR avec le mode VPN.", "tor_only": "Tor uniquement", "total": "Total", "total_saving": "Économies totales", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index ebd236332..6b73db8ed 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Ta hanyar juya wannan kashe, kudaden da zai iya zama ba daidai ba a wasu halaye, saboda haka zaku iya ƙare da overpaying ko a ƙarƙashin kudaden don ma'amaloli", "disable_fiat": "Dakatar da fiat", "disable_sell": "Kashe karbuwa", + "disable_tor": "Musaki tor", "disable_trade_option": "Musaki zaɓi na kasuwanci", "disableBatteryOptimization": "Kashe ingantawa baturi", "disableBatteryOptimizationDescription": "Shin kana son kashe ingantawa baturi don yin setnc bankwali gudu da yar kyauta da kyau?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Muna samar da sababbin adireshi duk lokacin da kuka yi amfani da ɗaya, amma adiresoshin da suka gabata suna ci gaba da aiki", "email_address": "Adireshin i-mel", "enable": "Ba dama", + "enable_builtin_tor": "Sanya riguna-inf", "enable_mempool_api": "Mampool API don ingantattun kudade da kwanakin", "enable_replace_by_fee": "Ba da damar maye gurbin-by-kudin", "enable_silent_payments_scanning": "Fara bincika biya na shiru, har sai tip ɗin ya kai", @@ -334,6 +336,7 @@ "error_while_processing": "Kuskure ya faru yayin bincike", "errorGettingCredentials": "Ba a yi nasara ba: Kuskure yayin samun takaddun shaida", "errorSigningTransaction": "An sami kuskure yayin sanya hannu kan ciniki", + "establishing_tor_connection": "Kafa Tor Haɗaɗɗa", "estimated": "Kiyasta", "estimated_new_fee": "An kiyasta sabon kudin", "estimated_receive_amount": "Kiyasta samun adadin", @@ -862,6 +865,7 @@ "sort_by": "Kasa", "spend_key_private": "makullin biya (maɓallin kalmar sirri)", "spend_key_public": "makullin biya (maɓallin jama'a)", + "starting_tor_proxy": "Fara tor proxy", "status": "Matsayi:", "step": "Taka", "string_default": "Ƙin cika alƙawari", @@ -918,6 +922,8 @@ "tokenID": "ID", "ton_extra_info": "Don Allah kar a manta su saka ID na Memo yayin aikawa da ma'amala don musayar", "tor_connection": "Tor haɗin gwiwa", + "tor_connection_timeout": "A lokacin Tor Haɗin", + "tor_experimental": "Wannan fasalin gwaji ne, ba duk haɗin da aka yi ba daga Wall Wall Wall Walling goyon baya Tor Routing - don cikakken amfani da hanyar zirga-zirga amfani da tor tare da yanayin vpn.", "tor_only": "Tor kawai", "total": "Duka", "total_saving": "Jimlar Adana", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 2e71b6685..f97b0f84f 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "इसे बंद करने से, कुछ मामलों में शुल्क दरें गलत हो सकती हैं, इसलिए आप अपने लेनदेन के लिए फीस को कम कर सकते हैं या कम कर सकते हैं", "disable_fiat": "िएट को अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", + "disable_tor": "टोर को अक्षम करें", "disable_trade_option": "व्यापार विकल्प अक्षम करें", "disableBatteryOptimization": "बैटरी अनुकूलन अक्षम करें", "disableBatteryOptimizationDescription": "क्या आप बैकग्राउंड सिंक को अधिक स्वतंत्र और सुचारू रूप से चलाने के लिए बैटरी ऑप्टिमाइज़ेशन को अक्षम करना चाहते हैं?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "हर बार जब आप एक का उपयोग करते हैं तो हम नए पते उत्पन्न करते हैं, लेकिन पिछले पते काम करना जारी रखते हैं", "email_address": "ईमेल पता", "enable": "सक्षम", + "enable_builtin_tor": "बिल्ट-इन टोर को सक्षम करें", "enable_mempool_api": "सटीक शुल्क और तिथियों के लिए मेमपूल एपीआई", "enable_replace_by_fee": "प्रतिस्थापित-दर-शुल्क सक्षम करें", "enable_silent_payments_scanning": "मूक भुगतान स्कैनिंग सक्षम करें", @@ -334,6 +336,7 @@ "error_while_processing": "प्रोकसिंग करते समय एक त्रुटि हुई", "errorGettingCredentials": "विफल: क्रेडेंशियल प्राप्त करते समय त्रुटि", "errorSigningTransaction": "लेन-देन पर हस्ताक्षर करते समय एक त्रुटि उत्पन्न हुई है", + "establishing_tor_connection": "टोर -कनेक्शन", "estimated": "अनुमानित", "estimated_new_fee": "अनुमानित नया शुल्क", "estimated_receive_amount": "अनुमानित राशि", @@ -862,6 +865,7 @@ "sort_by": "इसके अनुसार क्रमबद्ध करें", "spend_key_private": "खर्च करना (निजी)", "spend_key_public": "खर्च करना (जनता)", + "starting_tor_proxy": "टोर प्रॉक्सी शुरू करना", "status": "स्थिति: ", "step": "कदम", "string_default": "गलती करना", @@ -918,6 +922,8 @@ "tokenID": "पहचान", "ton_extra_info": "कृपया एक्सचेंज के लिए टन लेनदेन भेजते समय मेमो आईडी निर्दिष्ट करना न भूलें", "tor_connection": "टोर कनेक्शन", + "tor_connection_timeout": "टोर कनेक्शन टाइमआउट", + "tor_experimental": "यह एक प्रायोगिक विशेषता है, केक वॉलेट सपोर्ट टोर रूटिंग से किए गए सभी कनेक्शन नहीं - कुछ कनेक्शन क्लीयरनेट का उपयोग कर सकते हैं, ताकि वीपीएन मोड के साथ ऐप ट्रैफिक का उपयोग पूरी तरह से रूट करने के लिए हो।", "tor_only": "Tor केवल", "total": "कुल", "total_saving": "कुल बचत", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index b0aa12937..ef9e04bbe 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Isključivanjem ovoga, stope naknade u nekim bi slučajevima mogle biti netočne, tako da biste mogli preplatiti ili predati naknadu za vaše transakcije", "disable_fiat": "Isključi, fiat", "disable_sell": "Onemogući akciju prodaje", + "disable_tor": "Onemogućiti tor", "disable_trade_option": "Onemogući trgovinsku opciju", "disableBatteryOptimization": "Onemogući optimizaciju baterije", "disableBatteryOptimizationDescription": "Želite li onemogućiti optimizaciju baterije kako bi se pozadinska sinkronizacija radila slobodnije i glatko?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Minden egyes alkalommal új címeket generálunk, de a korábbi címek továbbra is működnek", "email_address": "Adresa e-pošte", "enable": "Omogućiti", + "enable_builtin_tor": "Omogućite ugrađeni u tor", "enable_mempool_api": "Mempool API za točne naknade i datume", "enable_replace_by_fee": "Omogući zamjenu", "enable_silent_payments_scanning": "Započnite skeniranje tihih plaćanja, dok se ne postigne savjet", @@ -334,6 +336,7 @@ "error_while_processing": "Došlo je do pogreške tijekom prožimanja", "errorGettingCredentials": "Neuspješno: Pogreška prilikom dobivanja vjerodajnica", "errorSigningTransaction": "Došlo je do pogreške prilikom potpisivanja transakcije", + "establishing_tor_connection": "Uspostavljanje torbice", "estimated": "procijenjen", "estimated_new_fee": "Procijenjena nova naknada", "estimated_receive_amount": "Procijenjeni iznos primanja", @@ -860,6 +863,7 @@ "sort_by": "Poredaj po", "spend_key_private": "Spend key (privatni)", "spend_key_public": "Spend key (javni)", + "starting_tor_proxy": "Početak Tor proxy", "status": "Status: ", "step": "Korak", "string_default": "Zadano", @@ -916,6 +920,8 @@ "tokenID": "iskaznica", "ton_extra_info": "Ne zaboravite navesti ID memorandu", "tor_connection": "Tor veza", + "tor_connection_timeout": "TOR TIMPOUCEN", + "tor_experimental": "Ovo je eksperimentalna značajka, nisu sve veze napravljene od podrške za kolače za torte - neke veze mogu koristiti ClearNet, kako bi se u potpunosti usmjerio na APP promet s TOR -om s VPN načinom rada.", "tor_only": "Samo Tor", "total": "Ukupno", "total_saving": "Ukupna ušteda", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index cafabe5a6..ac1bfda99 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Դրանից անջատելով, վճարների տեմպերը որոշ դեպքերում կարող են անճիշտ լինել, այնպես որ դուք կարող եք վերջ տալ ձեր գործարքների համար վճարների գերավճարների կամ գերավճարների վրա", "disable_fiat": "Անջատել ֆիատ", "disable_sell": "Անջատել վաճառք գործողությունը", + "disable_tor": "Անջատեք Tor- ը", "disable_trade_option": "Անջատեք առեւտրի տարբերակը", "disableBatteryOptimization": "Անջատել մարտկոցի օպտիմիզացիան", "disableBatteryOptimizationDescription": "Դուք ցանկանում եք անջատել մարտկոցի օպտիմիզացիան ֆոնային համաժամացման ավելի ազատ և հարթ ընթացքի համար?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Մենք ստեղծում ենք նոր հասցե ամեն անգամ, երբ դուք օգտագործում եք այն, բայց նախորդ հասցեները շարունակում են աշխատել", "email_address": "Էլ. փոստի հասցե", "enable": "Միացնել", + "enable_builtin_tor": "Միացրեք ներկառուցված ջահը", "enable_mempool_api": "Mempool API ճշգրիտ վճարների եւ ամսաթվերի համար", "enable_replace_by_fee": "Միացնել փոխարինումը միջնորդավճարով", "enable_silent_payments_scanning": "Միացնել Լուռ Վճարումների սկանավորումը", @@ -334,6 +336,7 @@ "error_while_processing": "Նախագծման ժամանակ սխալ է տեղի ունեցել", "errorGettingCredentials": "Սխալ. ծանրաբեռնված վստահագրեր ստանալիս", "errorSigningTransaction": "Սխալ է տեղի ունեցել գործարքը ստորագրելիս", + "establishing_tor_connection": "Չափայնացնում է Tor կապը", "estimated": "Գնահատված", "estimated_new_fee": "Գնահատված նոր միջնորդավճար", "estimated_receive_amount": "Գնահատված ստացված գումար", @@ -858,6 +861,7 @@ "sort_by": "Դասավորել ըստ", "spend_key_private": "Վճարման բանալի (գախտնի)", "spend_key_public": "Վճարման բանալի (հանրային)", + "starting_tor_proxy": "Սկսելով Tor վստահված անձը", "status": "Կարգավիճակ՝ ", "step": "Քայլ", "string_default": "Լռելայն", @@ -914,6 +918,8 @@ "tokenID": "ID", "ton_extra_info": "Խնդրում ենք մի մոռացեք նշել MEMO ID- ն, երբ փոխանակման համար տոննա գործարքը ուղարկեք", "tor_connection": "Tor կապ", + "tor_connection_timeout": "Tor կապի ժամկետ", + "tor_experimental": "Սա փորձարարական առանձնահատկություն է, ոչ բոլոր կապերը տորթ դրամապանակների աջակցության համար", "tor_only": "Միայն Tor", "total": "Ընդհանուր", "total_saving": "Ընդհանուր խնայողություն", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index e726ffb3c..e693c98e5 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Dengan mematikan ini, tarif biaya mungkin tidak akurat dalam beberapa kasus, jadi Anda mungkin akan membayar lebih atau membayar biaya untuk transaksi Anda", "disable_fiat": "Nonaktifkan fiat", "disable_sell": "Nonaktifkan aksi jual", + "disable_tor": "Nonaktifkan tor", "disable_trade_option": "Nonaktifkan opsi perdagangan", "disableBatteryOptimization": "Nonaktifkan optimasi baterai", "disableBatteryOptimizationDescription": "Apakah Anda ingin menonaktifkan optimasi baterai untuk membuat sinkronisasi latar belakang berjalan lebih bebas dan lancar?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Kami menghasilkan alamat baru setiap kali Anda menggunakan satu, tetapi alamat sebelumnya tetap berfungsi", "email_address": "Alamat Email", "enable": "Memungkinkan", + "enable_builtin_tor": "Aktifkan built-in tor", "enable_mempool_api": "API Mempool untuk biaya dan tanggal yang akurat", "enable_replace_by_fee": "Aktifkan ganti-by-fee", "enable_silent_payments_scanning": "Mulailah memindai pembayaran diam, sampai ujung tercapai", @@ -334,6 +336,7 @@ "error_while_processing": "Terjadi kesalahan saat prosedur", "errorGettingCredentials": "Gagal: Terjadi kesalahan saat mendapatkan kredensial", "errorSigningTransaction": "Terjadi kesalahan saat menandatangani transaksi", + "establishing_tor_connection": "Estabilishing Tor Connection", "estimated": "Diperkirakan", "estimated_new_fee": "Perkiraan biaya baru", "estimated_receive_amount": "Diperkirakan jumlah menerima", @@ -863,6 +866,7 @@ "sort_by": "Sortir dengan", "spend_key_private": "Kunci pengeluaran (privat)", "spend_key_public": "Kunci pengeluaran (publik)", + "starting_tor_proxy": "Mulai untuk proxy", "status": "Status: ", "step": "Melangkah", "string_default": "Bawaan", @@ -919,6 +923,8 @@ "tokenID": "PENGENAL", "ton_extra_info": "Harap jangan lupa untuk menentukan ID memo saat mengirim transaksi ton untuk pertukaran", "tor_connection": "koneksi Tor", + "tor_connection_timeout": "Tor Connection Timeout", + "tor_experimental": "Ini adalah fitur eksperimental, tidak semua koneksi yang terbuat dari dukungan dompet kue untuk routing - beberapa koneksi dapat menggunakan clearnet, untuk sepenuhnya merutekan aplikasi lalu lintas menggunakan untuk mode VPN.", "tor_only": "Hanya Tor", "total": "Total", "total_saving": "Total Pembayaran", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 4e94c349f..b92969550 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Disattivando quest'opzione, i tassi delle commissioni potrebbero essere inaccurati in alcuni casi, quindi potresti finire per pagare troppo o troppo poco le commissioni per le tue transazioni", "disable_fiat": "Disabilita fiat", "disable_sell": "Disabilita l'azione di vendita", + "disable_tor": "Disabilita Tor", "disable_trade_option": "Disabilita l'opzione di scambio", "disableBatteryOptimization": "Disabilita l'ottimizzazione della batteria", "disableBatteryOptimizationDescription": "Vuoi disabilitare l'ottimizzazione della batteria per migliorare la sincronizzazione in background?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Generiamo nuovi indirizzi ogni volta che ne utilizzi uno, ma gli indirizzi precedenti continuano a funzionare", "email_address": "Indirizzo e-mail", "enable": "Abilita", + "enable_builtin_tor": "Abilita buildi-in tor", "enable_mempool_api": "API Mempool per commissioni e date accurate", "enable_replace_by_fee": "Abilita Replace-By-Fee", "enable_silent_payments_scanning": "Inizia a scansionare le transazioni inviate al tuo indirizzo Silent Payments", @@ -334,6 +336,7 @@ "error_while_processing": "Si è verificato un errore durante la promozione", "errorGettingCredentials": "Non riuscito: errore durante il recupero delle credenziali", "errorSigningTransaction": "Si è verificato un errore durante la firma della transazione", + "establishing_tor_connection": "Connessione TOR di esame", "estimated": "Stimato", "estimated_new_fee": "Nuova commissione stimata", "estimated_receive_amount": "Importo di ricezione stimato", @@ -861,6 +864,7 @@ "sort_by": "Ordina per", "spend_key_private": "Chiave di spesa (privata)", "spend_key_public": "Chiave di spesa (pubblica)", + "starting_tor_proxy": "Avvio del proxy di Tor", "status": "Stato: ", "step": "Fare un passo", "string_default": "Predefinito", @@ -917,6 +921,8 @@ "tokenID": "ID", "ton_extra_info": "Non dimenticare di specificare l'ID memo durante l'invio della transazione Ton per lo scambio", "tor_connection": "Connessione Tor", + "tor_connection_timeout": "Timeout di connessione TOR", + "tor_experimental": "Questa è una funzionalità sperimentale, non tutte le connessioni realizzate con il supporto per il portafoglio di torta per routing - alcune connessioni possono utilizzare ClearNet, al fine di instradare completamente il traffico di app per l'uso del traffico con la modalità VPN.", "tor_only": "Solo Tor", "total": "Totale", "total_saving": "Risparmio totale", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 960634e27..9a0b9fc23 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "これをオフにすることで、料金金利は場合によっては不正確になる可能性があるため、取引の費用が過払いまたは不足している可能性があります", "disable_fiat": "フィアットを無効にする", "disable_sell": "販売アクションを無効にする", + "disable_tor": "torを無効にします", "disable_trade_option": "取引オプションを無効にします", "disableBatteryOptimization": "バッテリーの最適化を無効にします", "disableBatteryOptimizationDescription": "バックグラウンドシンクをより自由かつスムーズに実行するために、バッテリーの最適化を無効にしたいですか?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "使用するたびに新しいアドレスが生成されますが、以前のアドレスは引き続き機能します", "email_address": "メールアドレス", "enable": "有効にする", + "enable_builtin_tor": "組み込みのTORを有効にします", "enable_mempool_api": "正確な料金と日付のMempool API", "enable_replace_by_fee": "交換ごとに有効にします", "enable_silent_payments_scanning": "先端に達するまで、サイレント決済のスキャンを開始します", @@ -334,6 +336,7 @@ "error_while_processing": "処理中にエラーが発生しました", "errorGettingCredentials": "失敗: 認証情報の取得中にエラーが発生しました", "errorSigningTransaction": "トランザクションの署名中にエラーが発生しました", + "establishing_tor_connection": "TOR接続を確立します", "estimated": "推定", "estimated_new_fee": "推定新しい料金", "estimated_receive_amount": "推定受信金額", @@ -861,6 +864,7 @@ "sort_by": "並び替え", "spend_key_private": "キーを使う (プライベート)", "spend_key_public": "キーを使う (パブリック)", + "starting_tor_proxy": "プロキシを開始します", "status": "状態: ", "step": "ステップ", "string_default": "デフォルト", @@ -917,6 +921,8 @@ "tokenID": "ID", "ton_extra_info": "Exchangeのトントランザクションを送信しながら、メモIDを指定することを忘れないでください", "tor_connection": "Tor接続", + "tor_connection_timeout": "TOR接続タイムアウト", + "tor_experimental": "これは実験的な機能であり、ケーキウォレットサポートTORルーティングから作成されたすべての接続ではありません。一部の接続では、APPトラフィックをVPNモードで完全にルーティングするために、ClearNetを使用する場合があります。", "tor_only": "Torのみ", "total": "合計", "total_saving": "合計節約額", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index e107e1a29..b70d30328 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -268,7 +268,8 @@ "disable_exchange_option": "교환 옵션 비활성화", "disable_fee_api_warning": "이 기능을 끄면 경우에 따라 수수료율이 정확하지 않을 수 있으며, 트랜잭션 수수료를 과다 또는 과소 지불할 수 있습니다.", "disable_fiat": "법정화폐 비활성화", - "disable_sell": "판매 기능 비활성화", + "disable_sell": "판매 조치 비활성화", + "disable_tor": "Tor를 비활성화하십시오", "disable_trade_option": "거래 옵션 비활성화", "disableBatteryOptimization": "배터리 최적화 비활성화", "disableBatteryOptimizationDescription": "백그라운드 동기화가 더 자유롭고 원활하게 실행되도록 배터리 최적화를 비활성화하시겠습니까?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "사용할 때마다 새 주소를 생성하지만 이전 주소도 계속 작동합니다", "email_address": "이메일 주소", "enable": "활성화", + "enable_builtin_tor": "내장 Tor를 활성화하십시오", "enable_mempool_api": "정확한 수수료 및 날짜를 위한 Mempool API", "enable_replace_by_fee": "RBF(Replace-By-Fee) 활성화", "enable_silent_payments_scanning": "사일런트 페이먼트 주소로 전송된 트랜잭션 스캔 시작", @@ -334,6 +336,7 @@ "error_while_processing": "처리 중 오류가 발생했습니다", "errorGettingCredentials": "실패: 자격 증명을 가져오는 중 오류 발생", "errorSigningTransaction": "트랜잭션 서명 중 오류가 발생했습니다", + "establishing_tor_connection": "Tor 연결을 실행합니다", "estimated": "예상", "estimated_new_fee": "예상 새 수수료", "estimated_receive_amount": "예상 수령 금액", @@ -861,6 +864,7 @@ "sort_by": "정렬 기준", "spend_key_private": "지출 키 (개인)", "spend_key_public": "지출 키 (공개)", + "starting_tor_proxy": "Tor 프록시 시작", "status": "상태: ", "step": "단계", "string_default": "기본값", @@ -917,6 +921,8 @@ "tokenID": "ID", "ton_extra_info": "교환을 위해 TON 트랜잭션을 보내는 동안 메모 ID를 지정하는 것을 잊지 마십시오.", "tor_connection": "Tor 연결", + "tor_connection_timeout": "Tor 연결 시간 초과", + "tor_experimental": "이것은 케이크 지갑 지원 Tor 라우팅으로 만든 모든 연결을 실험적인 기능입니다. 일부 연결은 Clearnet을 사용할 수 있습니다.", "tor_only": "Tor 전용", "total": "합계", "total_saving": "총 절약액", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index d9f3c46da..f17bb7527 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "ဤအရာကိုဖွင့်ခြင်းအားဖြင့်အချို့သောကိစ္စရပ်များတွင်အခကြေးငွေနှုန်းထားများသည်တိကျမှုရှိနိုင်သည်,", "disable_fiat": "Fiat ကိုပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", + "disable_tor": "Tor ကိုပိတ်ပါ", "disable_trade_option": "ကုန်သွယ်ရေး option ကိုပိတ်ပါ", "disableBatteryOptimization": "ဘက်ထရီ optimization ကိုပိတ်ပါ", "disableBatteryOptimizationDescription": "နောက်ခံထပ်တူပြုခြင်းနှင့်ချောချောမွေ့မွေ့ပြုလုပ်နိုင်ရန်ဘက်ထရီ optimization ကိုသင်ပိတ်ထားလိုပါသလား။", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "သင်အသုံးပြုသည့်အချိန်တိုင်းတွင် ကျွန်ုပ်တို့သည် လိပ်စာအသစ်များကို ထုတ်ပေးသော်လည်း ယခင်လိပ်စာများသည် ဆက်လက်အလုပ်လုပ်နေပါသည်။", "email_address": "အီးမေးလ်လိပ်စာ", "enable": "စွမ်းဆောင်နိုင်စေ", + "enable_builtin_tor": "built-in Tor ကို Enable လုပ်ပါ", "enable_mempool_api": "Mempool API တိကျသောအခကြေးငွေနှင့်ရက်စွဲများအတွက်", "enable_replace_by_fee": "အစားထိုး - by- အခကြေးငွေ enable", "enable_silent_payments_scanning": "အစွန်အဖျားသို့ရောက်ရှိသည်အထိအသံတိတ်ငွေပေးချေမှုကိုစကင်ဖတ်စစ်ဆေးပါ", @@ -334,6 +336,7 @@ "error_while_processing": "procesing နေစဉ်အမှားတစ်ခုဖြစ်ပွားခဲ့သည်", "errorGettingCredentials": "မအောင်မြင်ပါ- အထောက်အထားများ ရယူနေစဉ် အမှားအယွင်း", "errorSigningTransaction": "ငွေပေးငွေယူ လက်မှတ်ထိုးစဉ် အမှားအယွင်းတစ်ခု ဖြစ်ပေါ်ခဲ့သည်။", + "establishing_tor_connection": "Tor connection ကို estabilishing", "estimated": "ခန့်မှန်း", "estimated_new_fee": "ခန့်မှန်းသစ်ခန့်မှန်း", "estimated_receive_amount": "ခန့်မှန်းရရှိသောပမာဏ", @@ -860,6 +863,7 @@ "sort_by": "အလိုက်စဥ်သည်", "spend_key_private": "သော့သုံးရန် (သီးသန့်)", "spend_key_public": "သုံးစွဲရန်သော့ (အများပြည်သူ)", + "starting_tor_proxy": "Tor proxy ကိုစတင်ခြင်း", "status": "အခြေအနေ:", "step": "လှမ်း", "string_default": "ပျက်ကွက်ခြင်း", @@ -916,6 +920,8 @@ "tokenID": "အမှတ်သညာ", "ton_extra_info": "ငွေလဲလှယ်မှုအတွက်တန်ပြန်ငွေပေးငွေယူကိုပို့နေစဉ် Memo ID ကိုသတ်မှတ်ရန်မမေ့ပါနှင့်", "tor_connection": "Tor ချိတ်ဆက်မှု", + "tor_connection_timeout": "Tor Connection အချိန်ကုန်", + "tor_experimental": "၎င်းသည်စမ်းသပ်အင်္ဂါရပ်တစ်ခုဖြစ်ပြီးကိတ်မုန့်ပိုက်ဆံအိတ်ကိုပံ့ပိုးပေးသော Tor Routing မှပြုလုပ်သောဆက်သွယ်မှုအားလုံးမှာ Connections သည် Concernet ကို အသုံးပြု. VPN tor ကို VPN Mode ဖြင့် Tor ကို အသုံးပြု. Concernet ကိုသုံးနိုင်သည်။", "tor_only": "Tor သာ", "total": "လုံးဝသော", "total_saving": "စုစုပေါင်းစုဆောင်းငွေ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 756f0b311..ebde13a68 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Door dit uit te schakelen, kunnen de tarieven in sommige gevallen onnauwkeurig zijn, dus u kunt de vergoedingen voor uw transacties te veel betalen of te weinig betalen", "disable_fiat": "Schakel Fiat uit", "disable_sell": "Verkoopactie uitschakelen", + "disable_tor": "Schakel Tor uit", "disable_trade_option": "Schakel handelsoptie uit", "disableBatteryOptimization": "Schakel de batterijoptimalisatie uit", "disableBatteryOptimizationDescription": "Wilt u de optimalisatie van de batterij uitschakelen om achtergrondsynchronisatie te laten werken, vrijer en soepeler?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "We genereren nieuwe adressen elke keer dat u er een gebruikt, maar eerdere adressen blijven werken", "email_address": "E-mailadres", "enable": "Inschakelen", + "enable_builtin_tor": "Schakel ingebouwde tor in", "enable_mempool_api": "Mempool API voor nauwkeurige kosten en datums", "enable_replace_by_fee": "Schakel vervangen door een fee", "enable_silent_payments_scanning": "Begin met het scannen van stille betalingen, totdat de tip is bereikt", @@ -334,6 +336,7 @@ "error_while_processing": "Er is een fout opgetreden tijdens het procederen", "errorGettingCredentials": "Mislukt: fout bij het ophalen van inloggegevens", "errorSigningTransaction": "Er is een fout opgetreden tijdens het ondertekenen van de transactie", + "establishing_tor_connection": "Estabilishing Tor -verbinding", "estimated": "Geschatte", "estimated_new_fee": "Geschatte nieuwe vergoeding", "estimated_receive_amount": "Geschat ontvangen bedrag", @@ -860,6 +863,7 @@ "sort_by": "Sorteer op", "spend_key_private": "Sleutel uitgeven (privaat)", "spend_key_public": "Sleutel uitgeven (openbaar)", + "starting_tor_proxy": "Tor Proxy starten", "status": "Staat: ", "step": "Stap", "string_default": "Standaard", @@ -916,6 +920,8 @@ "tokenID": "ID kaart", "ton_extra_info": "Vergeet niet om de memo -ID op te geven tijdens het verzenden van de ton -transactie voor de uitwisseling", "tor_connection": "Tor-verbinding", + "tor_connection_timeout": "Tor -verbindingstime -out", + "tor_experimental": "Dit is een experimentele functie, niet alle verbindingen gemaakt van Cake Wallet Support Tor Routing - Sommige verbindingen kunnen ClearNet gebruiken, om app -verkeer volledig te routeren Tor met VPN -modus.", "tor_only": "Alleen Tor", "total": "Totaal", "total_saving": "Totale besparingen", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 78a1af410..d83ef2f3e 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Wyłączając tą opcję, stawki opłaty mogą być w niektórych przypadkach niedokładne, więc możesz przepłacić opłatę za transakcje", "disable_fiat": "Wyłącz waluty FIAT", "disable_sell": "Wyłącz akcję sprzedaży", + "disable_tor": "Wyłącz Tor", "disable_trade_option": "Wyłącz opcję handlu", "disableBatteryOptimization": "Wyłącz optymalizację baterii", "disableBatteryOptimizationDescription": "Czy chcesz wyłączyć optymalizację baterii, aby synchronizacja tła działała swobodniej i płynnie?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Za każdym razem, gdy wykorzystasz adres, dla wiekszej prywatności generujemy nowy, ale poprzednie adresy nadal działają, i moga odbierać środki", "email_address": "Adres e-mail", "enable": "Włącz", + "enable_builtin_tor": "Włącz wbudowany tor", "enable_mempool_api": "Mempool API dla dokładnych opłat i dat", "enable_replace_by_fee": "Włącz wymianę po lewej", "enable_silent_payments_scanning": "Zacznij skanować Silent Payments", @@ -334,6 +336,7 @@ "error_while_processing": "Wystąpił błąd podczas przeróbki", "errorGettingCredentials": "Niepowodzenie: Błąd podczas uzyskiwania poświadczeń", "errorSigningTransaction": "Wystąpił błąd podczas podpisywania transakcji", + "establishing_tor_connection": "Ustalenie połączenia TOR", "estimated": "Oszacowano", "estimated_new_fee": "Szacowana nowa opłata", "estimated_receive_amount": "Szacowana kwota otrzymania", @@ -860,6 +863,7 @@ "sort_by": "Sortuj według", "spend_key_private": "Klucz prywatny", "spend_key_public": "Klucz publiczny", + "starting_tor_proxy": "Rozpoczęcie proxy TOR", "status": "Status: ", "step": "Krok", "string_default": "Domyślny", @@ -916,6 +920,7 @@ "tokenID": "ID", "ton_extra_info": "Nie zapomnij określić identyfikatora notatki podczas wysyłania transakcji TON dla wymiany", "tor_connection": "Połączenie przez Tor", + "tor_connection_timeout": "Tor Connection Timeout", "tor_only": "Tylko sieć Tor", "total": "Całkowity", "total_saving": "Całkowite oszczędności", @@ -1105,5 +1110,6 @@ "you_will_receive_estimated_amount": "Otrzymasz(oszacowane )", "you_will_send": "Konwertuj z", "youCanGoBackToYourDapp": "Możesz teraz wrócić do swojego dapp", - "yy": "RR" + "yy": "RR", + "tor_experimental": "Jest to funkcja eksperymentalna. Nie wszystkie połączenia wykonane z Cake Wallet korzystają z Tor, niektóre połączenia mogą korzystać z Clearnet, aby w pełni przekierować ruch skorzystaj z Tor w trybie VPN." } \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 64c54916a..020f37a78 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Ao desativar isso, as taxas de taxas podem ser imprecisas em alguns casos, para que você possa acabar pagando demais ou pagando as taxas por suas transações", "disable_fiat": "Desativar fiat", "disable_sell": "Desativar ação de venda", + "disable_tor": "Desativar tor", "disable_trade_option": "Desativar a opção comercial", "disableBatteryOptimization": "Desative a otimização da bateria", "disableBatteryOptimizationDescription": "Deseja desativar a otimização da bateria para fazer a sincronização de fundo funcionar de forma mais livre e suave?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Geramos novos endereços cada vez que você usa um, mas os endereços anteriores continuam funcionando", "email_address": "Endereço de e-mail", "enable": "Habilitar", + "enable_builtin_tor": "Ative o Buildi-in Tor", "enable_mempool_api": "Mempool API para taxas e datas precisas", "enable_replace_by_fee": "Habilite substituir por taxa", "enable_silent_payments_scanning": "Comece a escanear pagamentos silenciosos, até que o topo seja alcançada", @@ -334,6 +336,7 @@ "error_while_processing": "Ocorreu um erro ao procurar", "errorGettingCredentials": "Falha: Erro ao obter credenciais", "errorSigningTransaction": "Ocorreu um erro ao assinar a transação", + "establishing_tor_connection": "Estabilizar a conexão com", "estimated": "Estimado", "estimated_new_fee": "Nova taxa estimada", "estimated_receive_amount": "Valor estimado de recebimento", @@ -862,6 +865,7 @@ "sort_by": "Ordenar por", "spend_key_private": "Chave de gastos (privada)", "spend_key_public": "Chave de gastos (pública)", + "starting_tor_proxy": "Começando por proxy", "status": "Status: ", "step": "Etapa", "string_default": "Padrão", @@ -918,6 +922,8 @@ "tokenID": "EU IA", "ton_extra_info": "Não se esqueça de especificar o ID do memorando ao enviar a transação TON para a troca", "tor_connection": "Conexão Tor", + "tor_connection_timeout": "Tor de tempo limite da conexão", + "tor_experimental": "Esse é um recurso experimental, nem todas as conexões feitas com o suporte para o suporte para a carteira de bolo - algumas conexões podem usar o ClearNet, a fim de rotear totalmente o tráfego de aplicativos TOR com o modo VPN.", "tor_only": "Tor apenas", "total": "Total", "total_saving": "Economia total", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 7f0ce362a..003829450 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Выключив это, в некоторых случаях ставки платы могут быть неточными, так что вы можете в конечном итоге переплачивать или недоплачивать сборы за ваши транзакции", "disable_fiat": "Отключить фиат", "disable_sell": "Отключить действие продажи", + "disable_tor": "Отключить Tor", "disable_trade_option": "Отключить возможность торговли", "disableBatteryOptimization": "Отключить оптимизацию батареи", "disableBatteryOptimizationDescription": "Вы хотите отключить оптимизацию батареи, чтобы сделать фона синхронизации более свободно и плавно?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Мы генерируем новые адреса каждый раз, когда вы их используете, но предыдущие адреса продолжают работать.", "email_address": "Адрес электронной почты", "enable": "Давать возможность", + "enable_builtin_tor": "Включить встроенный в Tor", "enable_mempool_api": "Mempool API за точные сборы и даты", "enable_replace_by_fee": "Включить замену за пикой", "enable_silent_payments_scanning": "Начните сканировать безмолвные платежи, пока не будет достигнут наконечник", @@ -334,6 +336,7 @@ "error_while_processing": "Произошла ошибка во время выплаты", "errorGettingCredentials": "Не удалось: ошибка при получении учетных данных.", "errorSigningTransaction": "Произошла ошибка при подписании транзакции", + "establishing_tor_connection": "Остановка связи", "estimated": "Примерно", "estimated_new_fee": "Расчетная новая плата", "estimated_receive_amount": "Расчетная сумма получения", @@ -861,6 +864,7 @@ "sort_by": "Сортировать по", "spend_key_private": "Приватный ключ траты", "spend_key_public": "Публичный ключ траты", + "starting_tor_proxy": "Запуск прокси", "status": "Статус: ", "step": "Шаг", "string_default": "По умолчанию", @@ -917,6 +921,8 @@ "tokenID": "ИДЕНТИФИКАТОР", "ton_extra_info": "Пожалуйста, не забудьте указать идентификатор записки при отправке TON транзакции для обмена", "tor_connection": "Тор соединение", + "tor_connection_timeout": "TOR Timeout", + "tor_experimental": "Это экспериментальная функция, а не все соединения, сделанные из поддержки TOR для торта - некоторые подключения могут использовать ClearNet, чтобы полностью маршрутироваться с использованием трафика приложений в режиме VPN.", "tor_only": "Только Tor", "total": "Общий", "total_saving": "Общая экономия", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 9908f93a0..6a9c59f57 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "โดยการปิดสิ่งนี้อัตราค่าธรรมเนียมอาจไม่ถูกต้องในบางกรณีดังนั้นคุณอาจจบลงด้วยการจ่ายเงินมากเกินไปหรือจ่ายค่าธรรมเนียมสำหรับการทำธุรกรรมของคุณมากเกินไป", "disable_fiat": "ปิดใช้งานสกุลเงินตรา", "disable_sell": "ปิดการใช้งานการขาย", + "disable_tor": "ปิดการใช้งาน TOR", "disable_trade_option": "ปิดใช้งานตัวเลือกการค้า", "disableBatteryOptimization": "ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่", "disableBatteryOptimizationDescription": "คุณต้องการปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่เพื่อให้การซิงค์พื้นหลังทำงานได้อย่างอิสระและราบรื่นมากขึ้นหรือไม่?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "เราสร้างที่อยู่ใหม่ทุกครั้งที่คุณใช้หนึ่งอย่าง แต่ที่อยู่เก่ายังสามารถใช้ได้ต่อไป", "email_address": "ที่อยู่อีเมล", "enable": "เปิดใช้งาน", + "enable_builtin_tor": "เปิดใช้งาน Tor ในตัว", "enable_mempool_api": "Mempool API สำหรับค่าธรรมเนียมและวันที่ที่ถูกต้อง", "enable_replace_by_fee": "เปิดใช้งานการเปลี่ยนโดยค่าธรรมเนียม", "enable_silent_payments_scanning": "เริ่มสแกนการชำระเงินแบบเงียบจนกว่าจะถึงปลาย", @@ -334,6 +336,7 @@ "error_while_processing": "เกิดข้อผิดพลาดในขณะที่ proceessing", "errorGettingCredentials": "ล้มเหลว: เกิดข้อผิดพลาดขณะรับข้อมูลรับรอง", "errorSigningTransaction": "เกิดข้อผิดพลาดขณะลงนามธุรกรรม", + "establishing_tor_connection": "การเชื่อมต่อ Tor", "estimated": "ประมาณการ", "estimated_new_fee": "ค่าธรรมเนียมใหม่โดยประมาณ", "estimated_receive_amount": "โดยประมาณว่าจำนวนเงินที่ได้รับ", @@ -860,6 +863,7 @@ "sort_by": "เรียงตาม", "spend_key_private": "คีย์จ่าย (ส่วนตัว)", "spend_key_public": "คีย์จ่าย (สาธารณะ)", + "starting_tor_proxy": "เริ่มพร็อกซี TOR", "status": "สถานะ: ", "step": "ขั้นตอน", "string_default": "ค่าเริ่มต้น", @@ -916,6 +920,8 @@ "tokenID": "บัตรประจำตัวประชาชน", "ton_extra_info": "โปรดอย่าลืมระบุรหัสบันทึกในขณะที่ส่งธุรกรรม TON สำหรับการแลกเปลี่ยน", "tor_connection": "การเชื่อมต่อทอร์", + "tor_connection_timeout": "หมดเวลาเชื่อมต่อ tor", + "tor_experimental": "นี่เป็นคุณสมบัติการทดลองไม่ใช่การเชื่อมต่อทั้งหมดที่ทำจากการรองรับเค้กกระเป๋าเงิน TOR การกำหนดเส้นทาง - การเชื่อมต่อบางอย่างอาจใช้ Clearnet เพื่อกำหนดเส้นทางการจราจรแอพใช้ TOR ด้วยโหมด VPN", "tor_only": "Tor เท่านั้น", "total": "ทั้งหมด", "total_saving": "ประหยัดรวม", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 9ad025aa7..2f494adce 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Sa pamamagitan ng pag -off nito, ang mga rate ng bayad ay maaaring hindi tumpak sa ilang mga kaso, kaya maaari mong tapusin ang labis na bayad o pagsuporta sa mga bayarin para sa iyong mga transaksyon", "disable_fiat": "Huwag paganahin ang fiat", "disable_sell": "Huwag paganahin ang pagkilos ng pagbebenta", + "disable_tor": "Huwag paganahin ang tor", "disable_trade_option": "Huwag paganahin ang pagpipilian sa kalakalan", "disableBatteryOptimization": "Huwag Paganahin ang Pag-optimize ng Baterya", "disableBatteryOptimizationDescription": "Nais mo bang huwag paganahin ang pag-optimize ng baterya upang gawing mas malaya at maayos ang background sync?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Bumubuo kami ng mga bagong address sa tuwing gagamit ka ng isa, ngunit ang mga nakaraang address ay patuloy na gumagana", "email_address": "Email Address", "enable": "Paganahin", + "enable_builtin_tor": "Paganahin ang built-in tor", "enable_mempool_api": "Mempool API para sa tumpak na bayad at mga petsa", "enable_replace_by_fee": "Paganahin ang Replace-By-Fee", "enable_silent_payments_scanning": "Simulan ang pag -scan ng tahimik na pagbabayad, hanggang sa maabot ang tip", @@ -334,6 +336,7 @@ "error_while_processing": "May naganap na error habang nagpapatuloy", "errorGettingCredentials": "Nabigo: Error habang kumukuha ng mga kredensyal", "errorSigningTransaction": "Error habang pinipirmahan ang transaksyon", + "establishing_tor_connection": "Koneksyon ng Tor", "estimated": "Tinatayang", "estimated_new_fee": "Tinatayang bagong fee", "estimated_receive_amount": "Tinatayang natanggap na halaga", @@ -860,6 +863,7 @@ "sort_by": "Pag-uri-uriin sa pamamagitan ng", "spend_key_private": "Spend key (private)", "spend_key_public": "Spend key (public)", + "starting_tor_proxy": "Simula sa Tor Proxy", "status": "Katayuan: ", "step": "Hakbang", "string_default": "Default", @@ -916,6 +920,8 @@ "tokenID": "ID", "ton_extra_info": "Mangyaring huwag kalimutan na tukuyin ang memo ID habang nagpapadala ng toneladang transaksyon para sa palitan", "tor_connection": "Koneksyon ng Tor", + "tor_connection_timeout": "Oras ng koneksyon sa tor", + "tor_experimental": "Ito ay isang pang -eksperimentong tampok, hindi lahat ng mga koneksyon na ginawa mula sa cake ng suporta sa wallet ng tor ruta - ang ilang mga koneksyon ay maaaring gumamit ng clearnet, upang ganap na ruta ang paggamit ng trapiko ng trapiko na may mode na VPN.", "tor_only": "Tor lamang", "total": "Kabuuan", "total_saving": "Kabuuang ipon", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index add8ee22f..0b67edbe8 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Bunu kapatarak, ücret oranları bazı durumlarda yanlış olabilir, bu nedenle işlemleriniz için ücretleri fazla ödeyebilir veya az ödeyebilirsiniz.", "disable_fiat": "İtibari paraları devre dışı bırak", "disable_sell": "Satış işlemini devre dışı bırak", + "disable_tor": "Tor'u devre dışı bırak", "disable_trade_option": "Ticaret seçeneğini devre dışı bırakın", "disableBatteryOptimization": "Pil optimizasyonunu devre dışı bırakın", "disableBatteryOptimizationDescription": "Arka plan senkronizasyonunu daha özgür ve sorunsuz bir şekilde çalıştırmak için pil optimizasyonunu devre dışı bırakmak istiyor musunuz?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Adresini her kullandığında yeni adres oluşturuyoruz, ancak önceki adresler de çalışmaya devam eder", "email_address": "E-posta Adresi", "enable": "Olanak vermek", + "enable_builtin_tor": "Yerleşik Tor'u etkinleştirin", "enable_mempool_api": "Doğru ücretler ve tarihler için Mempool API'si", "enable_replace_by_fee": "Farklı Değiştir'i Etkinleştir", "enable_silent_payments_scanning": "Bahşiş ulaşılıncaya kadar sessiz ödemeleri taramaya başlayın", @@ -334,6 +336,7 @@ "error_while_processing": "ProceSting sırasında bir hata oluştu", "errorGettingCredentials": "Başarısız: Kimlik bilgileri alınırken hata oluştu", "errorSigningTransaction": "İşlem imzalanırken bir hata oluştu", + "establishing_tor_connection": "Fazla Tor Bağlantısı", "estimated": "Tahmini", "estimated_new_fee": "Tahmini yeni ücret", "estimated_receive_amount": "Tahmini alma miktarı", @@ -860,6 +863,7 @@ "sort_by": "Göre sırala", "spend_key_private": "Harcama anahtarı (özel)", "spend_key_public": "Harcama anahtarı (genel)", + "starting_tor_proxy": "Tor Proxy'yi Başlatma", "status": "Durum: ", "step": "Adım", "string_default": "Varsayılan", @@ -916,6 +920,8 @@ "tokenID": "İD", "ton_extra_info": "Lütfen değişim için ton işlemini gönderirken not kimliğini belirtmeyi unutmayın", "tor_connection": "Tor bağlantısı", + "tor_connection_timeout": "Tor bağlantı zaman aşımı", + "tor_experimental": "Bu deneysel bir özelliktir, kek cüzdanı desteğinden yapılan tüm bağlantılar değil, TOR yönlendirmesinden yapılmıştır - bazı bağlantılar, VPN moduyla uygulama trafik kullanımını tam olarak yönlendirmek için ClearNet kullanabilir.", "tor_only": "Yalnızca Tor", "total": "Toplam", "total_saving": "Toplam Tasarruf", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 07ed5d532..a4078a341 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Вимкнувши це, ставки плати в деяких випадках можуть бути неточними, тому ви можете переплатити або недооплатити плату за свої транзакції", "disable_fiat": "Вимкнути фиат", "disable_sell": "Вимкнути дію продажу", + "disable_tor": "Вимкнути Тор", "disable_trade_option": "Вимкнути можливість торгівлі", "disableBatteryOptimization": "Вимкнути оптимізацію акумулятора", "disableBatteryOptimizationDescription": "Ви хочете відключити оптимізацію акумулятора, щоб зробити фонову синхронізацію більш вільно та плавно?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "Ми створюємо нові адреси щоразу, коли ви використовуєте їх, але попередні адреси продовжують працювати", "email_address": "Адреса електронної пошти", "enable": "Ввімкнути", + "enable_builtin_tor": "Увімкнути вбудований tor", "enable_mempool_api": "API Mempool для точних зборів та дат", "enable_replace_by_fee": "Увімкнути заміну з комісією", "enable_silent_payments_scanning": "Почніть сканувати мовчазні платежі, поки не буде досягнуто наконечника", @@ -334,6 +336,7 @@ "error_while_processing": "Помилка сталася під час проектування", "errorGettingCredentials": "Помилка: помилка під час отримання облікових даних", "errorSigningTransaction": "Під час підписання транзакції сталася помилка", + "establishing_tor_connection": "Закінчення підключення до TOR", "estimated": "Приблизно ", "estimated_new_fee": "Орієнтовна нова комісія", "estimated_receive_amount": "Орієнтовна сума отримує", @@ -861,6 +864,7 @@ "sort_by": "Сортувати за", "spend_key_private": "Приватний ключ витрати", "spend_key_public": "Публічний ключ витрати", + "starting_tor_proxy": "Початок Tor Proxy", "status": "Статус: ", "step": "Крок", "string_default": "За замовчуванням", @@ -917,6 +921,8 @@ "tokenID": "ID", "ton_extra_info": "Не забудьте вказати ідентифікатор пам’яті під час надсилання транзакції TON для обміну", "tor_connection": "Підключення Tor", + "tor_connection_timeout": "Тайм -аут підключення TOR", + "tor_experimental": "Це експериментальна функція, не всі з'єднання, зроблені з підтримки тортів, що підтримують маршрутизацію - деякі з'єднання можуть використовувати Clearnet, щоб повністю маршрутизувати трафік додатків, використовуючи TOR з режимом VPN.", "tor_only": "Тільки Tor", "total": "Загальний", "total_saving": "Загальна економія", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 7240db6b0..3c805fe6b 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "اس کو بند کرنے سے ، کچھ معاملات میں فیس کی شرح غلط ہوسکتی ہے ، لہذا آپ اپنے لین دین کے لئے فیسوں کو زیادہ ادائیگی یا ادائیگی ختم کرسکتے ہیں۔", "disable_fiat": "فیاٹ کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", + "disable_tor": "ٹور کو غیر فعال کریں", "disable_trade_option": "تجارت کے آپشن کو غیر فعال کریں", "disableBatteryOptimization": "بیٹری کی اصلاح کو غیر فعال کریں", "disableBatteryOptimizationDescription": "کیا آپ پس منظر کی مطابقت پذیری کو زیادہ آزادانہ اور آسانی سے چلانے کے لئے بیٹری کی اصلاح کو غیر فعال کرنا چاہتے ہیں؟", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "جب بھی آپ ایک کا استعمال کرتے ہیں تو ہم نئے پتے تیار کرتے ہیں، لیکن پچھلے پتے کام کرتے رہتے ہیں۔", "email_address": "ای میل اڈریس", "enable": "قابل بنائیں", + "enable_builtin_tor": "بلٹٹی ان ٹور کو فعال کریں", "enable_mempool_api": "درست فیسوں اور تاریخوں کے لئے میمپول API", "enable_replace_by_fee": "فی فیس کو تبدیل کریں", "enable_silent_payments_scanning": "خاموش ادائیگیوں کو اسکین کرنا شروع کریں ، جب تک کہ نوک نہ پہنچ جائے", @@ -334,6 +336,7 @@ "error_while_processing": "ایک غلطی پیش کرتے وقت ہوئی", "errorGettingCredentials": "۔ﯽﺑﺍﺮﺧ ﮟﯿﻣ ﮯﻧﺮﮐ ﻞﺻﺎﺣ ﺩﺎﻨﺳﺍ :ﻡﺎﮐﺎﻧ", "errorSigningTransaction": "۔ﮯﮨ ﯽﺌﮔﺁ ﺶﯿﭘ ﯽﺑﺍﺮﺧ ﮏﯾﺍ ﺖﻗﻭ ﮯﺗﺮﮐ ﻂﺨﺘﺳﺩ ﺮﭘ ﻦﯾﺩ ﻦﯿﻟ", + "establishing_tor_connection": "ٹور کنکشن کو کھڑا کرنا", "estimated": "تخمینہ لگایا", "estimated_new_fee": "تخمینہ شدہ نئی فیس", "estimated_receive_amount": "تخمینہ وصول کی رقم", @@ -862,6 +865,7 @@ "sort_by": "ترتیب دیں", "spend_key_private": "خرچ کی کلید (نجی)", "spend_key_public": "خرچ کی کلید (عوامی)", + "starting_tor_proxy": "ٹور پراکسی شروع کرنا", "status": "حالت:", "step": "مرحلہ", "string_default": "پہلے سے طے شدہ", @@ -918,6 +922,8 @@ "tokenID": "ID", "ton_extra_info": "ایکسچینج کے لئے ٹن ٹرانزیکشن بھیجتے وقت براہ کرم میمو آئی ڈی کی وضاحت کرنا نہ بھولیں", "tor_connection": "ﻦﺸﮑﻨﮐ ﺭﻮﭨ", + "tor_connection_timeout": "ٹور کنکشن ٹائم آؤٹ", + "tor_experimental": "یہ ایک تجرباتی خصوصیت ہے ، کیک پرس پرس سپورٹ ٹور روٹنگ سے بنے تمام رابطے نہیں - کچھ رابطے کلینیٹ کا استعمال کرسکتے ہیں ، تاکہ مکمل طور پر ایپ ٹریفک کو VPN وضع کے ساتھ ٹور کا استعمال کریں۔", "tor_only": "صرف Tor", "total": "کل", "total_saving": "کل بچت", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 3dd184ddd..ea3281b2a 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -268,6 +268,7 @@ "disable_fee_api_warning": "Khi tắt chức năng này, tỉ lệ phí có thể không chính xác trong một số trường hợp, dẫn đến bạn trả quá hoặc không đủ phí cho giao dịch của mình.", "disable_fiat": "Vô hiệu hóa tiền tệ fiat", "disable_sell": "Vô hiệu hóa chức năng bán", + "disable_tor": "Tắt Tor", "disable_trade_option": "Tắt tùy chọn thương mại", "disableBatteryOptimization": "Vô hiệu hóa Tối ưu hóa Pin", "disableBatteryOptimizationDescription": "Bạn có muốn vô hiệu hóa tối ưu hóa pin để đồng bộ hóa nền hoạt động mượt mà hơn không?", @@ -293,6 +294,7 @@ "electrum_address_disclaimer": "Chúng tôi tạo địa chỉ mới mỗi khi bạn sử dụng, nhưng các địa chỉ cũ vẫn tiếp tục hoạt động", "email_address": "Địa chỉ Email", "enable": "Cho phép", + "enable_builtin_tor": "Bật Buildi-In Tor", "enable_mempool_api": "API Mempool cho các khoản phí và ngày chính xác", "enable_replace_by_fee": "Bật Thay thế Bằng Phí", "enable_silent_payments_scanning": "Bật quét thanh toán im lặng", @@ -333,6 +335,7 @@ "error_while_processing": "Xảy ra lỗi trong khi sử dụng", "errorGettingCredentials": "Không thành công: Lỗi khi nhận thông tin xác thực", "errorSigningTransaction": "Đã xảy ra lỗi khi ký giao dịch", + "establishing_tor_connection": "Kết nối tor", "estimated": "Ước tính", "estimated_new_fee": "Phí mới ước tính", "estimated_receive_amount": "Số tiền nhận ước tính", @@ -857,6 +860,7 @@ "sort_by": "Sắp xếp theo", "spend_key_private": "Khóa chi tiêu (riêng tư)", "spend_key_public": "Khóa chi tiêu (công khai)", + "starting_tor_proxy": "Bắt đầu proxy tor", "status": "Trạng thái: ", "step": "Bước chân", "string_default": "Mặc định", @@ -913,6 +917,8 @@ "tokenID": "ID", "ton_extra_info": "Xin đừng quên chỉ định ID ghi nhớ trong khi gửi giao dịch tấn cho trao đổi", "tor_connection": "Kết nối Tor", + "tor_connection_timeout": "Thời gian chờ kết nối tor", + "tor_experimental": "Đây là một tính năng thử nghiệm, không phải tất cả các kết nối được làm từ Wake Wallet Hỗ trợ định tuyến TOR - một số kết nối có thể sử dụng ClearNet, để định tuyến đầy đủ về lưu lượng truy cập ứng dụng Tor với chế độ VPN.", "tor_only": "Chỉ Tor", "total": "Tổng cộng", "total_saving": "Tiết kiệm tổng cộng", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 0a3e382e7..109dac40f 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "Nipa yiyi eyi kuro, awọn oṣuwọn owo naa le jẹ aiṣe deede ni awọn ọrọ kan, nitorinaa o le pari apọju tabi awọn idiyele ti o ni agbara fun awọn iṣowo rẹ", "disable_fiat": "Pa owó tí ìjọba pàṣẹ wa lò", "disable_sell": "Ko iṣọrọ iṣọrọ", + "disable_tor": "Mu lile", "disable_trade_option": "Mu aṣayan iṣowo ṣiṣẹ", "disableBatteryOptimization": "Mu Ifasi batiri", "disableBatteryOptimizationDescription": "Ṣe o fẹ lati mu iṣapelo batiri si lati le ṣiṣe ayẹwo ẹhin ati laisiyonu?", @@ -295,6 +296,7 @@ "electrum_address_disclaimer": "A dá àwọn àdírẹ́sì títun ní gbogbo àwọn ìgbà t'ẹ́ lo ó kan ṣùgbọ́n ẹ lè tẹ̀síwájú lo àwọn àdírẹ́sì tẹ́lẹ̀tẹ́lẹ̀.", "email_address": "Àdírẹ́sì ímeèlì", "enable": "Mu ṣiṣẹ", + "enable_builtin_tor": "Mu ṣiṣẹ", "enable_mempool_api": "Mempool API fun awọn owo deede ati awọn ọjọ", "enable_replace_by_fee": "Mu ki o rọpo", "enable_silent_payments_scanning": "Bẹrẹ awọn sisanwo ipalọlọ, titi ti o fi de opin", @@ -335,6 +337,7 @@ "error_while_processing": "Aṣiṣe kan waye lakoko ti o duro", "errorGettingCredentials": "Kuna: Aṣiṣe lakoko gbigba awọn iwe-ẹri", "errorSigningTransaction": "Aṣiṣe kan ti waye lakoko ti o fowo si iṣowo", + "establishing_tor_connection": "Isopọ Stanishing", "estimated": "Ó tó a fojú díwọ̀n", "estimated_new_fee": "Ifoju tuntun owo tuntun", "estimated_receive_amount": "Ifoju gba iye", @@ -861,6 +864,7 @@ "sort_by": "Sa pelu", "spend_key_private": "Kọ́kọ́rọ́ sísan (àdáni)", "spend_key_public": "Kọ́kọ́rọ́ sísan (kò àdáni)", + "starting_tor_proxy": "Bibẹrẹ Oluṣaaju Aṣoju", "status": "Tó ń ṣẹlẹ̀: ", "step": "Igbesẹ", "string_default": "Aiyipada", @@ -917,6 +921,8 @@ "tokenID": "ID", "ton_extra_info": "Jọwọ maṣe gbagbe lati tokasi ID akọsilẹ lakoko fifiranṣẹ idunadura pupọ fun paṣipaarọ naa", "tor_connection": "Tor asopọ", + "tor_connection_timeout": "Akoko asopọ asopọ", + "tor_experimental": "Eyi jẹ ẹya ti esiperimenta ti a ṣe lati awọn Awoṣe Adwat atilẹyin ti o wa ni atilẹyin idiwọ - diẹ ninu awọn asopọ le lo imple, lati le ni ipa ọna App Prat pẹlu ipo VPN.", "tor_only": "Tor nìkan", "total": "Apapọ", "total_saving": "Owó t'ẹ́ ti pamọ́", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 2f02673b7..8f146e0fb 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -269,6 +269,7 @@ "disable_fee_api_warning": "通过将其关闭,在某些情况下,收费率可能不准确,因此您最终可能会超额付款或支付交易费用", "disable_fiat": "禁用法令", "disable_sell": "禁用卖出操作", + "disable_tor": "禁用TOR", "disable_trade_option": "禁用贸易选项", "disableBatteryOptimization": "禁用电池优化", "disableBatteryOptimizationDescription": "您是否要禁用电池优化以使背景同步更加自由,平稳地运行?", @@ -294,6 +295,7 @@ "electrum_address_disclaimer": "每次您使用一个地址时,我们都会生成新地址,但之前的地址仍然有效", "email_address": "电子邮件地址", "enable": "使能够", + "enable_builtin_tor": "启用内置的tor", "enable_mempool_api": "Mempool API获得准确的费用和日期", "enable_replace_by_fee": "启用by-Fee替换", "enable_silent_payments_scanning": "开始扫描无声付款,直到达到提示", @@ -334,6 +336,7 @@ "error_while_processing": "发动机时发生错误", "errorGettingCredentials": "失败:获取凭据时出错", "errorSigningTransaction": "签署交易时发生错误", + "establishing_tor_connection": "tor连接", "estimated": "估计值", "estimated_new_fee": "估计新费用", "estimated_receive_amount": "估计接收金额", @@ -860,6 +863,7 @@ "sort_by": "排序方式", "spend_key_private": "Spend 密钥 (私钥)", "spend_key_public": "Spend 密钥 (公钥)", + "starting_tor_proxy": "启动Tor代理", "status": "状态: ", "step": "步", "string_default": "默认", @@ -916,6 +920,8 @@ "tokenID": "ID", "ton_extra_info": "请不要忘记在发送TON交易时指定备忘录", "tor_connection": "Tor连接", + "tor_connection_timeout": "TOR连接超时", + "tor_experimental": "这是一个实验功能,并非所有由Cake Wallet Support Tor路由组成的连接 - 某些连接可能会使用Clearnet,以便完全路由App App App Appl使用VPN模式。", "tor_only": "仅限 Tor", "total": "全部的", "total_saving": "总储蓄", From 4434ad7363f300f6002b442413b7f28ec14c0af8 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 24 Jun 2025 03:41:43 +0100 Subject: [PATCH 19/29] CW-1086: Polygon Issues (#2329) * fix(polygon): Polygon wallets not showing in address book when sending tokens * feat(polygon_issues): Enhance gas price handling in EVM Chains This change: - Updates gas price calculation to account for minimum priority fee in Polygon - Adjusts gas price handling to use maxFeePerGas when gasPrice is not provided. - Fixes issue with send all for Polygon --- cw_evm/lib/evm_chain_client.dart | 5 +- cw_evm/lib/evm_chain_wallet.dart | 54 +++++++++++++++---- .../lib/default_polygon_erc20_tokens.dart | 2 +- cw_polygon/lib/polygon_client.dart | 11 +++- cw_polygon/lib/polygon_wallet.dart | 7 ++- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index 5f383b5a3..1fecc9c1e 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -109,7 +109,6 @@ abstract class EVMChainClient { sender: senderAddress, to: toAddress, value: value, - // maxFeePerGas: maxFeePerGas, ); return estimatedGas.toInt(); @@ -165,6 +164,7 @@ abstract class EVMChainClient { required int exponent, String? contractAddress, String? data, + int? gasPrice, }) async { assert(currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly || @@ -180,6 +180,7 @@ abstract class EVMChainClient { data: data != null ? hexToBytes(data) : null, maxGas: estimatedGasUnits, maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null, ); Uint8List signedTransaction; @@ -224,6 +225,7 @@ abstract class EVMChainClient { required EVMChainTransactionPriority priority, required int exponent, required String contractAddress, + int? gasPrice, }) async { final Transaction transaction = createTransaction( @@ -233,6 +235,7 @@ abstract class EVMChainClient { amount: EtherAmount.zero(), maxGas: estimatedGasUnits, maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null, ); final erc20 = ERC20( diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 18f6e6ee5..1791258f5 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -263,31 +263,65 @@ abstract class EVMChainWalletBase final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); int maxFeePerGas; + int adjustedGasPrice; + + bool isPolygon = _client.chainId == 137; + if (gasBaseFee != null) { // MaxFeePerGas with EIP1559; maxFeePerGas = gasBaseFee! + priorityFee; } else { // MaxFeePerGas with gasPrice - maxFeePerGas = gasPrice; + maxFeePerGas = gasPrice + priorityFee; + } + + adjustedGasPrice = maxFeePerGas; + + // Polygon has a minimum priority fee of 25 gwei + if (isPolygon) { + int minPriorityFee = 25; + int minPriorityFeeWei = + EtherAmount.fromInt(EtherUnit.gwei, minPriorityFee).getInWei.toInt(); + + // Calculate user selected priority-based additional fee on top of minimum + int additionalPriorityFee = 0; + switch (priority) { + case EVMChainTransactionPriority.slow: + // We use minimum priority fee only + additionalPriorityFee = 0; + break; + case EVMChainTransactionPriority.medium: + // We add 15 gwei on top of minimum + additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 15).getInWei.toInt(); + break; + case EVMChainTransactionPriority.fast: + // We add 35 gwei on top of minimum + additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 35).getInWei.toInt(); + break; + } + + int totalPriorityFee = minPriorityFeeWei + additionalPriorityFee; + adjustedGasPrice = gasPrice + totalPriorityFee; + maxFeePerGas = gasPrice + totalPriorityFee; } final estimatedGas = await _client.getEstimatedGasUnitsForTransaction( contractAddress: contractAddress, senderAddress: _evmChainPrivateKey.address, value: EtherAmount.fromBigInt(EtherUnit.wei, amount!), - gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), + gasPrice: EtherAmount.fromInt(EtherUnit.wei, adjustedGasPrice), toAddress: EthereumAddress.fromHex(receivingAddressHex), maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), data: data, ); - final totalGasFee = estimatedGas * maxFeePerGas; + final totalGasFee = estimatedGas * adjustedGasPrice; return GasParamsHandler( estimatedGasUnits: estimatedGas, estimatedGasFee: totalGasFee, maxFeePerGas: maxFeePerGas, - gasPrice: gasPrice, + gasPrice: adjustedGasPrice, ); } return GasParamsHandler.zero(); @@ -476,23 +510,20 @@ abstract class EVMChainWalletBase contractAddress: transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, data: hexOpReturnMemo, + gasPrice: maxFeePerGasForTransaction, ); return pendingEVMChainTransaction; } - Future createApprovalTransaction( - BigInt amount, - String spender, - CryptoCurrency token, - EVMChainTransactionPriority priority) async { + Future createApprovalTransaction(BigInt amount, String spender, + CryptoCurrency token, EVMChainTransactionPriority priority) async { final CryptoCurrency transactionCurrency = balance.keys.firstWhere((element) => element.title == token.title); assert(transactionCurrency is Erc20Token); final data = _client.getEncodedDataForApprovalTransaction( - contractAddress: EthereumAddress.fromHex( - (transactionCurrency as Erc20Token).contractAddress), + contractAddress: EthereumAddress.fromHex((transactionCurrency as Erc20Token).contractAddress), value: EtherAmount.fromBigInt(EtherUnit.wei, amount), toAddress: EthereumAddress.fromHex(spender), ); @@ -515,6 +546,7 @@ abstract class EVMChainWalletBase estimatedGasUnits: gasFeesModel.estimatedGasUnits, exponent: transactionCurrency.decimal, contractAddress: transactionCurrency.contractAddress, + gasPrice: gasFeesModel.gasPrice, ); } diff --git a/cw_polygon/lib/default_polygon_erc20_tokens.dart b/cw_polygon/lib/default_polygon_erc20_tokens.dart index 2c739e8c5..208a63345 100644 --- a/cw_polygon/lib/default_polygon_erc20_tokens.dart +++ b/cw_polygon/lib/default_polygon_erc20_tokens.dart @@ -84,6 +84,6 @@ class DefaultPolygonErc20Tokens { .iconPath; } catch (_) {} - return Erc20Token.copyWith(token, iconPath, 'POLY'); + return Erc20Token.copyWith(token, iconPath, 'POL'); }).toList(); } diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart index 5c344debe..62dcec50e 100644 --- a/cw_polygon/lib/polygon_client.dart +++ b/cw_polygon/lib/polygon_client.dart @@ -18,13 +18,20 @@ class PolygonClient extends EVMChainClient { EtherAmount? gasPrice, EtherAmount? maxFeePerGas, }) { + EtherAmount? finalGasPrice = gasPrice; + + if (gasPrice == null && maxFeePerGas != null) { + // If we have EIP-1559 parameters but no legacy gasPrice, then use maxFeePerGas as gasPrice + finalGasPrice = maxFeePerGas; + } + return Transaction( from: from, to: to, value: amount, - // data: data, + data: data, maxGas: maxGas, - // gasPrice: gasPrice, + gasPrice: finalGasPrice, // maxFeePerGas: maxFeePerGas, // maxPriorityFeePerGas: maxPriorityFeePerGas, ); diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart index c1d36b9cb..991552068 100644 --- a/cw_polygon/lib/polygon_wallet.dart +++ b/cw_polygon/lib/polygon_wallet.dart @@ -45,7 +45,12 @@ class PolygonWallet extends EVMChainWallet { final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens; for (final token in initialErc20Tokens) { - if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) { + if (evmChainErc20TokensBox.containsKey(token.contractAddress)) { + final existingToken = evmChainErc20TokensBox.get(token.contractAddress); + if (existingToken?.tag != token.tag) { + evmChainErc20TokensBox.put(token.contractAddress, token); + } + } else { if (isMigration) token.enabled = false; evmChainErc20TokensBox.put(token.contractAddress, token); } From af89603b81c5c62b39c44def92ff58fe34791a71 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 24 Jun 2025 03:47:21 +0100 Subject: [PATCH 20/29] CW-1103:Token Validation Issues (#2327) * feat(token_validation): Improve flow for adding new tokens across wallets This change: - Implements check to see if a token is already added, preventing duplicates - Triggers dialog warning if its a duplicate token - Takes EVM Chains contract adddress case-insensitivity when making checks for potential scams. * refactor(token_validation): Modify token management flow This change: - Removes duplicate token check during token addition in EVMChainWalletBase. - Introduces a flag to indicate if a token is being edited - Adjusts token addition validation to bypass checks when editing an existing token. * Update lib/src/screens/dashboard/edit_token_page.dart --------- Co-authored-by: Omar Hatem --- cw_evm/lib/evm_chain_wallet.dart | 14 ++++++++-- lib/ethereum/cw_ethereum.dart | 12 +++++++-- lib/polygon/cw_polygon.dart | 6 +++++ lib/solana/cw_solana.dart | 6 +++++ .../screens/dashboard/edit_token_page.dart | 22 +++++++++++++++ lib/tron/cw_tron.dart | 6 +++++ .../dashboard/home_settings_view_model.dart | 27 ++++++++++++++++++- lib/zano/cw_zano.dart | 6 +++++ res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + tool/configure.dart | 5 ++++ 37 files changed, 127 insertions(+), 5 deletions(-) diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 1791258f5..a1285f0e4 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -198,8 +198,8 @@ abstract class EVMChainWalletBase for (var token in erc20Currencies) { bool isPotentialScam = false; - bool isWhitelisted = - getDefaultTokenContractAddresses.any((element) => element == token.contractAddress); + bool isWhitelisted = getDefaultTokenContractAddresses + .any((element) => element.toLowerCase() == token.contractAddress.toLowerCase()); final tokenSymbol = token.title.toUpperCase(); @@ -214,6 +214,16 @@ abstract class EVMChainWalletBase token.iconPath = null; await token.save(); } + + // For fixing wrongly classified tokens + if (!isPotentialScam && token.isPotentialScam) { + token.isPotentialScam = false; + final iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + token.iconPath = iconPath; + await token.save(); + } } } diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 3e3675d1f..a6a6a7205 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -199,8 +199,16 @@ class CWEthereum extends Ethereum { } @override - List getDefaultTokenContractAddresses() => - DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); + List getDefaultTokenContractAddresses() { + return DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); + } + + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final ethereumWallet = wallet as EthereumWallet; + return ethereumWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase()); + } Future createTokenApproval(WalletBase wallet, BigInt amount, String spender, CryptoCurrency token, TransactionPriority priority) => diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 557cedc39..2e6e7dd13 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -216,4 +216,10 @@ class CWPolygon extends Polygon { .initialPolygonErc20Tokens .map((e) => e.contractAddress) .toList(); + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final polygonWallet = wallet as PolygonWallet; + return polygonWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase()); + } } diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 9036f5584..ca001bb3b 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -155,4 +155,10 @@ class CWSolana extends Solana { List getDefaultTokenContractAddresses() { return DefaultSPLTokens().initialSPLTokens.map((e) => e.mintAddress).toList(); } + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final solanaWallet = wallet as SolanaWallet; + return solanaWallet.splTokenCurrencies.any((element) => element.mintAddress == contractAddress); + } } diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart index f36ca1456..8f9f67178 100644 --- a/lib/src/screens/dashboard/edit_token_page.dart +++ b/lib/src/screens/dashboard/edit_token_page.dart @@ -73,6 +73,7 @@ class _EditTokenPageBodyState extends State { bool _showDisclaimer = false; bool _disclaimerChecked = false; + bool isEditingToken = false; @override void initState() { @@ -88,6 +89,8 @@ class _EditTokenPageBodyState extends State { _tokenSymbolController.text = widget.token!.title; _tokenDecimalController.text = widget.token!.decimals.toString(); _tokenIconPathController.text = widget.token?.iconPath ?? ''; + + isEditingToken = true; } if (widget.initialContractAddress != null) { @@ -200,6 +203,25 @@ class _EditTokenPageBodyState extends State { onPressed: () async { if (_formKey.currentState!.validate() && (!_showDisclaimer || _disclaimerChecked)) { + final isTokenAlreadyAdded = isEditingToken + ? false + : await widget.homeSettingsViewModel + .checkIfTokenIsAlreadyAdded(_contractAddressController.text); + if (isTokenAlreadyAdded) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithOneAction( + alertTitle: S.current.warning, + alertContent: S.of(context).token_already_exists, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(dialogContext).pop(), + ); + }, + ); + return; + } + final isWhitelisted = await widget.homeSettingsViewModel .checkIfTokenIsWhitelisted(_contractAddressController.text); diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart index 8c29dab58..395129b8f 100644 --- a/lib/tron/cw_tron.dart +++ b/lib/tron/cw_tron.dart @@ -138,4 +138,10 @@ class CWTron extends Tron { List getDefaultTokenContractAddresses() { return DefaultTronTokens().initialTronTokens.map((e) => e.contractAddress).toList(); } + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final tronWallet = wallet as TronWallet; + return tronWallet.tronTokenCurrencies.any((element) => element.contractAddress == contractAddress); + } } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index f16d2aa95..fdf4d75a5 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -126,6 +126,31 @@ abstract class HomeSettingsViewModelBase with Store { } } + @action + bool checkIfTokenIsAlreadyAdded(String contractAddress) { + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + return ethereum!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); + } + + if (_balanceViewModel.wallet.type == WalletType.polygon) { + return polygon!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); + } + + if (_balanceViewModel.wallet.type == WalletType.solana) { + return solana!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); + } + + if (_balanceViewModel.wallet.type == WalletType.tron) { + return tron!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); + } + + if (_balanceViewModel.wallet.type == WalletType.zano) { + return zano!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); + } + + return false; + } + @action Future deleteToken(CryptoCurrency token) async { try { @@ -297,7 +322,7 @@ abstract class HomeSettingsViewModelBase with Store { required bool isEthereum, }) async { final uri = Uri.https( - "api.etherscan.io", + "api.etherscan.io", "/v2/api", { "chainid": isEthereum ? "1" : "137", diff --git a/lib/zano/cw_zano.dart b/lib/zano/cw_zano.dart index 7bd515e32..ede37eabc 100644 --- a/lib/zano/cw_zano.dart +++ b/lib/zano/cw_zano.dart @@ -136,4 +136,10 @@ class CWZano extends Zano { Map> debugCallLength() { return api.debugCallLength(); } + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final zanoWallet = wallet as ZanoWallet; + return zanoWallet.zanoAssets.values.any((element) => element?.assetId == contractAddress); + } } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index a5b287384..0a48d4da5 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -913,6 +913,7 @@ "tip": "بقشيش:", "to": "ل", "today": "اليوم", + "token_already_exists": "رمز موجود بالفعل", "token_contract_address": "عنوان عقد الرمز", "token_decimal": "رمز عشري", "token_name": "اسم الرمز ، على سبيل المثال: Tether", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 805fd9273..c083b8297 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -913,6 +913,7 @@ "tip": "Tip:", "to": "Да", "today": "Днес", + "token_already_exists": "Токена вече съществува", "token_contract_address": "Адрес на токен договор", "token_decimal": "Токен десетичен", "token_name": "Име на токена, напр.: Tether", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 658bcdf6e..49becb211 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -913,6 +913,7 @@ "tip": "Spropitné:", "to": "Na", "today": "Dnes", + "token_already_exists": "Token již existuje", "token_contract_address": "Adresa tokenové smlouvy", "token_decimal": "Token v desítkové soustavě", "token_name": "Název tokenu např.: Tether", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index b35f79cd3..4f5c3f87e 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -914,6 +914,7 @@ "tip": "Hinweis:", "to": "Zu", "today": "Heute", + "token_already_exists": "Token existiert bereits", "token_contract_address": "Token-Contract-Adresse", "token_decimal": "Token-Dezimalzahl", "token_name": "Token-Name, z. B.: Tether", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index b6e655674..3de612694 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -914,6 +914,7 @@ "tip": "Tip:", "to": "To", "today": "Today", + "token_already_exists": "Token already exists", "token_contract_address": "Token contract address", "token_decimal": "Token decimal", "token_name": "Token name eg: Tether", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 6a75f6e62..d19ec108d 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -914,6 +914,7 @@ "tip": "Consejo:", "to": "A", "today": "Hoy", + "token_already_exists": "Token ya existe", "token_contract_address": "Dirección de contrato de token", "token_decimal": "Token decimal", "token_name": "Nombre del token, por ejemplo: Tether", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index d2f8b4e6b..9056dbc49 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -913,6 +913,7 @@ "tip": "Pourboire:", "to": "À", "today": "Aujourd'hui", + "token_already_exists": "Le jeton existe déjà", "token_contract_address": "Adresse du contrat de token", "token_decimal": "Décimales de token", "token_name": "Nom du token, par exemple : Tether", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 6b73db8ed..b5f77a1c0 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -915,6 +915,7 @@ "tip": "Tukwici:", "to": "Zuwa", "today": "Yau", + "token_already_exists": "Alama an riga an wanzu", "token_contract_address": "Adireshin kwangilar Token", "token_decimal": "Alamar ƙima", "token_name": "Alamar sunan misali: Tether", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f97b0f84f..79d25a1fb 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -915,6 +915,7 @@ "tip": "टिप:", "to": "को", "today": "आज", + "token_already_exists": "टोकन पहले से मौजूद है", "token_contract_address": "टोकन अनुबंध पता", "token_decimal": "सांकेतिक दशमलव", "token_name": "टोकन नाम जैसे: टीथर", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index ef9e04bbe..6f94329ff 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -913,6 +913,7 @@ "tip": "Savjet:", "to": "Do", "today": "Danas", + "token_already_exists": "Token već postoji", "token_contract_address": "Adresa ugovora tokena", "token_decimal": "Token decimalni", "token_name": "Naziv tokena npr.: Tether", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index ac1bfda99..c54d6b5b8 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -911,6 +911,7 @@ "tip": "Թեյավճար", "to": "Դեպի", "today": "Այսօր", + "token_already_exists": "Նշանն արդեն գոյություն ունի", "token_contract_address": "Token-ի պայմանագրի հասցե", "token_decimal": "Token-ի տասանիշ", "token_name": "Token-ի անուն, օրինակ՝ Tether", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index e693c98e5..7682903fc 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -916,6 +916,7 @@ "tip": "Tip:", "to": "Ke", "today": "Hari ini", + "token_already_exists": "Token sudah ada", "token_contract_address": "Alamat kontrak token", "token_decimal": "Desimal token", "token_name": "Nama token misalnya: Tether", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index b92969550..9d03f7947 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -914,6 +914,7 @@ "tip": "Suggerimento:", "to": "A", "today": "Oggi", + "token_already_exists": "Il token esiste già", "token_contract_address": "Indirizzo del contratto token", "token_decimal": "Decimale del token", "token_name": "Nome del token, ad esempio: Tether", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 9a0b9fc23..c92a6dab1 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -914,6 +914,7 @@ "tip": "ヒント: ", "to": "に", "today": "今日", + "token_already_exists": "トークンはすでに存在します", "token_contract_address": "トークンコントラクトアドレス", "token_decimal": "トークン10進数", "token_name": "トークン名 例: Tether", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index b70d30328..0184673d7 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -914,6 +914,7 @@ "tip": "팁:", "to": "받는 통화", "today": "오늘", + "token_already_exists": "토큰이 이미 존재합니다", "token_contract_address": "토큰 계약 주소", "token_decimal": "토큰 소수 자릿수", "token_name": "토큰 이름 (예: Tether)", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index f17bb7527..9b158be76 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -913,6 +913,7 @@ "tip": "အကြံပြုချက်-", "to": "သို့", "today": "ဒီနေ့", + "token_already_exists": "တိုကင်ရှိပြီးသား", "token_contract_address": "တိုကင်စာချုပ်လိပ်စာ", "token_decimal": "တိုကင်ဒဿမ", "token_name": "တိုကင်အမည် ဥပမာ- Tether", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index ebde13a68..948798b0a 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -913,6 +913,7 @@ "tip": "Tip:", "to": "Naar", "today": "Vandaag", + "token_already_exists": "Token bestaat al", "token_contract_address": "Token contractadres", "token_decimal": "Token decimaal", "token_name": "Tokennaam bijv.: Tether", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index d83ef2f3e..4b39a949b 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -913,6 +913,7 @@ "tip": "tip:", "to": "Do", "today": "Dzisiaj", + "token_already_exists": "Token już istnieje", "token_contract_address": "Adres kontraktu tokena", "token_decimal": "Token dziesiętny", "token_name": "Nazwa tokena, np.: Tether", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 020f37a78..c2f0f209c 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -915,6 +915,7 @@ "tip": "Dica:", "to": "Para", "today": "Hoje", + "token_already_exists": "Token já existe", "token_contract_address": "Endereço do contrato de token", "token_decimal": "Token decimal", "token_name": "Nome do token, por exemplo: Tether", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 003829450..1b59e84ab 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -914,6 +914,7 @@ "tip": "Совет:", "to": "К", "today": "Сегодня", + "token_already_exists": "Токен уже существует", "token_contract_address": "Адрес контракта токена", "token_decimal": "Десятичный токен", "token_name": "Имя токена, например: Tether", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 6a9c59f57..1fc8d81d5 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -913,6 +913,7 @@ "tip": "เพิ่มค่าตอบแทน:", "to": "ถึง", "today": "วันนี้", + "token_already_exists": "โทเค็นมีอยู่แล้ว", "token_contract_address": "ที่อยู่สัญญาโทเค็น", "token_decimal": "โทเค็นทศนิยม", "token_name": "ชื่อโทเค็น เช่น Tether", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 2f494adce..2ca3b02e4 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -913,6 +913,7 @@ "tip": "Tip:", "to": "Sa", "today": "Ngayon", + "token_already_exists": "Mayroon nang token", "token_contract_address": "Address ng token contract", "token_decimal": "Token decimal", "token_name": "Pangalan ng token, halimbawa: Tether", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 0b67edbe8..c5582b4ef 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -913,6 +913,7 @@ "tip": "Bahşiş:", "to": "İle", "today": "Bugün", + "token_already_exists": "Token zaten var", "token_contract_address": "Token sözleşme adresi", "token_decimal": "Belirteç ondalık", "token_name": "Belirteç adı, örneğin: Tether", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index a4078a341..cdfc91433 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -914,6 +914,7 @@ "tip": "Порада:", "to": "До", "today": "Сьогодні", + "token_already_exists": "Маркер вже існує", "token_contract_address": "Адреса договору маркера", "token_decimal": "Токен десятковий", "token_name": "Назва токена, наприклад: Tether", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 3c805fe6b..42a2398c2 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -915,6 +915,7 @@ "tip": "ٹپ:", "to": "to", "today": "آج", + "token_already_exists": "ٹوکن پہلے ہی موجود ہے", "token_contract_address": "ٹوکن کنٹریکٹ ایڈریس", "token_decimal": "ٹوکن اعشاریہ", "token_name": "ٹوکن کا نام جیسے: Tether", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index ea3281b2a..5f76e666a 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -910,6 +910,7 @@ "tip": "Mẹo:", "to": "ĐẾN", "today": "Hôm nay", + "token_already_exists": "Mã thông báo đã tồn tại", "token_contract_address": "Địa chỉ hợp đồng token", "token_decimal": "Số thập phân của token", "token_name": "Tên token ví dụ: Tether", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 109dac40f..360fcd6b6 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -914,6 +914,7 @@ "tip": "Owó àfikún:", "to": "Si", "today": "Lénìí", + "token_already_exists": "Token tẹlẹ wa", "token_contract_address": "Àmi guide adirẹsi", "token_decimal": "Àmi eleemewa", "token_name": "Orukọ àmi fun apẹẹrẹ: Tether", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 8f146e0fb..3bf18677c 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -913,6 +913,7 @@ "tip": "提示:", "to": "到", "today": "今天", + "token_already_exists": "令牌已经存在", "token_contract_address": "代币合约地址", "token_decimal": "令牌十进制", "token_name": "代币名称例如:Tether", diff --git a/tool/configure.dart b/tool/configure.dart index 95c8a4d5a..f6e1496de 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -760,6 +760,7 @@ abstract class Ethereum { void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); List getDefaultTokenContractAddresses(); + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); } """; @@ -870,6 +871,7 @@ abstract class Polygon { void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection); Future> getHardwareWalletAccounts(LedgerViewModel ledgerVM, {int index = 0, int limit = 5}); List getDefaultTokenContractAddresses(); + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); } """; @@ -1156,6 +1158,7 @@ abstract class Solana { List? getValidationLength(CryptoCurrency type); double? getEstimateFees(WalletBase wallet); List getDefaultTokenContractAddresses(); + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); } """; @@ -1234,6 +1237,7 @@ abstract class Tron { void updateTronGridUsageState(WalletBase wallet, bool isEnabled); List getDefaultTokenContractAddresses(); + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); } """; @@ -1308,6 +1312,7 @@ abstract class Zano { String getAddress(WalletBase wallet); bool validateAddress(String address); Map> debugCallLength(); + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); } """; const zanoEmptyDefinition = 'Zano? zano;\n'; From 65bb917bfb805ef5066055bb7a057a21a6105e8d Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Tue, 24 Jun 2025 03:48:27 +0100 Subject: [PATCH 21/29] CW-1094-WalletConnect-Issues (#2318) * feat(walletconnect): Minor update to WalletConnect tile UI to fix expanded image issue * feat(walletconnect): Minor update to WalletConnect tile UI to fix expanded image issue * feat(walletconnect): Enhance WalletConnect EVM chain service. This change: - Improves signTypedDataV4 method handing and data parsing in extractPermitData. - Adjusts UI for One Click Auth requests * feat(walletconnect): Add redirect to PairingMetadata in WalletKit setup * fix(walletconnect): Ensure session null checks before handling redirects in EvmChainService * fix(walletconnect): Add null safety checks for permitData properties in EvmChainService * refactor(walletconnect): Update WCPairingItemWidget layout and improve error handling for image loading * fix(walletconnect): Handle break in connection flow triggered by global exception handler when SVGParser fails on invalid SvgData and triggers FlutterError. * refactor(solana): Remove redundant session request responses and simplify error handling in SolanaChainService --------- Co-authored-by: Omar Hatem --- .../chain_service/eth/evm_chain_service.dart | 123 ++++++++++-------- .../solana/solana_chain_service.dart | 26 +--- .../services/walletkit_service.dart | 1 + .../widgets/wc_connection_item_widget.dart | 2 +- .../widgets/wc_pairing_item_widget.dart | 40 +++--- .../wc_session_auth_request_widget.dart | 10 +- lib/src/widgets/cake_image_widget.dart | 4 +- lib/src/widgets/provider_optoin_tile.dart | 13 ++ lib/utils/exception_handler.dart | 6 + lib/utils/image_utill.dart | 13 ++ 10 files changed, 141 insertions(+), 97 deletions(-) diff --git a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart index 426c1580f..e70930b48 100644 --- a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart +++ b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart @@ -379,16 +379,21 @@ class EvmChainServiceImpl { topic: topic, response: response, ); + + if (session == null) return; + MethodsUtils.handleRedirect( topic, - session!.peer.metadata.redirect, + session.peer.metadata.redirect, response.error?.message, response.error == null, ); } on ReownSignError catch (error) { + if (session == null) return; + MethodsUtils.handleRedirect( topic, - session!.peer.metadata.redirect, + session.peer.metadata.redirect, error.message, ); } @@ -489,68 +494,76 @@ class EvmChainServiceImpl { final typedData = jsonDecode(data[1] as String) as Map; // Extracting domain details. - final domain = typedData['domain'] ?? {} as Map; + final domain = typedData['domain'] as Map? ?? {}; final domainName = domain['name']?.toString() ?? ''; + final version = domain['version']?.toString() ?? ''; + final chainId = domain['chainId']?.toString() ?? ''; final verifyingContract = domain['verifyingContract']?.toString() ?? ''; - final chainId = domain['chainId']?.toString() ?? ''; - final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type); - - // Get the primary type. + // Get the primary type and types final primaryType = typedData['primaryType']?.toString() ?? ''; + final types = typedData['types'] as Map? ?? {}; + final message = typedData['message'] as Map? ?? {}; - // Extracting message details. - final message = typedData['message'] ?? {} as Map; - final details = message['details'] ?? {} as Map; - final amount = details['amount']?.toString() ?? ''; - final expirationRaw = details['expiration']?.toString() ?? ''; - final nonce = details['nonce']?.toString() ?? ''; + // Build a readable message based on the primary type and its structure + String messageDetails = ''; - final tokenAddress = details['token']?.toString() ?? ''; - final token = await getTokenDetails(tokenAddress, chainName); - - final spender = message['spender']?.toString() ?? ''; - final sigDeadlineRaw = message['sigDeadline']?.toString() ?? ''; - - // Converting expiration and sigDeadline from Unix time (seconds) to DateTime. - DateTime? expirationDate; - DateTime? sigDeadlineDate; - try { - if (expirationRaw.isNotEmpty) { - final int expirationInt = int.parse(expirationRaw); - expirationDate = DateTime.fromMillisecondsSinceEpoch(expirationInt * 1000); - } - if (sigDeadlineRaw.isNotEmpty) { - final int sigDeadlineInt = int.parse(sigDeadlineRaw); - sigDeadlineDate = DateTime.fromMillisecondsSinceEpoch(sigDeadlineInt * 1000); - } - } catch (e) { - // Parsing failed; we leave dates as null. + if (types.containsKey(primaryType)) { + final typeFields = types[primaryType] as List; + messageDetails = _formatMessageFields(message, typeFields, types); + } else { + // For unknown types, show the raw message + messageDetails = message.toString(); } - final permitData = { - 'domainName': domainName, - 'chainId': chainId, - 'verifyingContract': verifyingContract, - 'primaryType': primaryType, - 'token': token, - 'amount': amount, - 'expiration': expirationDate, - 'nonce': nonce, - 'spender': spender, - 'sigDeadline': sigDeadlineDate, - }; - - return 'Domain: ${permitData['domainName']}' - 'Chain ID: ${permitData['chainId']}' - 'Verifying Contract: ${permitData['verifyingContract']}' - 'Primary Type: ${permitData['primaryType']}' - 'Token: ${permitData['token']}' - 'Expiration: ${permitData['expiration'] != null ? permitData['expiration'] : 'N/A'}' - 'Spender: ${permitData['spender']}' - 'Signature Deadline: ${permitData['sigDeadline'] != null ? permitData['sigDeadline'] : 'N/A'}'; + return '''Domain Name: $domainName +Version: $version +Chain ID: $chainId +Verifying Contract: $verifyingContract +Primary Type: $primaryType\n +Message: +$messageDetails'''; } - return ''; + return 'Invalid typed data format'; + } + + String _formatMessageFields( + Map message, List fields, Map types) { + final buffer = StringBuffer(); + + for (var field in fields) { + final fieldName = _toCamelCase(field['name'] as String); + final fieldType = field['type'] as String; + final value = message[field['name'] as String]; + + if (value == null) continue; + + if (types.containsKey(fieldType)) { + // Handle nested types + final nestedFields = types[fieldType] as List; + if (fieldType == 'Person') { + // Special formatting for Person type + final name = value['name'] as String; + final wallet = value['wallet'] as String; + buffer.writeln('$fieldName: $name ($wallet)'); + } else { + // For other nested types, format each field + final formattedValue = + _formatMessageFields(value as Map, nestedFields, types); + buffer.writeln('$fieldName: $formattedValue'); + } + } else { + // Handle primitive types + buffer.writeln('$fieldName: $value'); + } + } + + return buffer.toString(); + } + + String _toCamelCase(String input) { + if (input.isEmpty) return input; + return input[0].toUpperCase() + input.substring(1).toLowerCase(); } Future getTokenDetails(String contractAddress, String chainName) async { diff --git a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart index 076c63228..f7a57b51d 100644 --- a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart +++ b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:blockchain_utils/base58/base58.dart'; import 'package:blockchain_utils/blockchain_utils.dart' as blockchain_utils; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart'; import 'package:flutter/material.dart'; import 'package:on_chain/solana/solana.dart'; @@ -91,8 +90,6 @@ class SolanaChainService { ); } - await walletKit.respondSessionRequest(topic: topic, response: response); - _handleResponseForTopic(topic, response); } @@ -158,8 +155,6 @@ class SolanaChainService { ); } - await walletKit.respondSessionRequest(topic: topic, response: response); - _handleResponseForTopic(topic, response); } @@ -221,8 +216,6 @@ class SolanaChainService { ); } - await walletKit.respondSessionRequest(topic: topic, response: response); - _handleResponseForTopic(topic, response); } @@ -242,21 +235,14 @@ class SolanaChainService { topic, session!.peer.metadata.redirect, response.error?.message, + response.error == null, ); } on ReownSignError catch (error) { - if (error.message.contains('No matching key')) { - MethodsUtils.handleRedirect( - topic, - session!.peer.metadata.redirect, - '${S.current.error_while_processing} ${S.current.youCanGoBackToYourDapp}', - ); - } else { - MethodsUtils.handleRedirect( - topic, - session!.peer.metadata.redirect, - error.message, - ); - } + MethodsUtils.handleRedirect( + topic, + session!.peer.metadata.redirect, + error.message, + ); } } } diff --git a/lib/src/screens/wallet_connect/services/walletkit_service.dart b/lib/src/screens/wallet_connect/services/walletkit_service.dart index 9b48cd233..4c25032a4 100644 --- a/lib/src/screens/wallet_connect/services/walletkit_service.dart +++ b/lib/src/screens/wallet_connect/services/walletkit_service.dart @@ -78,6 +78,7 @@ abstract class WalletKitServiceBase with Store { description: 'Cake Wallet', url: 'https://cakewallet.com', icons: ['https://cakewallet.com/assets/image/cake_logo.png'], + redirect: Redirect(native: 'cakewallet://'), ), ); diff --git a/lib/src/screens/wallet_connect/widgets/wc_connection_item_widget.dart b/lib/src/screens/wallet_connect/widgets/wc_connection_item_widget.dart index d97cc96b6..b701bdc62 100644 --- a/lib/src/screens/wallet_connect/widgets/wc_connection_item_widget.dart +++ b/lib/src/screens/wallet_connect/widgets/wc_connection_item_widget.dart @@ -85,7 +85,7 @@ class _ModelElementWidget extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontWeight: FontWeight.w600, ), - maxLines: 10, + maxLines: 50, overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/src/screens/wallet_connect/widgets/wc_pairing_item_widget.dart b/lib/src/screens/wallet_connect/widgets/wc_pairing_item_widget.dart index 93e2e32f0..3d0ce8dc0 100644 --- a/lib/src/screens/wallet_connect/widgets/wc_pairing_item_widget.dart +++ b/lib/src/screens/wallet_connect/widgets/wc_pairing_item_widget.dart @@ -26,10 +26,18 @@ class WCPairingItemWidget extends StatelessWidget { '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}'; return ListTile( - leading: CakeImageWidget( - imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0] : null, - errorWidget: CircleAvatar( - backgroundImage: AssetImage('assets/images/walletconnect_logo.png'), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: SizedBox( + width: 60, + height: 60, + child: CakeImageWidget( + borderRadius: 8, + width: 60, + height: 60, + imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0] : null, + errorWidget: CircleAvatar( + backgroundImage: AssetImage('assets/images/walletconnect_logo.png'), + ), ), ), title: Text( @@ -59,18 +67,20 @@ class WCPairingItemWidget extends StatelessWidget { ), ], ), - trailing: Container( - height: 40, + trailing: SizedBox( width: 44, - padding: EdgeInsets.all(10), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.primary, - ), - child: Icon( - Icons.edit, - size: 14, - color: Theme.of(context).colorScheme.onPrimary, + height: 40, + child: Container( + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary, + ), + child: Icon( + Icons.edit, + size: 14, + color: Theme.of(context).colorScheme.onPrimary, + ), ), ), onTap: onTap, diff --git a/lib/src/screens/wallet_connect/widgets/wc_session_auth_request_widget.dart b/lib/src/screens/wallet_connect/widgets/wc_session_auth_request_widget.dart index ad4c6b8e1..7e10c0469 100644 --- a/lib/src/screens/wallet_connect/widgets/wc_session_auth_request_widget.dart +++ b/lib/src/screens/wallet_connect/widgets/wc_session_auth_request_widget.dart @@ -17,7 +17,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget { child: SingleChildScrollView(child: child), ), const SizedBox(height: 16), - Row( + Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ PrimaryButton( @@ -30,7 +30,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget { color: Theme.of(context).colorScheme.error, textColor: Theme.of(context).colorScheme.onError, ), - const SizedBox(width: 8), + const SizedBox(height: 8), PrimaryButton( onPressed: () { if (Navigator.canPop(context)) { @@ -41,7 +41,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget { color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, ), - const SizedBox(width: 8), + const SizedBox(height: 8), PrimaryButton( onPressed: () { if (Navigator.canPop(context)) { @@ -49,8 +49,8 @@ class WCSessionAuthRequestWidget extends StatelessWidget { } }, text: S.current.sign_all, - color: Theme.of(context).secondaryHeaderColor, - textColor: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.secondaryContainer, + textColor: Theme.of(context).colorScheme.onSecondaryContainer, ), ], ), diff --git a/lib/src/widgets/cake_image_widget.dart b/lib/src/widgets/cake_image_widget.dart index b24c7f3d3..04d298e2e 100644 --- a/lib/src/widgets/cake_image_widget.dart +++ b/lib/src/widgets/cake_image_widget.dart @@ -11,6 +11,7 @@ class CakeImageWidget extends StatelessWidget { this.loadingWidget, this.errorWidget, this.color, + this.borderRadius = 24.0, }); final String? imageUrl; @@ -20,6 +21,7 @@ class CakeImageWidget extends StatelessWidget { final Widget? loadingWidget; final Widget? errorWidget; final Color? color; + final double borderRadius; @override Widget build(BuildContext context) { @@ -80,7 +82,7 @@ class CakeImageWidget extends StatelessWidget { height: height, width: width, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), + borderRadius: BorderRadius.circular(borderRadius), color: Theme.of(context).colorScheme.surfaceContainerHighest, ), child: Center( diff --git a/lib/src/widgets/provider_optoin_tile.dart b/lib/src/widgets/provider_optoin_tile.dart index 337e50c9e..be6873975 100644 --- a/lib/src/widgets/provider_optoin_tile.dart +++ b/lib/src/widgets/provider_optoin_tile.dart @@ -289,6 +289,15 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo child: CircularProgressIndicator(), ), ), + errorBuilder: (_, __, ___) { + return Container( + height: imageHeight, + width: imageWidth, + child: Center( + child: Icon(Icons.error_outline, color: Colors.grey), + ), + ); + }, ) : Image.network( imagePath, @@ -315,6 +324,9 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo return Container( height: imageHeight, width: imageWidth, + child: Center( + child: Icon(Icons.error_outline, color: Colors.grey), + ), ); }, ); @@ -325,6 +337,7 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo height: imageHeight, width: imageWidth, colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null, + errorBuilder: (_, __, ___) => Icon(Icons.error, color: Colors.grey), ) : Image.asset(imagePath, height: imageHeight, width: imageWidth); } diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index a6d3774ce..0f80fa737 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -274,6 +274,12 @@ class ExceptionHandler { "NetworkImage._loadAsync", "SSLV3_ALERT_BAD_RECORD_MAC", "PlatformException(already_active, File picker is already active", + // SVG-related errors + "SvgParser", + "SVG parsing error", + "Invalid SVG", + "SVG format error", + "SvgPicture", // Temporary ignored, More context: Flutter secure storage reads the values as null some times // probably when the device was locked and then opened on Cake // this is solved by a restart of the app diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart index 994a3957b..40dde7020 100644 --- a/lib/utils/image_utill.dart +++ b/lib/utils/image_utill.dart @@ -26,6 +26,15 @@ class ImageUtil { child: CircularProgressIndicator(), ), ), + errorBuilder: (_, __, ___) { + return Container( + height: _height, + width: _width, + child: Center( + child: Icon(Icons.error_outline, color: Colors.grey), + ), + ); + }, ) : Image.network( key: ValueKey(imagePath), @@ -54,6 +63,9 @@ class ImageUtil { return Container( height: _height, width: _width, + child: Center( + child: Icon(Icons.error_outline, color: Colors.grey), + ), ); }, ); @@ -64,6 +76,7 @@ class ImageUtil { height: _height, width: _width, placeholderBuilder: (_) => Icon(Icons.error), + errorBuilder: (_, __, ___) => Icon(Icons.error), key: ValueKey(imagePath), ) : Image.asset( From 04c86c567f8fd9cad71f9f8277db5bbc4c948872 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 26 Jun 2025 15:43:16 +0200 Subject: [PATCH 22/29] fix: use cake wallet frontend code, remove unused imports (#2337) --- cw_ethereum/lib/deuro/deuro_savings.dart | 40 ++++++++---------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/cw_ethereum/lib/deuro/deuro_savings.dart b/cw_ethereum/lib/deuro/deuro_savings.dart index 4c37f944c..e5faa606a 100644 --- a/cw_ethereum/lib/deuro/deuro_savings.dart +++ b/cw_ethereum/lib/deuro/deuro_savings.dart @@ -1,19 +1,16 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:crypto/crypto.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_ethereum/deuro/deuro_savings_contract.dart'; import 'package:cw_ethereum/ethereum_wallet.dart'; import 'package:cw_evm/contract/erc20.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/pending_evm_chain_transaction.dart'; +import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; -const String savingsGatewayAddress = - "0x073493d73258C4BEb6542e8dd3e1b2891C972303"; +const String savingsGatewayAddress = "0x073493d73258C4BEb6542e8dd3e1b2891C972303"; const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea"; +const String frontendCode = "0x00000000000000000000000000000000000000000043616b652057616c6c6574"; class DEuro { final SavingsGateway _savingsGateway; @@ -35,27 +32,21 @@ class DEuro { client: client, ); - final frontendCode = - Uint8List.fromList(sha256.convert(utf8.encode("wallet")).bytes); - - EthereumAddress get _address => - EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress); + EthereumAddress get _address => EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress); Future get savingsBalance async => (await _savingsGateway.savings(accountOwner: _address)).saved; - Future get accruedInterest => - _savingsGateway.accruedInterest(accountOwner: _address); + Future get accruedInterest => _savingsGateway.accruedInterest(accountOwner: _address); Future get interestRate => _savingsGateway.currentRatePPM(); - Future get approvedBalance => - _dEuro.allowance(_address, _savingsGateway.self.address); + Future get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address); Future depositSavings( BigInt amount, EVMChainTransactionPriority priority) async { final signedTransaction = await _savingsGateway.save( - (amount: amount, frontendCode: frontendCode), + (amount: amount, frontendCode: hexToBytes(frontendCode)), credentials: _wallet.evmChainPrivateKey, ); @@ -64,12 +55,10 @@ class DEuro { contractAddress: _savingsGateway.self.address.hexEip55, receivingAddressHex: _savingsGateway.self.address.hexEip55, priority: priority, - data: _savingsGateway.self.abi.functions[17] - .encodeCall([amount, frontendCode]), + data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), ); - final sendTransaction = - () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); return PendingEVMChainTransaction( sendTransaction: sendTransaction, @@ -82,7 +71,7 @@ class DEuro { Future withdrawSavings( BigInt amount, EVMChainTransactionPriority priority) async { final signedTransaction = await _savingsGateway.withdraw( - (target: _address, amount: amount, frontendCode: frontendCode), + (target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)), credentials: _wallet.evmChainPrivateKey, ); @@ -91,12 +80,10 @@ class DEuro { contractAddress: _savingsGateway.self.address.hexEip55, receivingAddressHex: _savingsGateway.self.address.hexEip55, priority: priority, - data: _savingsGateway.self.abi.functions[17] - .encodeCall([amount, frontendCode]), + data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), ); - final sendTransaction = - () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); return PendingEVMChainTransaction( sendTransaction: sendTransaction, @@ -107,8 +94,7 @@ class DEuro { } // Set an infinite approval to save gas in the future - Future enableSavings( - EVMChainTransactionPriority priority) async => + Future enableSavings(EVMChainTransactionPriority priority) async => (await _wallet.createApprovalTransaction( BigInt.parse( 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', From 1a5601f755847b7b364ce1d884ef09238bdbebea Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 26 Jun 2025 19:02:05 +0300 Subject: [PATCH 23/29] bugfix-unspent-coins-back-button (#2335) * Update unspent_coins_list_page.dart * minor fix * close the page after the update is done --- .../unspent_coins/unspent_coins_list_page.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index 965926d63..3c47ea203 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -41,14 +41,18 @@ class UnspentCoinsListPage extends BasePage { final UnspentCoinsListViewModel unspentCoinsListViewModel; Future handleOnPopInvoked(BuildContext context) async { + final navigator = Navigator.of(context); final hasChanged = unspentCoinsListViewModel.hasAdjustableFieldChanged; if (unspentCoinsListViewModel.items.isEmpty || !hasChanged) { - Navigator.of(context).pop(); - } else { - unspentCoinsListViewModel.setIsDisposing(true); - await unspentCoinsListViewModel.dispose(); - Navigator.of(context).pop(); + if (navigator.canPop()) navigator.pop(); + return; } + + unspentCoinsListViewModel.setIsDisposing(true); + await unspentCoinsListViewModel.dispose(); + + if (navigator.canPop()) navigator.pop(); + if (navigator.canPop()) navigator.pop(); } @override From 73588071ba5065a2df37a4d1bf5a041e4e071048 Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 27 Jun 2025 14:05:54 +0200 Subject: [PATCH 24/29] feat(tor): add Tor endpoints for fiat-api and service-api (#2339) --- lib/core/fiat_conversion_service.dart | 3 +-- .../dashboard/dashboard_view_model.dart | 17 +++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 046c2b149..4a33feb63 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -5,8 +5,7 @@ import 'dart:convert'; import 'package:cake_wallet/.secrets.g.dart' as secrets; const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com'; -// const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion'; -const _fiatApiOnionAuthority = _fiatApiClearNetAuthority; +const _fiatApiOnionAuthority = 'kfkyguqtz5vcnbvar5pjgddkaeawbo4j3r4fj3e22k3tzqageplosiid.onion'; const _fiatApiPath = '/v2/rates'; Future _fetchPrice(String crypto, String fiat, bool torOnly) async { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index db359a3f5..eab0c8572 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1123,13 +1123,18 @@ abstract class DashboardViewModelBase with Store { Future _getServicesStatus() async { try { if (isEnabledBulletinAction) { - final uri = Uri.https( - "service-api.cakewallet.com", - "/v1/active-notices", - {'key': secrets.fiatApiKey}, + final res = await ProxyWrapper().get( + clearnetUri: Uri.https( + "service-api.cakewallet.com", + "/v1/active-notices", + {'key': secrets.fiatApiKey}, + ), + onionUri: Uri.http( + "jpirgl4lrwzjgdqj2nsv3g7twhp2efzty5d3cnypktyczzqfc5qcwwyd.onion", + "/v1/active-notices", + {'key': secrets.fiatApiKey}, + ), ); - - final res = await ProxyWrapper().get(clearnetUri: uri); if (res.statusCode < 200 || res.statusCode >= 300) { throw res.body; } From 900304d40532e9226439a38387853519299f670a Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 27 Jun 2025 14:06:56 +0200 Subject: [PATCH 25/29] fix(cw_monero): keys nullcheck handling (#2338) add default values to monero wallet keys functions in order to prevent null check crashes --- cw_monero/lib/api/wallet.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index e98ba71ca..148b271ff 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -116,12 +116,17 @@ String getSeedLegacy(String? language) { } String getPassphrase() { - return currentWallet!.getCacheAttribute(key: "cakewallet.passphrase"); + return currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? ""; } Map>> addressCache = {}; String getAddress({int accountIndex = 0, int addressIndex = 0}) { + // this is a workaround for when we switch the wallet pointer, + // it should never reach UI but should be good enough to prevent gray screen + // or other errors because of forced null check. + if (currentWallet == null) return ""; + // printV("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}"); // this could be a while loop, but I'm in favor of making it if to not cause freezes if (currentWallet!.numSubaddresses(accountIndex: accountIndex)-1 < addressIndex) { @@ -272,13 +277,13 @@ void closeCurrentWallet() { currentWallet!.stop(); } -String getSecretViewKey() => currentWallet!.secretViewKey(); +String getSecretViewKey() => currentWallet?.secretViewKey() ?? ""; -String getPublicViewKey() => currentWallet!.publicViewKey(); +String getPublicViewKey() => currentWallet?.publicViewKey() ?? ""; -String getSecretSpendKey() => currentWallet!.secretSpendKey(); +String getSecretSpendKey() => currentWallet?.secretSpendKey() ?? ""; -String getPublicSpendKey() => currentWallet!.publicSpendKey(); +String getPublicSpendKey() => currentWallet?.publicSpendKey() ?? ""; class SyncListener { SyncListener(this.onNewBlock, this.onNewTransaction) From d0827dd39e660bc30152f67d3bfbf41450ed4ba6 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 27 Jun 2025 15:47:34 +0300 Subject: [PATCH 26/29] add USDT Binance Smart Chain (BSC) support (#2341) --- assets/images/usdtbsc_icon.png | Bin 0 -> 920 bytes cw_core/lib/crypto_currency.dart | 4 +++- .../provider/thorchain_exchange.provider.dart | 3 ++- .../provider/trocador_exchange_provider.dart | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 assets/images/usdtbsc_icon.png diff --git a/assets/images/usdtbsc_icon.png b/assets/images/usdtbsc_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9f2cda2370168aaad92eeec449730458b6df7d2a GIT binary patch literal 920 zcmV;J184k+P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H110_jB zK~y-6t(0GEQ*{)_Kc~Gb_omsDvMSwN0wl_iWjstLi((TJd72L<4Iv>IU?yZ~OAI7N z5oLx!B$>l`sV_t&nTaOyWSm6tWuYO9*xwP8<7#q-7?A8PwMwMYWX*n_pV0Tj%vUdzzXu&4GZ|^&i-})wwl4|KPV& z%Kuj`S6fxG>LxOo;~rGstMotR^DWGEbxp;0?8xREM}C@|6gT$s^tAq6C`flpOYVMG z*L1yQ&99n7+(7j#BA){S@U&@WuFcHce}818>1H~;*))vYrnHVNoz?Bj zTFKQheW-kCEcOi|`+*`rAr#{Dph><2SNj&3ob@m{>)~qOBKZ=W9yC#v(KTDHx;c^9 zhw7m3j|orh_dj^tM~Q@&TfRJOaO#qUDhS&7Sl|lu4ZzuO>dbD06AxLN-gNrzNB;>} uX~E#ePQ`mnL|+C_mEQ%Ye{ODe2;d)xgHl-NOT0?}0000 with Serializable implemen CryptoCurrency.zano, CryptoCurrency.ton, CryptoCurrency.flip, - CryptoCurrency.deuro + CryptoCurrency.deuro, + CryptoCurrency.usdtbsc, ]; static const havenCurrencies = [ @@ -233,6 +234,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/zano_icon.png', decimals: 12); static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18); static const deuro = CryptoCurrency(title: 'DEURO', tag: 'ETH', fullName: 'Decentralized Euro', raw: 98, name: 'deuro', iconPath: 'assets/images/deuro_icon.png', decimals: 18); + static const usdtbsc = CryptoCurrency(title: 'USDT', tag: 'BSC', fullName: 'USDT Binance coin', raw: 99, name: 'usdtbsc', iconPath: 'assets/images/usdtbsc_icon.png', decimals: 18); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart index 0cdb2a423..16246abac 100644 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -23,6 +23,7 @@ class ThorChainExchangeProvider extends ExchangeProvider { // CryptoCurrency.eth, CryptoCurrency.ltc, CryptoCurrency.bch, + CryptoCurrency.usdtbsc, // CryptoCurrency.aave, // CryptoCurrency.dai, // CryptoCurrency.gusd, @@ -259,7 +260,7 @@ class ThorChainExchangeProvider extends ExchangeProvider { } String _normalizeCurrency(CryptoCurrency currency) { - final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.title; + final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.tag ?? currency.title; return '$networkTitle.${currency.title}'; } diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index f3a70b912..cdb0efba8 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -347,6 +347,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { return 'TRC20'; case 'LN': return 'Lightning'; + case 'BSC': + return 'BEP20'; default: return tag.toLowerCase(); } From b3c20a5818df13a8c5e4f2839f14ca473f9aa5c0 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Fri, 27 Jun 2025 15:38:09 +0200 Subject: [PATCH 27/29] Generic fixes for payjoin (#2342) * feat: enhance Payjoin transaction details with block explorer link * feat: enhance Payjoin transaction details with block explorer link * fix: handle connectivity errors in Payjoin session operations --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 4 ++++ .../lib/payjoin/payjoin_send_worker.dart | 4 ++-- .../payjoin_details_view_model.dart | 20 +++++++++++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index b33d722ab..d84d958be 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -67,6 +67,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S payjoinManager.resumeSessions(); } catch (e) { printV(e); + // Ignore Connectivity errors + if (!e.toString().contains("error sending request for url")) rethrow; } } @@ -79,6 +81,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S payjoinManager.spawnReceiver(receiver: currentPayjoinReceiver!); } catch (e) { printV(e); + // Ignore Connectivity errors + if (!e.toString().contains("error sending request for url")) rethrow; } } } diff --git a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart index c11342800..7e85cc773 100644 --- a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart +++ b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart @@ -106,8 +106,8 @@ class PayjoinSenderWorker { sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted}); return await postRequest.$2.processResponse(response: response); - } catch (e) { - throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e'); + } catch (e, stack) { + throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e, $stack'); } } diff --git a/lib/view_model/payjoin_details_view_model.dart b/lib/view_model/payjoin_details_view_model.dart index b19207373..7d63aaae3 100644 --- a/lib/view_model/payjoin_details_view_model.dart +++ b/lib/view_model/payjoin_details_view_model.dart @@ -4,7 +4,9 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; import 'package:cake_wallet/themes/core/theme_store.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; import 'package:cw_core/payjoin_session.dart'; @@ -12,6 +14,7 @@ import 'package:cw_core/transaction_info.dart'; import 'package:flutter/widgets.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:mobx/mobx.dart'; +import 'package:url_launcher/url_launcher.dart'; part 'payjoin_details_view_model.g.dart'; @@ -40,7 +43,7 @@ abstract class PayjoinDetailsViewModelBase with Store { @observable late PayjoinSession payjoinSession; - final ObservableList items; + final ObservableList items; late final StreamSubscription listener; @@ -69,12 +72,25 @@ abstract class PayjoinDetailsViewModelBase with Store { title: S.current.error, value: payjoinSession.error!, ), - if (payjoinSession.txId?.isNotEmpty == true && transactionInfo != null) + if (payjoinSession.txId?.isNotEmpty == true && transactionInfo != null) ...[ StandartListItem( title: S.current.transaction_details_transaction_id, value: payjoinSession.txId!, key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + BlockExplorerListItem( + title: S.current.view_in_block_explorer, + value: '${S.current.view_transaction_on}mempool.space', + onTap: () async { + try { + final uri = Uri.parse('https://mempool.cakewallet.com/tx/${payjoinSession.txId!}'); + if (await canLaunchUrl(uri)) + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (e) {} + }, + key: ValueKey('block_explorer_list_item_wallet_type_key'), ) + ] ]); if (transactionInfo != null) { From 5aeb6b752291ee129084aad59ed99b3c54dad5b2 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:53:46 +0100 Subject: [PATCH 28/29] Deuro Savings Error Handling (#2340) * feat(deuro): Enhance gas fee handling and error management for Deuro Savings Transactions. This change: - Introduces DeuroGasFeeException to handle insufficient ETH for gas fees. - Adds check for ETH balance before savings transactions to prevent failures due to insufficient funds. - Updates savings transaction methods to include error handling. - Adds UI feedback for transaction failures in DEuroSavingsPage. * Fix conflicts * Update cw_ethereum/lib/deuro/deuro_savings.dart Co-authored-by: Konstantin Ullrich --------- Co-authored-by: Omar Hatem Co-authored-by: Konstantin Ullrich --- cw_ethereum/lib/deuro/deuro_savings.dart | 127 +++++++++++++----- cw_evm/lib/evm_chain_exceptions.dart | 29 ++++ .../integrations/deuro/savings_page.dart | 20 +++ .../integrations/deuro_view_model.dart | 69 ++++++---- 4 files changed, 186 insertions(+), 59 deletions(-) diff --git a/cw_ethereum/lib/deuro/deuro_savings.dart b/cw_ethereum/lib/deuro/deuro_savings.dart index e5faa606a..bce05905c 100644 --- a/cw_ethereum/lib/deuro/deuro_savings.dart +++ b/cw_ethereum/lib/deuro/deuro_savings.dart @@ -2,6 +2,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_ethereum/deuro/deuro_savings_contract.dart'; import 'package:cw_ethereum/ethereum_wallet.dart'; import 'package:cw_evm/contract/erc20.dart'; +import 'package:cw_evm/evm_chain_exceptions.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:web3dart/crypto.dart'; @@ -43,59 +44,111 @@ class DEuro { Future get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address); - Future depositSavings( - BigInt amount, EVMChainTransactionPriority priority) async { - final signedTransaction = await _savingsGateway.save( - (amount: amount, frontendCode: hexToBytes(frontendCode)), - credentials: _wallet.evmChainPrivateKey, - ); + Future _checkEthBalanceForGasFees(EVMChainTransactionPriority priority) async { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + final currentBalance = ethBalance.getInWei; - final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( - amount: amount, + final gasFeesModel = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: BigInt.zero, contractAddress: _savingsGateway.self.address.hexEip55, receivingAddressHex: _savingsGateway.self.address.hexEip55, priority: priority, - data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), + data: _savingsGateway.self.abi.functions[17] + .encodeCall([BigInt.zero, hexToBytes(frontendCode)]), ); - final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + final estimatedGasFee = BigInt.from(gasFeesModel.estimatedGasFee); + final requiredBalance = estimatedGasFee; - return PendingEVMChainTransaction( + if (currentBalance < requiredBalance) { + throw DeuroGasFeeException( + requiredGasFee: requiredBalance, + currentBalance: currentBalance, + ); + } + } + + Future depositSavings( + BigInt amount, EVMChainTransactionPriority priority) async { + try { + await _checkEthBalanceForGasFees(priority); + + final signedTransaction = await _savingsGateway.save( + (amount: amount, frontendCode: hexToBytes(frontendCode)), + credentials: _wallet.evmChainPrivateKey, + ); + + final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: amount, + contractAddress: _savingsGateway.self.address.hexEip55, + receivingAddressHex: _savingsGateway.self.address.hexEip55, + priority: priority, + data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), + ); + + final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + + return PendingEVMChainTransaction( sendTransaction: sendTransaction, signedTransaction: signedTransaction, fee: BigInt.from(fee.estimatedGasFee), amount: amount.toString(), - exponent: 18); + exponent: 18, + ); + } catch (e) { + if (e.toString().contains('insufficient funds for gas')) { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + throw DeuroGasFeeException( + currentBalance: ethBalance.getInWei, + ); + } + rethrow; + } } Future withdrawSavings( BigInt amount, EVMChainTransactionPriority priority) async { - final signedTransaction = await _savingsGateway.withdraw( - (target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)), - credentials: _wallet.evmChainPrivateKey, - ); + try { + await _checkEthBalanceForGasFees(priority); - final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( - amount: amount, - contractAddress: _savingsGateway.self.address.hexEip55, - receivingAddressHex: _savingsGateway.self.address.hexEip55, - priority: priority, - data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), - ); + final signedTransaction = await _savingsGateway.withdraw( + (target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)), + credentials: _wallet.evmChainPrivateKey, + ); - final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction( + amount: amount, + contractAddress: _savingsGateway.self.address.hexEip55, + receivingAddressHex: _savingsGateway.self.address.hexEip55, + priority: priority, + data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]), + ); - return PendingEVMChainTransaction( - sendTransaction: sendTransaction, - signedTransaction: signedTransaction, - fee: BigInt.from(fee.estimatedGasFee), - amount: amount.toString(), - exponent: 18); + final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction); + + return PendingEVMChainTransaction( + sendTransaction: sendTransaction, + signedTransaction: signedTransaction, + fee: BigInt.from(fee.estimatedGasFee), + amount: amount.toString(), + exponent: 18); + } catch (e) { + if (e.toString().contains('insufficient funds for gas')) { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + throw DeuroGasFeeException( + currentBalance: ethBalance.getInWei, + ); + } + rethrow; + } } // Set an infinite approval to save gas in the future - Future enableSavings(EVMChainTransactionPriority priority) async => - (await _wallet.createApprovalTransaction( + Future enableSavings(EVMChainTransactionPriority priority) async { + try { + await _checkEthBalanceForGasFees(priority); + + return (await _wallet.createApprovalTransaction( BigInt.parse( 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', radix: 16, @@ -104,4 +157,14 @@ class DEuro { CryptoCurrency.deuro, priority, )) as PendingEVMChainTransaction; + } catch (e) { + if (e.toString().contains('insufficient funds for gas')) { + final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address); + throw DeuroGasFeeException( + currentBalance: ethBalance.getInWei, + ); + } + rethrow; + } + } } diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart index c7509a17f..19bb047ef 100644 --- a/cw_evm/lib/evm_chain_exceptions.dart +++ b/cw_evm/lib/evm_chain_exceptions.dart @@ -22,3 +22,32 @@ class EVMChainTransactionFeesException implements Exception { @override String toString() => exceptionMessage; } + +class DeuroGasFeeException implements Exception { + final String exceptionMessage; + final BigInt? requiredGasFee; + final BigInt? currentBalance; + + DeuroGasFeeException({ + this.requiredGasFee, + this.currentBalance, + }) : exceptionMessage = _buildMessage(requiredGasFee, currentBalance); + + static String _buildMessage(BigInt? requiredGasFee, BigInt? currentBalance) { + const baseMessage = 'Insufficient ETH for gas fees.'; + const addEthMessage = ' Please add ETH to your wallet to cover transaction fees.'; + + if (requiredGasFee != null) { + final requiredEth = (requiredGasFee / BigInt.from(10).pow(18)).toStringAsFixed(8); + final balanceInfo = currentBalance != null + ? ', Available: ${(currentBalance / BigInt.from(10).pow(18)).toStringAsFixed(8)} ETH' + : ''; + return '$baseMessage Required: ~$requiredEth ETH$balanceInfo.$addEthMessage'; + } + + return '$baseMessage$addEthMessage'; + } + + @override + String toString() => exceptionMessage; +} diff --git a/lib/src/screens/integrations/deuro/savings_page.dart b/lib/src/screens/integrations/deuro/savings_page.dart index a58578893..0b0f4941a 100644 --- a/lib/src/screens/integrations/deuro/savings_page.dart +++ b/lib/src/screens/integrations/deuro/savings_page.dart @@ -4,9 +4,11 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/interest_card_widget.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_card_widget.dart'; import 'package:cake_wallet/src/screens/integrations/deuro/widgets/savings_edit_sheet.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -190,6 +192,24 @@ class DEuroSavingsPage extends BasePage { ); }); } + + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!context.mounted) return; + + await showPopUp( + context: context, + builder: (BuildContext popupContext) { + return AlertWithOneAction( + alertTitle: S.of(popupContext).error, + alertContent: state.error, + buttonText: S.of(popupContext).ok, + buttonAction: () => Navigator.of(popupContext).pop(), + ); + }, + ); + }); + } }); _isReactionsSet = true; diff --git a/lib/view_model/integrations/deuro_view_model.dart b/lib/view_model/integrations/deuro_view_model.dart index 09f9a363c..eca3c2376 100644 --- a/lib/view_model/integrations/deuro_view_model.dart +++ b/lib/view_model/integrations/deuro_view_model.dart @@ -46,10 +46,8 @@ abstract class DEuroViewModelBase with Store { @action Future reloadSavingsUserData() async { - final savingsBalanceRaw = - ethereum!.getDEuroSavingsBalance(_appStore.wallet!); - final accruedInterestRaw = - ethereum!.getDEuroAccruedInterest(_appStore.wallet!); + final savingsBalanceRaw = ethereum!.getDEuroSavingsBalance(_appStore.wallet!); + final accruedInterestRaw = ethereum!.getDEuroAccruedInterest(_appStore.wallet!); approvedTokens = await ethereum!.getDEuroSavingsApproved(_appStore.wallet!); @@ -63,56 +61,73 @@ abstract class DEuroViewModelBase with Store { @action Future reloadInterestRate() async { - final interestRateRaw = - await ethereum!.getDEuroInterestRate(_appStore.wallet!); + final interestRateRaw = await ethereum!.getDEuroInterestRate(_appStore.wallet!); interestRate = (interestRateRaw / BigInt.from(10000)).toString(); } @action Future prepareApproval() async { - final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; - approvalTransaction = - await ethereum!.enableDEuroSaving(_appStore.wallet!, priority); + try { + state = TransactionCommitting(); + final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; + approvalTransaction = await ethereum!.enableDEuroSaving(_appStore.wallet!, priority); + state = InitialExecutionState(); + } catch (e) { + state = FailureState(e.toString()); + } } @action Future prepareSavingsEdit(String amountRaw, bool isAdding) async { - final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18)); - final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; - transaction = await (isAdding - ? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority) - : ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority)); + try { + state = TransactionCommitting(); + final amount = BigInt.from(num.parse(amountRaw) * pow(10, 18)); + final priority = _appStore.settingsStore.priority[WalletType.ethereum]!; + transaction = await (isAdding + ? ethereum!.addDEuroSaving(_appStore.wallet!, amount, priority) + : ethereum!.removeDEuroSaving(_appStore.wallet!, amount, priority)); + state = InitialExecutionState(); + } catch (e) { + state = FailureState(e.toString()); + } } - Future prepareCollectInterest() => - prepareSavingsEdit(accruedInterest, false); + Future prepareCollectInterest() => prepareSavingsEdit(accruedInterest, false); @action Future commitTransaction() async { if (transaction != null) { - state = TransactionCommitting(); - await transaction!.commit(); - transaction = null; - reloadSavingsUserData(); - state = TransactionCommitted(); + try { + state = TransactionCommitting(); + await transaction!.commit(); + transaction = null; + reloadSavingsUserData(); + state = TransactionCommitted(); + } catch (e) { + state = FailureState(e.toString()); + } } } @action Future commitApprovalTransaction() async { if (approvalTransaction != null) { - state = TransactionCommitting(); - await approvalTransaction!.commit(); - approvalTransaction = null; - reloadSavingsUserData(); - state = TransactionCommitted(); + try { + state = TransactionCommitting(); + await approvalTransaction!.commit(); + approvalTransaction = null; + reloadSavingsUserData(); + state = TransactionCommitted(); + } catch (e) { + state = FailureState(e.toString()); + } } } @action void dismissTransaction() { - transaction == null; + transaction = null; approvalTransaction = null; state = InitialExecutionState(); } From c2cca1ff37e770e6322ce40e102a45a1b9601300 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 27 Jun 2025 22:20:18 +0300 Subject: [PATCH 29/29] v5.1.0 Release Candidate (#2332) * v5.1.0 Release Candidate * update app versions [skip ci] * separate secrets for Monero.com [skip ci] * fix: amount getting wiped when pasting address * update Trocador Monero api key [skip ci] * move fiat api key to headers [skip ci] * latest Release candidate --- .../workflows/automated_integration_test.yml | 8 ++-- .github/workflows/pr_test_build_android.yml | 8 ++-- .github/workflows/pr_test_build_linux.yml | 8 ++-- assets/text/Monerocom_Release_Notes.txt | 5 ++- assets/text/Release_Notes.txt | 11 +++-- ios/Podfile.lock | 4 +- lib/anonpay/anonpay_api.dart | 3 +- lib/core/fiat_conversion_service.dart | 4 +- .../provider/changenow_exchange_provider.dart | 10 ++--- .../provider/exolix_exchange_provider.dart | 3 +- .../provider/trocador_exchange_provider.dart | 3 +- .../dashboard/pages/cake_features_page.dart | 41 ++++++++++--------- .../exchange/widgets/exchange_card.dart | 3 +- lib/src/widgets/address_text_field.dart | 16 +++++--- scripts/android/app_env.sh | 8 ++-- scripts/ios/app_env.sh | 8 ++-- scripts/linux/app_env.sh | 4 +- scripts/macos/app_env.sh | 8 ++-- scripts/windows/build_exe_installer.iss | 2 +- tool/utils/secret_key.dart | 8 ++-- 20 files changed, 95 insertions(+), 70 deletions(-) diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index a26e3645d..47b08c44d 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -153,8 +153,8 @@ jobs: echo "const shortKey = '${{ secrets.SHORT_KEY }}';" >> lib/.secrets.g.dart echo "const backupSalt = '${{ secrets.BACKUP_SALT }}';" >> lib/.secrets.g.dart echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -168,6 +168,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -178,7 +179,8 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index cdbd7ca37..f7c226ce4 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -98,8 +98,8 @@ jobs: else echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart fi - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -113,6 +113,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -124,7 +125,8 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 119cd7530..f057b19e5 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -91,8 +91,8 @@ jobs: else echo "const backupKeychainSalt = '${{ secrets.BACKUP_KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart fi - echo "const changeNowApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart - echo "const changeNowApiKeyDesktop = '${{ secrets.CHANGE_NOW_API_KEY_DESKTOP }}';" >> lib/.secrets.g.dart + echo "const changeNowCakeWalletApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart + echo "const changeNowMoneroApiKey = '${{ secrets.CHANGE_NOW_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreSecretKey = '${{ secrets.WYRE_SECRET_KEY }}';" >> lib/.secrets.g.dart echo "const wyreApiKey = '${{ secrets.WYRE_API_KEY }}';" >> lib/.secrets.g.dart echo "const wyreAccountId = '${{ secrets.WYRE_ACCOUNT_ID }}';" >> lib/.secrets.g.dart @@ -106,6 +106,7 @@ jobs: echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorMoneroApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart @@ -117,7 +118,8 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixCakeWalletApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart + echo "const exolixMoneroApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const exchangeHelperApiKey = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 1176d3d8c..faf57258a 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,4 @@ -New themes and UI/UX improvements -Ledger flow enhancements +Add built-in Tor support (experimental) +Ledger improvements +UI/UX improvements Bug fixes \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 6afc88fa1..c49b895e3 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,4 +1,9 @@ -New themes and UI/UX improvements -Payjoin enhancements -Ledger flow enhancements +Add built-in Tor support (experimental) +Add dEuro investments +Solana fixes/enhancements +Polygon fixes/enhancements +WalletConnect improvements +Ledger improvements +Payjoin improvements +UI/UX improvements Bug fixes \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 575b43d5f..abbd40673 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -76,7 +76,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - payjoin_flutter (0.20.0) + - payjoin_flutter (0.23.0) - permission_handler_apple (9.3.0): - Flutter - reown_yttrium (0.0.1): @@ -235,7 +235,7 @@ SPEC CHECKSUMS: OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5 + payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 reown_yttrium: c0e87e5965fa60a3559564cc35cffbba22976089 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 diff --git a/lib/anonpay/anonpay_api.dart b/lib/anonpay/anonpay_api.dart index d68807b95..6f401ae3f 100644 --- a/lib/anonpay/anonpay_api.dart +++ b/lib/anonpay/anonpay_api.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/anonpay/anonpay_status_response.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -27,7 +28,7 @@ class AnonPayApi { static const anonPayPath = '/anonpay'; static const anonPayStatus = '/anonpay/status'; static const coinPath = 'api/coin'; - static const apiKey = secrets.trocadorApiKey; + static final apiKey = isMoneroOnly ? secrets.trocadorMoneroApiKey : secrets.trocadorApiKey; Future paymentStatus(String id) async { final response = await ProxyWrapper().get( diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 4a33feb63..669e38128 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -14,7 +14,6 @@ Future _fetchPrice(String crypto, String fiat, bool torOnly) async { 'interval_count': '1', 'base': crypto.split(".").first, 'quote': fiat, - 'key': secrets.fiatApiKey, }; num price = 0.0; @@ -26,6 +25,9 @@ Future _fetchPrice(String crypto, String fiat, bool torOnly) async { final response = await ProxyWrapper().get( onionUri: onionUri, clearnetUri: torOnly ? onionUri : clearnetUri, + headers: { + "x-api-key": secrets.fiatApiKey, + } ); diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart index 2cbd97dd2..dbc4cc3ab 100644 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -11,13 +11,12 @@ import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/distribution_info.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; + class ChangeNowExchangeProvider extends ExchangeProvider { ChangeNowExchangeProvider({required SettingsStore settingsStore}) : _settingsStore = settingsStore, @@ -30,7 +29,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { ]; static final apiKey = - DeviceInfo.instance.isMobile ? secrets.changeNowApiKey : secrets.changeNowApiKeyDesktop; + isMoneroOnly ? secrets.changeNowMoneroApiKey : secrets.changeNowCakeWalletApiKey; static const apiAuthority = 'api.changenow.io'; static const createTradePath = '/v2/exchange'; static const findTradeByIdPath = '/v2/exchange/by-id'; @@ -74,7 +73,6 @@ class ChangeNowExchangeProvider extends ExchangeProvider { }; final uri = Uri.https(apiAuthority, rangePath, params); final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); - if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -120,7 +118,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final uri = Uri.https(apiAuthority, estimatedAmountPath, params); final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); - + final responseJSON = json.decode(response.body) as Map; final fromAmount = double.parse(responseJSON['fromAmount'].toString()); final toAmount = double.parse(responseJSON['toAmount'].toString()); @@ -184,7 +182,6 @@ class ChangeNowExchangeProvider extends ExchangeProvider { headers: headers, body: json.encode(body), ); - if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; @@ -228,7 +225,6 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final params = {'id': id}; final uri = Uri.https(apiAuthority, findTradeByIdPath, params); final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); - if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index 2d5011fe0..0b7491bf5 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -16,7 +17,7 @@ import 'package:cw_core/utils/print_verbose.dart'; class ExolixExchangeProvider extends ExchangeProvider { ExolixExchangeProvider() : super(pairList: supportedPairs(_notSupported)); - static final apiKey = secrets.exolixApiKey; + static final apiKey = isMoneroOnly ? secrets.exolixMoneroApiKey : secrets.exolixCakeWalletApiKey; static const apiBaseUrl = 'exolix.com'; static const transactionsPath = '/api/v2/transactions'; static const ratePath = '/api/v2/rate'; diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index cdb0efba8..cc8d8fa60 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -51,7 +52,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { CryptoCurrency.zaddr, ]; - static const apiKey = secrets.trocadorApiKey; + static final apiKey = isMoneroOnly ? secrets.trocadorMoneroApiKey : secrets.trocadorApiKey; static const clearNetAuthority = 'api.trocador.app'; static const onionApiAuthority = clearNetAuthority; // static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index ba19f5329..29066bc75 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -10,11 +10,11 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:url_launcher/url_launcher.dart'; class CakeFeaturesPage extends StatelessWidget { - CakeFeaturesPage( - {required this.dashboardViewModel, required this.cakeFeaturesViewModel}); + CakeFeaturesPage({required this.dashboardViewModel, required this.cakeFeaturesViewModel}); final DashboardViewModel dashboardViewModel; final CakeFeaturesViewModel cakeFeaturesViewModel; @@ -59,23 +59,26 @@ class CakeFeaturesPage extends StatelessWidget { fit: BoxFit.cover, ), ), - if (dashboardViewModel.type == WalletType.ethereum) ...[ - DashBoardRoundedCardWidget( - isDarkTheme: dashboardViewModel.isDarkTheme, - shadowBlur: dashboardViewModel.getShadowBlur(), - shadowSpread: dashboardViewModel.getShadowSpread(), - onTap: () => - Navigator.of(context).pushNamed(Routes.dEuroSavings), - title: S.of(context).deuro_savings, - subTitle: S.of(context).deuro_savings_subtitle, - image: Image.asset( - 'assets/images/deuro_icon.png', - height: 80, - width: 80, - fit: BoxFit.cover, - ), - ), - ], + Observer(builder: (_) { + if (dashboardViewModel.type == WalletType.ethereum) { + return DashBoardRoundedCardWidget( + isDarkTheme: dashboardViewModel.isDarkTheme, + shadowBlur: dashboardViewModel.getShadowBlur(), + shadowSpread: dashboardViewModel.getShadowSpread(), + onTap: () => Navigator.of(context).pushNamed(Routes.dEuroSavings), + title: S.of(context).deuro_savings, + subTitle: S.of(context).deuro_savings_subtitle, + image: Image.asset( + 'assets/images/deuro_icon.png', + height: 80, + width: 80, + fit: BoxFit.cover, + ), + ); + } + + return const SizedBox(); + }), DashBoardRoundedCardWidget( isDarkTheme: dashboardViewModel.isDarkTheme, shadowBlur: dashboardViewModel.getShadowBlur(), diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index bf761bd4d..94dd3c928 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -302,7 +302,8 @@ class ExchangeCardState extends State> { final paymentRequest = PaymentRequest.fromUri(uri); addressController.text = paymentRequest.address; - if (amountController.text.isNotEmpty) { + if (amountController.text.isNotEmpty && + paymentRequest.amount.isNotEmpty) { _showAmountPopup(context, paymentRequest); return; } diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index a34abc7d4..94312aaa1 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -263,11 +263,17 @@ class AddressTextField extends StatelessWidget { final address = clipboard?.text ?? ''; if (address.isNotEmpty) { - try { - final uri = Uri.parse(address); - controller?.text = uri.path; - onURIScanned?.call(uri); - } catch (_) { + // if it has query parameters then it's a valid uri + // added because Uri.parse(address) can parse a normal address string and would still be valid + if (address.contains("=")) { + try { + final uri = Uri.parse(address); + controller?.text = uri.path; + onURIScanned?.call(uri); + } catch (_) { + controller?.text = address; + } + } else { controller?.text = address; } } diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index a4ab28ed7..740364dd1 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,15 +14,15 @@ TYPES=($MONERO_COM $CAKEWALLET) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="5.0.0" -MONERO_COM_BUILD_NUMBER=125 +MONERO_COM_VERSION="5.1.0" +MONERO_COM_BUILD_NUMBER=127 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.0.1" -CAKEWALLET_BUILD_NUMBER=264 +CAKEWALLET_VERSION="5.1.0" +CAKEWALLET_BUILD_NUMBER=267 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index b7cc58a73..e1105d96d 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -12,13 +12,13 @@ TYPES=($MONERO_COM $CAKEWALLET) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="5.0.0" -MONERO_COM_BUILD_NUMBER=125 +MONERO_COM_VERSION="5.1.0" +MONERO_COM_BUILD_NUMBER=127 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.0.1" -CAKEWALLET_BUILD_NUMBER=322 +CAKEWALLET_VERSION="5.1.0" +CAKEWALLET_BUILD_NUMBER=324 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 0257868b2..e85cd8238 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -14,8 +14,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.0.0" -CAKEWALLET_BUILD_NUMBER=56 +CAKEWALLET_VERSION="5.1.0" +CAKEWALLET_BUILD_NUMBER=58 if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index c03bf8b15..29c7f0291 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="5.0.0" -MONERO_COM_BUILD_NUMBER=54 +MONERO_COM_VERSION="5.1.0" +MONERO_COM_BUILD_NUMBER=56 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.0.0" -CAKEWALLET_BUILD_NUMBER=116 +CAKEWALLET_VERSION="5.1.0" +CAKEWALLET_BUILD_NUMBER=119 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss index 13375c22a..4ea9d1153 100644 --- a/scripts/windows/build_exe_installer.iss +++ b/scripts/windows/build_exe_installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Cake Wallet" -#define MyAppVersion "5.0.0" +#define MyAppVersion "5.1.0" #define MyAppPublisher "Cake Labs LLC" #define MyAppURL "https://cakewallet.com/" #define MyAppExeName "CakeWallet.exe" diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index a8ebcc8cc..3bd227506 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -12,8 +12,8 @@ class SecretKey { SecretKey('shortKey', () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), SecretKey('backupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(8).bytes)), SecretKey('backupKeychainSalt', () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), - SecretKey('changeNowApiKey', () => ''), - SecretKey('changeNowApiKeyDesktop', () => ''), + SecretKey('changeNowCakeWalletApiKey', () => ''), + SecretKey('changeNowMoneroApiKey', () => ''), SecretKey('wyreSecretKey', () => ''), SecretKey('wyreApiKey', () => ''), SecretKey('wyreAccountId', () => ''), @@ -26,12 +26,14 @@ class SecretKey { SecretKey('onramperApiKey', () => ''), SecretKey('ioniaClientId', () => ''), SecretKey('trocadorApiKey', () => ''), + SecretKey('trocadorMoneroApiKey', () => ''), SecretKey('trocadorExchangeMarkup', () => ''), SecretKey('twitterBearerToken', () => ''), SecretKey('anonPayReferralCode', () => ''), SecretKey('fiatApiKey', () => ''), SecretKey('chatwootWebsiteToken', () => ''), - SecretKey('exolixApiKey', () => ''), + SecretKey('exolixCakeWalletApiKey', () => ''), + SecretKey('exolixMoneroApiKey', () => ''), SecretKey('robinhoodApplicationId', () => ''), SecretKey('exchangeHelperApiKey', () => ''), SecretKey('walletConnectProjectId', () => ''),