From d86c501fc55041b4bc551f63bd918009fc974b0a Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 28 Apr 2025 07:21:12 +0300 Subject: [PATCH 01/90] patch: running android related code on other platforms --- ios/Podfile.lock | 26 ++++++++++++++----- lib/utils/exception_handler.dart | 4 ++- .../dashboard/dashboard_view_model.dart | 19 +++++++------- scripts/ios/app_env.sh | 8 +++--- scripts/linux/app_env.sh | 2 +- scripts/macos/app_env.sh | 4 +-- 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d3ba59827..50ca9d6de 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,7 +1,6 @@ PODS: - connectivity_plus (0.0.1): - Flutter - - ReachabilitySwift - CryptoSwift (1.8.4) - cw_decred (0.0.1): - Flutter @@ -59,6 +58,8 @@ PODS: - OrderedSet (~> 6.0.3) - flutter_local_authentication (1.2.0): - Flutter + - flutter_local_notifications (0.0.1): + - Flutter - flutter_mailer (0.0.1): - Flutter - flutter_secure_storage (6.0.0): @@ -77,7 +78,9 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - ReachabilitySwift (5.2.4) + - reown_yttrium (0.0.1): + - Flutter + - YttriumWrapper (= 0.8.35) - SDWebImage (5.20.0): - SDWebImage/Core (= 5.20.0) - SDWebImage/Core (5.20.0) @@ -100,6 +103,7 @@ PODS: - Flutter - wakelock_plus (0.0.1): - Flutter + - YttriumWrapper (0.8.35) DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) @@ -114,6 +118,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) @@ -122,6 +127,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - reown_yttrium (from `.symlinks/plugins/reown_yttrium/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -137,9 +143,9 @@ SPEC REPOS: - DKImagePickerController - DKPhotoGallery - OrderedSet - - ReachabilitySwift - SDWebImage - SwiftyGif + - YttriumWrapper EXTERNAL SOURCES: connectivity_plus: @@ -164,6 +170,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_local_authentication: :path: ".symlinks/plugins/flutter_local_authentication/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_mailer: :path: ".symlinks/plugins/flutter_mailer/ios" flutter_secure_storage: @@ -180,6 +188,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + reown_yttrium: + :path: ".symlinks/plugins/reown_yttrium/ios" sensitive_clipboard: :path: ".symlinks/plugins/sensitive_clipboard/ios" share_plus: @@ -198,7 +208,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 @@ -212,6 +222,7 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb + flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f @@ -221,7 +232,7 @@ SPEC CHECKSUMS: package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda + reown_yttrium: c0e87e5965fa60a3559564cc35cffbba22976089 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f @@ -231,8 +242,9 @@ SPEC CHECKSUMS: uni_links: d97da20c7701486ba192624d99bffaaffcfc298a universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 + wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e + YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966 -PODFILE CHECKSUM: e448f662d4c41f0c0b1ccbb78afd57dbf895a597 +PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a COCOAPODS: 1.16.2 diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 547ffa571..a9348dd33 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -230,6 +230,8 @@ class ExceptionHandler { static Future _addDeviceInfo(File file) async { final packageInfo = await PackageInfo.fromPlatform(); final currentVersion = packageInfo.version; + final appName = packageInfo.appName; + final package = packageInfo.packageName; final deviceInfoPlugin = DeviceInfoPlugin(); Map deviceInfo = {}; @@ -252,7 +254,7 @@ class ExceptionHandler { } await file.writeAsString( - "App Version: $currentVersion\n\nDevice Info $deviceInfo\n\n", + "App Version: $currentVersion\nApp Name: $appName\nPackage: $package\n\nDevice Info $deviceInfo\n\n", mode: FileMode.append, ); } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index e5f021783..00353f117 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -4,7 +4,6 @@ import 'dart:io' show Platform; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/core/background_sync.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; @@ -538,7 +537,8 @@ abstract class DashboardViewModelBase with Store { } @observable - late bool backgroundSyncNotificationsEnabled = sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false; + late bool backgroundSyncNotificationsEnabled = + sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false; @action Future setBackgroundSyncNotificationsEnabled(bool value) async { @@ -558,7 +558,6 @@ abstract class DashboardViewModelBase with Store { await sharedPreferences.setBool(PreferencesKey.backgroundSyncNotificationsEnabled, value); } - bool get hasBgsyncNetworkConstraints => Platform.isAndroid; bool get hasBgsyncBatteryNotLowConstraints => Platform.isAndroid; bool get hasBgsyncChargingConstraints => Platform.isAndroid; @@ -575,12 +574,14 @@ abstract class DashboardViewModelBase with Store { @observable bool backgroundSyncDeviceIdle = false; - + Future _loadConstraints() async { - backgroundSyncNetworkUnmetered = await FlutterDaemon().getNetworkType(); - backgroundSyncBatteryNotLow = await FlutterDaemon().getBatteryNotLow(); - backgroundSyncCharging = await FlutterDaemon().getRequiresCharging(); - backgroundSyncDeviceIdle = await FlutterDaemon().getDeviceIdle(); + if (Platform.isAndroid) { + backgroundSyncNetworkUnmetered = await FlutterDaemon().getNetworkType(); + backgroundSyncBatteryNotLow = await FlutterDaemon().getBatteryNotLow(); + backgroundSyncCharging = await FlutterDaemon().getRequiresCharging(); + backgroundSyncDeviceIdle = await FlutterDaemon().getDeviceIdle(); + } } @action @@ -618,7 +619,7 @@ abstract class DashboardViewModelBase with Store { await enableBackgroundSync(); } } - + bool get hasBatteryOptimization => Platform.isAndroid; @observable diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 0b30e7a88..a6d01f16e 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="4.27.0" -MONERO_COM_BUILD_NUMBER=118 +MONERO_COM_VERSION="4.27.1" +MONERO_COM_BUILD_NUMBER=120 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.27.0" -CAKEWALLET_BUILD_NUMBER=313 +CAKEWALLET_VERSION="4.27.1" +CAKEWALLET_BUILD_NUMBER=314 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index 4bc735e60..9e3676b22 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -15,7 +15,7 @@ fi CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.27.0" -CAKEWALLET_BUILD_NUMBER=53 +CAKEWALLET_BUILD_NUMBER=54 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 bc40f02b8..f99cee740 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -17,12 +17,12 @@ fi MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="4.27.0" -MONERO_COM_BUILD_NUMBER=50 +MONERO_COM_BUILD_NUMBER=51 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.27.0" -CAKEWALLET_BUILD_NUMBER=112 +CAKEWALLET_BUILD_NUMBER=113 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From e527083871fda2f90da12d98ebf9c7003ada08f8 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 28 Apr 2025 15:12:42 +0300 Subject: [PATCH 02/90] Gracefully handle toast messages error on desktop platforms --- cw_zano/lib/zano_formatter.dart | 5 +++-- .../pages/balance/balance_row_widget.dart | 19 +++++++++++-------- lib/view_model/link_view_model.dart | 18 ++++++++++-------- macos/Podfile.lock | 12 +++++++----- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/cw_zano/lib/zano_formatter.dart b/cw_zano/lib/zano_formatter.dart index e20e3a5f7..4aa35b1b1 100644 --- a/cw_zano/lib/zano_formatter.dart +++ b/cw_zano/lib/zano_formatter.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_zano/zano_wallet_api.dart'; import 'package:decimal/decimal.dart'; import 'package:decimal/intl.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -57,7 +56,9 @@ class ZanoFormatter { static int parseAmount(String amount, [int decimalPoint = defaultDecimalPoint]) { final resultBigInt = (Decimal.parse(amount) * Decimal.fromBigInt(BigInt.from(10).pow(decimalPoint))).toBigInt(); if (!resultBigInt.isValidInt) { - Fluttertoast.showToast(msg: 'Cannot transfer $amount. Maximum is ${intAmountToString(resultBigInt.toInt(), decimalPoint)}.'); + try { + Fluttertoast.showToast(msg: 'Cannot transfer $amount. Maximum is ${intAmountToString(resultBigInt.toInt(), decimalPoint)}.'); + } catch (_) {} } return resultBigInt.toInt(); } diff --git a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart index 47d522173..33839a25f 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_row_widget.dart @@ -87,10 +87,7 @@ class BalanceRowWidget extends StatelessWidget { // ], ), child: TextButton( - onPressed: () => Fluttertoast.showToast( - msg: S.current.show_balance_toast, - backgroundColor: Color.fromRGBO(0, 0, 0, 0.85), - ), + onPressed: _showToast, onLongPress: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), style: TextButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), @@ -366,10 +363,7 @@ class BalanceRowWidget extends StatelessWidget { // ], ), child: TextButton( - onPressed: () => Fluttertoast.showToast( - msg: S.current.show_balance_toast, - backgroundColor: Color.fromRGBO(0, 0, 0, 0.85), - ), + onPressed: _showToast, onLongPress: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), style: TextButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), @@ -709,4 +703,13 @@ class BalanceRowWidget extends StatelessWidget { void _showBalanceDescription(BuildContext context, String content) { showPopUp(context: context, builder: (_) => InformationPage(information: content)); } + + void _showToast() async { + try { + await Fluttertoast.showToast( + msg: S.current.show_balance_toast, + backgroundColor: Color.fromRGBO(0, 0, 0, 0.85), + ); + } catch (_) {} + } } diff --git a/lib/view_model/link_view_model.dart b/lib/view_model/link_view_model.dart index 27f0c0560..1a89943d4 100644 --- a/lib/view_model/link_view_model.dart +++ b/lib/view_model/link_view_model.dart @@ -81,14 +81,16 @@ class LinkViewModel { } Future _errorToast(String message, {double fontSize = 16}) async { - Fluttertoast.showToast( - msg: message, - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.SNACKBAR, - backgroundColor: Colors.black, - textColor: Colors.white, - fontSize: fontSize, - ); + try { + await Fluttertoast.showToast( + msg: message, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.SNACKBAR, + backgroundColor: Colors.black, + textColor: Colors.white, + fontSize: fontSize, + ); + } catch (_) {} } Future handleLink() async { diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 689f0ea03..5b209b0cf 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,7 +1,6 @@ PODS: - connectivity_plus (0.0.1): - FlutterMacOS - - ReachabilitySwift - cw_mweb (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): @@ -15,6 +14,8 @@ PODS: - OrderedSet (~> 6.0.3) - flutter_local_authentication (1.2.0): - FlutterMacOS + - flutter_local_notifications (0.0.1): + - FlutterMacOS - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) @@ -26,7 +27,6 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - ReachabilitySwift (5.2.4) - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): @@ -50,6 +50,7 @@ DEPENDENCIES: - fast_scanner (from `Flutter/ephemeral/.symlinks/plugins/fast_scanner/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_local_authentication (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_authentication/macos`) + - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) @@ -65,7 +66,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - OrderedSet - - ReachabilitySwift EXTERNAL SOURCES: connectivity_plus: @@ -82,6 +82,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos flutter_local_authentication: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_authentication/macos + flutter_local_notifications: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos flutter_secure_storage_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: @@ -106,20 +108,20 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos SPEC CHECKSUMS: - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802 cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 fast_scanner: d31bae07e2653403a69dac99fb710c1722b16a97 flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0 + flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda share_plus: 1fa619de8392a4398bfaf176d441853922614e89 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: 269d96e0ec3173e69156be7239b95182be3b8303 From da8a82a10f1d915342bcf73e7a4965a39dea2b1d Mon Sep 17 00:00:00 2001 From: cyan Date: Wed, 30 Apr 2025 08:29:08 +0200 Subject: [PATCH 03/90] feat(dev): add secrets dev screen (used to recover corrupted wallet) (#2234) * feat(dev): add secrets dev screen (used to recover corrupted wallet) * fix(dev): decode only password fields --- lib/core/backup_service.dart | 16 ++++ lib/di.dart | 6 ++ lib/router.dart | 6 ++ lib/routes.dart | 1 + .../screens/dev/secure_preferences_page.dart | 63 ++++++++++++++++ .../screens/settings/other_settings_page.dart | 6 ++ lib/view_model/dev/secure_preferences.dart | 75 +++++++++++++++++++ 7 files changed, 173 insertions(+) create mode 100644 lib/src/screens/dev/secure_preferences_page.dart create mode 100644 lib/view_model/dev/secure_preferences.dart diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 385598060..cb36072fe 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -251,6 +251,22 @@ class $BackupService { await importWalletKeychainInfo(info); }); + for (var key in (keychainJSON['_all'] as Map).keys) { + try { + if (!key.startsWith('MONERO_WALLET_')) continue; + final decodedPassword = decodeWalletPassword(password: keychainJSON['_all'][key].toString()); + final walletName = key.split('_WALLET_')[1]; + final walletType = key.split('_WALLET_')[0].toLowerCase(); + await importWalletKeychainInfo({ + 'name': walletName, + 'type': "WalletType.$walletType", + 'password': decodedPassword, + }); + } catch (e) { + printV('Error importing wallet ($key) password: $e'); + } + } + keychainDumpFile.deleteSync(); } diff --git a/lib/di.dart b/lib/di.dart index 60a0f240d..067fd7c4f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -33,12 +33,14 @@ 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/secure_preferences_page.dart'; import 'package:cake_wallet/src/screens/dev/shared_preferences_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'; import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.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/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; @@ -920,6 +922,8 @@ Future setup({ getIt.registerFactory(() => DevSharedPreferences()); + getIt.registerFactory(() => DevSecurePreferences()); + getIt.registerFactoryParam((bool isWalletCreated, _) => WalletSeedPage(getIt.get(), isNewWalletCreated: isWalletCreated)); @@ -1463,6 +1467,8 @@ Future setup({ getIt.registerFactory(() => DevMoneroCallProfilerPage()); getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get())); + + getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get())); getIt.registerFactory(() => BackgroundSyncLogsViewModel()); diff --git a/lib/router.dart b/lib/router.dart index dfd163122..322c79904 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -36,6 +36,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/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'; import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; @@ -853,6 +854,11 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(), ); + case Routes.devSecurePreferences: + return MaterialPageRoute( + builder: (_) => getIt.get(), + ); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index a9a637c75..73fe8f9b5 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -115,6 +115,7 @@ class Routes { 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 signPage = '/sign_page'; diff --git a/lib/src/screens/dev/secure_preferences_page.dart b/lib/src/screens/dev/secure_preferences_page.dart new file mode 100644 index 000000000..23ab0c9df --- /dev/null +++ b/lib/src/screens/dev/secure_preferences_page.dart @@ -0,0 +1,63 @@ +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/view_model/dev/secure_preferences.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class DevSecurePreferencesPage extends BasePage { + final DevSecurePreferences viewModel; + + DevSecurePreferencesPage(this.viewModel); + + @override + String? get title => "[dev] secure preferences"; + + @override + Widget body(BuildContext context) { + return Observer( + builder: (_) { + if (viewModel.values.isEmpty) { + return Center(child: Text("No secure preferences found")); + } + final keys = viewModel.keys; + Map values = {}; + for (final key in keys) { + values[key] = viewModel.get(key); + } + Map types = {}; + for (final key in keys) { + types[key] = viewModel.getPreferenceType(key); + } + return ListView.builder( + itemCount: keys.length, + itemBuilder: (context, index) { + final key = keys[index]; + final type = types[key]!; + return ListTile( + onTap: () { + Clipboard.setData(ClipboardData(text: key + ": " + values[key].toString())); + }, + title: switch (type) { + PreferenceType.bool => Text(key, style: TextStyle(color: Colors.blue)), + PreferenceType.int => Text(key, style: TextStyle(color: Colors.green)), + PreferenceType.double => Text(key, style: TextStyle(color: Colors.yellow)), + PreferenceType.listString => Text(key, style: TextStyle(color: Colors.purple)), + PreferenceType.string => Text(key), + PreferenceType.unknown => Text(key), + }, + subtitle: switch (type) { + PreferenceType.bool => Text("bool: ${values[key]}"), + PreferenceType.int => Text("int: ${values[key]}"), + PreferenceType.double => Text("double: ${values[key]}"), + PreferenceType.listString => values[key].isEmpty as bool ? Text("listString: []") : Text("listString:\n- ${values[key].join("\n- ")}"), + PreferenceType.string => Text("string: ${values[key]}"), + PreferenceType.unknown => Text("UNKNOWN(${values[key].runtimeType}): ${values[key]}"), + }, + ); + }, + ); + }, + ); + } + +} \ No newline at end of file diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 730699286..a52f9ae06 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -81,6 +81,12 @@ class OtherSettingsPage extends BasePage { handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.devSharedPreferences), ), + if (FeatureFlag.hasDevOptions) + SettingsCellWithArrow( + title: '[dev] secure storage preferences', + handler: (BuildContext context) => + Navigator.of(context).pushNamed(Routes.devSecurePreferences), + ), if (FeatureFlag.hasDevOptions) SettingsCellWithArrow( title: '[dev] background sync logs', diff --git a/lib/view_model/dev/secure_preferences.dart b/lib/view_model/dev/secure_preferences.dart new file mode 100644 index 000000000..379022f89 --- /dev/null +++ b/lib/view_model/dev/secure_preferences.dart @@ -0,0 +1,75 @@ +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/entities/encrypt.dart'; +import 'package:mobx/mobx.dart'; + +part 'secure_preferences.g.dart'; + +class DevSecurePreferences = DevSecurePreferencesBase with _$DevSecurePreferences; + +enum PreferenceType { + unknown, + string, + int, + double, + bool, + listString +} + +abstract class DevSecurePreferencesBase with Store { + DevSecurePreferencesBase() { + secureStorageShared.readAll().then((value) { + values = value; + }); + } + + @observable + Map values = {}; + + @computed + List get keys => values.keys.toList()..sort(); + + @action + Future delete(String key) async { + + } + + dynamic get(String key) { + if (!values.containsKey(key)) { + return null; + } + if (!key.startsWith("MONERO_WALLET_")) return values[key]!; + try { + final decodedPassword = decodeWalletPassword(password: values[key]!); + return values[key]! + "\n\nDecoded: $decodedPassword"; + } catch (e) { + return values[key]! +"\n$e"; + } + } + + Future set(String key, PreferenceType type, dynamic value) async { + + } + + PreferenceType getPreferenceType(String key) { + if (!values.containsKey(key)) { + return PreferenceType.unknown; + } + final value = values[key]; + if (value is String) { + return PreferenceType.string; + } + if (value is bool) { + return PreferenceType.bool; + } + if (value is int) { + return PreferenceType.int; + } + if (value is double) { + return PreferenceType.double; + } + if (value is List) { + return PreferenceType.listString; + } + return PreferenceType.unknown; + } +} From 0188535eb97f3c64060485051e00f37c269f0806 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Wed, 30 Apr 2025 10:27:21 +0300 Subject: [PATCH 04/90] Enable error reports without cooldown in testing builds --- lib/utils/exception_handler.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index a9348dd33..7f6571f3f 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -20,7 +20,8 @@ import 'package:shared_preferences/shared_preferences.dart'; class ExceptionHandler { static bool _hasError = false; - static const _coolDownDurationInDays = 7; + static const _coolDownDurationInDays = + bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode) ? 0 : 7; static File? _file; static Future _saveException(String? error, StackTrace? stackTrace, From 4448adb49ddc698baeb97687d91970233111989c Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 30 Apr 2025 19:11:36 +0300 Subject: [PATCH 05/90] Cw 925 improve automatic subaddress generation behavior (#2217) * fix: update usedAddresses for monero * fix: subaddresses generation for bitcoin * fix: minor fix * fix: update receive address UI when used * minor fix [skip ci] --- cw_bitcoin/lib/bitcoin_address_record.dart | 9 +++-- cw_bitcoin/lib/electrum_wallet_addresses.dart | 39 +++++++++++-------- cw_monero/lib/monero_wallet_addresses.dart | 4 +- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 7e4b5f58f..1509f913a 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:mobx/mobx.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; @@ -16,7 +17,7 @@ abstract class BaseBitcoinAddressRecord { }) : _txCount = txCount, _balance = balance, _name = name, - _isUsed = isUsed; + _isUsed = Observable(isUsed); @override bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; @@ -27,7 +28,7 @@ abstract class BaseBitcoinAddressRecord { int _txCount; int _balance; String _name; - bool _isUsed; + final Observable _isUsed; BasedUtxoNetwork? network; int get txCount => _txCount; @@ -40,9 +41,9 @@ abstract class BaseBitcoinAddressRecord { set balance(int value) => _balance = value; - bool get isUsed => _isUsed; + bool get isUsed => _isUsed.value; - void setAsUsed() => _isUsed = true; + void setAsUsed() => _isUsed.value = true; void setNewName(String label) => _name = label; int get hashCode => address.hashCode; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 35c15e578..614a06a3b 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -144,27 +144,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return silentAddress.toString(); } - String receiveAddress; + final typeMatchingAddresses = _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); + final typeMatchingReceiveAddresses = typeMatchingAddresses.where((addr) => !addr.isUsed).toList(); - final typeMatchingReceiveAddresses = - receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed); - - if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || - typeMatchingReceiveAddresses.isEmpty) { - receiveAddress = generateNewAddress().address; - } else { - final previousAddressMatchesType = - previousAddressRecord != null && previousAddressRecord!.type == addressPageType; - - if (previousAddressMatchesType && - typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) { - receiveAddress = previousAddressRecord!.address; - } else { - receiveAddress = typeMatchingReceiveAddresses.first.address; + if (!isEnabledAutoGenerateSubaddress) { + if (previousAddressRecord != null && + previousAddressRecord!.type == addressPageType) { + return previousAddressRecord!.address; } + + if (typeMatchingAddresses.isNotEmpty) { + return typeMatchingAddresses.first.address; + } + + return generateNewAddress().address; } - return receiveAddress; + if (typeMatchingAddresses.isEmpty || typeMatchingReceiveAddresses.isEmpty) { + return generateNewAddress().address; + } + + final prev = previousAddressRecord; + if (prev != null && prev.type == addressPageType && !prev.isUsed) { + return prev.address; + } + + return typeMatchingReceiveAddresses.first.address; } @observable diff --git a/cw_monero/lib/monero_wallet_addresses.dart b/cw_monero/lib/monero_wallet_addresses.dart index 14b8a2b9b..d0cf87489 100644 --- a/cw_monero/lib/monero_wallet_addresses.dart +++ b/cw_monero/lib/monero_wallet_addresses.dart @@ -130,8 +130,8 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store { final transactions = _moneroTransactionHistory.transactions.values.toList(); transactions.forEach((element) { - final accountIndex = element.accountIndex; - final addressIndex = element.addressIndex; + final accountIndex = element.additionalInfo['accountIndex'] as int? ?? 0; + final addressIndex = element.additionalInfo['addressIndex'] as int? ?? 0; usedAddresses.add(getAddress(accountIndex: accountIndex, addressIndex: addressIndex)); }); } From 0e4c9bb1388fc43066f9d3b5b20da52a1b447a77 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 1 May 2025 16:21:47 +0300 Subject: [PATCH 06/90] Fix not accepting "," in some currencies --- lib/view_model/send/output.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 23efe8c1a..6c3588404 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -250,7 +250,7 @@ abstract class OutputBase with Store { sendAll = false; } - cryptoAmount = amount; + cryptoAmount = amount.replaceAll(',', '.'); _updateFiatAmount(); } From d4190cbb0f741e2156f877fd1ea7b5e64c2e1aad Mon Sep 17 00:00:00 2001 From: rottenwheel <92872541+rottenwheel@users.noreply.github.com> Date: Thu, 1 May 2025 11:21:16 -0400 Subject: [PATCH 07/90] en: fix unmetered network typo (#2241) * fix: remove comma in OpenAlias title * spanish: some necessary, minor rewording for new background sync strings * en: fix unmetered network typo --- res/values/strings_en.arb | 4 ++-- res/values/strings_es.arb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 69495ba72..2072b149b 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -76,7 +76,7 @@ "background_sync_on_battery_low": "Synchronize on low battery", "background_sync_on_charging": "Synchronize only when charging", "background_sync_on_device_idle": "Synchronize only when device is not being used", - "background_sync_on_unmetered_network": "Require unmetred network", + "background_sync_on_unmetered_network": "Require unmetered network", "backup": "Backup", "backup_file": "Backup file", "backup_password": "Backup password", @@ -1059,4 +1059,4 @@ "you_will_send": "Convert from", "youCanGoBackToYourDapp": "You can go back to your dApp now", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 70a809243..bc3cbff48 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -1059,4 +1059,4 @@ "you_will_send": "Convertir de", "youCanGoBackToYourDapp": "Puedes volver a tu dapp ahora", "yy": "YY" -} \ No newline at end of file +} From 2a6dad22576ece73807a844f4cac66b369290b26 Mon Sep 17 00:00:00 2001 From: Tobias Wienkoop Date: Thu, 1 May 2025 17:22:54 +0200 Subject: [PATCH 08/90] Improve german translation (#2230) * Update strings_de.arb * Update strings_de.arb * Update strings_de.arb --- res/values/strings_de.arb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index b87ff03d3..c5743cd8b 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -73,10 +73,10 @@ "awaiting_payment_confirmation": "Warten auf Zahlungsbestätigung", "background_sync": "Hintergrundsynchronisation", "background_sync_mode": "Hintergrundsynchronisierungsmodus", - "background_sync_on_battery_low": "Synchronisieren Sie einen niedrigen Akku", + "background_sync_on_battery_low": "Bei niedrigem Akkustand synchronisieren", "background_sync_on_charging": "Nur beim Laden synchronisieren", "background_sync_on_device_idle": "Nur dann synchronisieren, wenn das Gerät nicht verwendet wird", - "background_sync_on_unmetered_network": "Erfordern ein nicht modisches Netzwerk", + "background_sync_on_unmetered_network": "Erfordere ein ungetaktetes Netzwerk", "backup": "Sicherung", "backup_file": "Sicherungsdatei", "backup_password": "Passwort sichern", @@ -276,7 +276,7 @@ "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", - "enabled": "Ermöglicht", + "enabled": "Aktiviert", "enter_amount": "Betrag eingeben", "enter_backup_password": "Sicherungskennwort hier eingeben", "enter_code": "Code eingeben", @@ -1061,4 +1061,4 @@ "you_will_send": "Konvertieren von", "youCanGoBackToYourDapp": "Sie können jetzt zu Ihrem Dapp zurückkehren", "yy": "YY" -} \ No newline at end of file +} From cb257e1391325633acd818356f9e7fb77b8e4bb2 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 1 May 2025 18:23:56 +0300 Subject: [PATCH 09/90] fix: allow address only edits (#2233) --- lib/view_model/contact_list/contact_view_model.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index efa1f09f4..2030cf03e 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -57,7 +57,11 @@ abstract class ContactViewModelBase with Store { state = IsExecutingState(); final now = DateTime.now(); - if (doesContactNameExist(name)) { + final nameExists = _contact == null + ? doesContactNameExist(name) + : doesContactNameExist(name) && _contact.original.name != name; + + if (nameExists) { state = FailureState(S.current.contact_name_exists); return; } From dd8413bae264c79a3a9eb0fa1233004160fe3363 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 2 May 2025 00:56:06 +0100 Subject: [PATCH 10/90] CW 818: Improve Passphrase Discoverability When Restoring (#2224) * feat(passphrase for restore): improve passphrase discoverability for restore flow. Makes the option for adding a passphrase more visible for the user to see while restoring a wallet with seeds. This change: - Adds a checkbox on the restore from seeds page that asks if the wallet has a passphrase - Switches the main CTA for the page to "Add Passphrase" when the checkbox is checked - Pops up a bottomsheet with input fields for the passphrase when the "Add Passphrase" button is pressed - Completes the flow and restores the wallet with the passphrase when the "Restore" button on the bottomsheet is pressed - Hides the passphrase input fields in AdvancedSettings page when its a restore flow. * feat(passphrase for restore): improve passphrase discoverability for restore flow. (Cont.) This change: - Obscures the passpgrase input fields by default - Changes the color for the main CTA button * feat(passphrase for restore): improve passphrase discoverability for restore flow. (Cont.) This change: - Adjusts the colors for the buttons on both the seed page and the bottomsheet * feat(passphrase for restore): improve passphrase discoverability for restore flow. (Cont.) This change: - Combines the control for the visibility icons of the passphrase input fields - one tap to rule them all :) --- assets/images/passphrase_key.svg | 22 ++ .../advanced_privacy_settings_page.dart | 2 +- .../screens/restore/wallet_restore_page.dart | 154 +++++++----- lib/src/widgets/base_text_form_field.dart | 6 + .../add_passphrase_bottom_sheet_widget.dart | 220 ++++++++++++++++++ lib/view_model/wallet_restore_view_model.dart | 4 + res/values/strings_ar.arb | 5 + res/values/strings_bg.arb | 5 + res/values/strings_cs.arb | 5 + res/values/strings_de.arb | 5 + res/values/strings_en.arb | 5 + res/values/strings_es.arb | 5 + res/values/strings_fr.arb | 5 + res/values/strings_ha.arb | 5 + res/values/strings_hi.arb | 5 + res/values/strings_hr.arb | 5 + res/values/strings_hy.arb | 5 + res/values/strings_id.arb | 5 + res/values/strings_it.arb | 5 + res/values/strings_ja.arb | 5 + res/values/strings_ko.arb | 5 + res/values/strings_my.arb | 5 + res/values/strings_nl.arb | 5 + res/values/strings_pl.arb | 5 + res/values/strings_pt.arb | 5 + res/values/strings_ru.arb | 5 + res/values/strings_th.arb | 5 + res/values/strings_tl.arb | 5 + res/values/strings_tr.arb | 5 + res/values/strings_uk.arb | 5 + res/values/strings_ur.arb | 5 + res/values/strings_vi.arb | 5 + res/values/strings_yo.arb | 5 + res/values/strings_zh.arb | 5 + 34 files changed, 491 insertions(+), 57 deletions(-) create mode 100644 assets/images/passphrase_key.svg create mode 100644 lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart diff --git a/assets/images/passphrase_key.svg b/assets/images/passphrase_key.svg new file mode 100644 index 000000000..c577dc30a --- /dev/null +++ b/assets/images/passphrase_key.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index d03f334a1..1511ffd83 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -204,7 +204,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ); return Container(); }), - if (widget.privacySettingsViewModel.hasPassphraseOption) + if (widget.privacySettingsViewModel.hasPassphraseOption && !widget.isFromRestore) Padding( padding: EdgeInsets.all(24), child: Form( diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index e2e149644..2c26741e1 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -5,10 +5,14 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_from_keys_form.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_from_seed_form.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; -import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; @@ -52,7 +56,6 @@ class WalletRestorePage extends BasePage { // String? derivationPath = null; DerivationInfo? derivationInfo; - @override Function(BuildContext)? get popWidget => (context) => seedSettingsViewModel.setPassphrase(null); @@ -102,37 +105,81 @@ class WalletRestorePage extends BasePage { padding: EdgeInsets.only(top: 20, bottom: 24, left: 24, right: 24), child: Column( children: [ + Observer( + builder: (context) { + return walletRestoreViewModel.mode == WalletRestoreMode.seed + ? StandardCheckbox( + value: walletRestoreViewModel.hasPassphrase, + caption: S.of(context).wallet_has_passphrase, + onChanged: (value) { + walletRestoreViewModel.hasPassphrase = value; + }, + ) + : SizedBox.shrink(); + }, + ), + SizedBox(height: 16), + PrimaryButton( + key: ValueKey('wallet_restore_advanced_settings_button_key'), + onPressed: () { + Navigator.of(context).pushNamed( + Routes.advancedPrivacySettings, + arguments: { + 'isFromRestore': true, + 'type': walletRestoreViewModel.type, + 'useTestnet': walletRestoreViewModel.useTestnet, + 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet + }, + ); + }, + text: S.of(context).advanced_settings, + color: Theme.of(context).cardColor, + textColor: Theme.of(context).extension()!.buttonTextColor, + ), + SizedBox(height: 8), Observer( builder: (context) { return LoadingPrimaryButton( key: ValueKey('wallet_restore_seed_or_key_restore_button_key'), - onPressed: () async => await _confirmForm(context), - text: S.of(context).restore_recover, - color: Theme.of(context) - .extension()! - .createNewWalletButtonBackgroundColor, - textColor: Theme.of(context) - .extension()! - .restoreWalletButtonTextColor, + onPressed: () async { + if (walletRestoreViewModel.hasPassphrase) { + await showModalBottomSheet( + context: context, + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom, + ), + child: AddPassphraseBottomSheet( + currentTheme: currentTheme, + titleText: S.of(context).add_passphrase, + onRestoreButtonPressed: (passphrase) async { + await _onPassphraseBottomSheetRestoreButtonPressed( + passphrase, + context, + ); + }, + ), + ); + }, + ); + } else { + await _confirmForm(context); + } + }, + text: walletRestoreViewModel.hasPassphrase + ? S.of(context).add_passphrase + : S.of(context).restore_recover, + color: Theme.of(context).primaryColor, + textColor: Colors.white, isLoading: walletRestoreViewModel.state is IsExecutingState, isDisabled: !walletRestoreViewModel.isButtonEnabled, ); }, ), - const SizedBox(height: 25), - GestureDetector( - key: ValueKey('wallet_restore_advanced_settings_button_key'), - onTap: () { - Navigator.of(context) - .pushNamed(Routes.advancedPrivacySettings, arguments: { - 'isFromRestore': true, - 'type': walletRestoreViewModel.type, - 'useTestnet': walletRestoreViewModel.useTestnet, - 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet - }); - }, - child: Text(S.of(context).advanced_settings), - ), + const SizedBox(height: 24), ], ), ) @@ -144,6 +191,14 @@ class WalletRestorePage extends BasePage { ); } + Future _onPassphraseBottomSheetRestoreButtonPressed( + String passphrase, + BuildContext context, + ) async { + walletRestoreViewModel.seedSettingsViewModel.setPassphrase(passphrase); + await _confirmForm(context); + } + Map _credentials() { final credentials = {}; @@ -172,9 +227,10 @@ class WalletRestorePage extends BasePage { walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; if (walletRestoreViewModel.type != WalletType.decred) { - credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; + credentials['address'] = + walletRestoreFromKeysFormKey.currentState!.addressController.text; credentials['spendKey'] = - walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; + walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; credentials['height'] = walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; } @@ -335,8 +391,8 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> final initialIndex = walletRestoreViewModel.mode == WalletRestoreMode.seed ? 0 : _hasKeysTab - ? 1 - : 0; + ? 1 + : 0; _tabController = TabController(length: tabCount, vsync: this, initialIndex: initialIndex); @@ -429,7 +485,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> padding: EdgeInsets.zero, tabs: [ Tab(text: S.of(context).widgets_seed), - if (_hasKeysTab) Tab(text: S.of(context).keys), + if (_hasKeysTab) Tab(text: S.of(context).keys), ], ), ), @@ -527,42 +583,30 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> } bool _isValidSeed() { - final seedPhrase = walletRestoreFromSeedFormKey - .currentState!.seedWidgetStateKey.currentState!.text; + final seedPhrase = + walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; if (walletRestoreViewModel.isPolyseed(seedPhrase)) return true; final seedWords = seedPhrase.split(' '); - if (seedWords.length == 14 && - walletRestoreViewModel.type == WalletType.wownero) return true; - if (seedWords.length == 26 && - walletRestoreViewModel.type == WalletType.zano) return true; + if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true; + if (seedWords.length == 26 && walletRestoreViewModel.type == WalletType.zano) return true; - if (seedWords.length == 12 && - walletRestoreViewModel.type == WalletType.monero) { - return walletRestoreFromSeedFormKey - .currentState - ?.blockchainHeightKey - .currentState - ?.restoreHeightController - .text - .isNotEmpty == true; + if (seedWords.length == 12 && walletRestoreViewModel.type == WalletType.monero) { + return walletRestoreFromSeedFormKey.currentState?.blockchainHeightKey.currentState + ?.restoreHeightController.text.isNotEmpty == + true; } if ([WalletType.monero, WalletType.wownero, WalletType.haven] .contains(walletRestoreViewModel.type) && - seedWords.length == - WalletRestoreViewModelBase.moneroSeedMnemonicLength) { + seedWords.length == WalletRestoreViewModelBase.moneroSeedMnemonicLength) { return true; } // bip39: final validBip39SeedLengths = [12, 18, 24]; - final nonBip39WalletTypes = [ - WalletType.wownero, - WalletType.haven, - WalletType.decred - ]; + final nonBip39WalletTypes = [WalletType.wownero, WalletType.haven, WalletType.decred]; // if it's a bip39 wallet and the length is not valid return false if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) && !(validBip39SeedLengths.contains(seedWords.length))) { @@ -570,14 +614,12 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> } if ((walletRestoreViewModel.type == WalletType.decred) && - seedWords.length != - WalletRestoreViewModelBase.decredSeedMnemonicLength) { + seedWords.length != WalletRestoreViewModelBase.decredSeedMnemonicLength) { return false; } - final words = walletRestoreFromSeedFormKey - .currentState!.seedWidgetStateKey.currentState!.words - .toSet(); + final words = + walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet(); return seedWords.toSet().difference(words).toSet().isEmpty; } diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index 4648b88cc..806ceb04b 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -15,6 +15,8 @@ class BaseTextFormField extends StatelessWidget { this.textColor, this.hintColor, this.borderColor, + this.fillColor, + this.filled, this.prefix, this.prefixIcon, this.suffix, @@ -44,6 +46,8 @@ class BaseTextFormField extends StatelessWidget { final Color? textColor; final Color? hintColor; final Color? borderColor; + final Color? fillColor; + bool? filled; final Widget? prefix; final Widget? prefixIcon; final Widget? suffix; @@ -89,6 +93,8 @@ class BaseTextFormField extends StatelessWidget { prefixIcon: prefixIcon, suffix: suffix, suffixIcon: suffixIcon, + fillColor: fillColor, + filled: filled, hintStyle: placeholderTextStyle ?? TextStyle( color: hintColor ?? Theme.of(context).hintColor, diff --git a/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart new file mode 100644 index 000000000..b6d9b08f1 --- /dev/null +++ b/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart @@ -0,0 +1,220 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class AddPassphraseBottomSheet extends StatefulWidget { + AddPassphraseBottomSheet({ + required String titleText, + required this.currentTheme, + required this.onRestoreButtonPressed, + }); + + final void Function(String) onRestoreButtonPressed; + final ThemeBase currentTheme; + + @override + State createState() => _AddPassphraseBottomSheetState(); +} + +class _AddPassphraseBottomSheetState extends State { + late final TextEditingController passphraseController; + late final TextEditingController confirmPassphraseController; + + @override + void initState() { + super.initState(); + passphraseController = TextEditingController(); + confirmPassphraseController = TextEditingController(); + } + + @override + void dispose() { + passphraseController.dispose(); + confirmPassphraseController.dispose(); + super.dispose(); + } + + bool obscurePassphrase = true; + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical(top: Radius.circular(30.0)), + color: Theme.of(context).dialogBackgroundColor, + ), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + children: [ + Row( + children: [ + const Spacer(flex: 4), + Expanded( + flex: 2, + child: Container( + height: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: + Theme.of(context).extension()!.titleColor.withOpacity(0.6), + ), + ), + ), + const Spacer(flex: 4), + ], + ), + SizedBox(height: 16), + Text( + S.of(context).add_passphrase, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + SvgPicture.asset('assets/images/passphrase_key.svg'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${S.of(context).warning.toUpperCase()}: ', + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w700, + color: Palette.red, + decoration: TextDecoration.none, + ), + ), + TextSpan( + text: S.of(context).add_passphrase_warning_text, + ), + ], + ), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor.withOpacity(0.7), + decoration: TextDecoration.none, + ), + ), + ), + SizedBox(height: 24), + TextFormField( + key: ValueKey('add_passphrase_bottom_sheet_widget_passphrase_textfield_key'), + controller: passphraseController, + obscureText: obscurePassphrase, + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 12), + filled: true, + fillColor: Theme.of(context).cardColor, + hintText: S.of(context).required_passphrase, + suffixIcon: GestureDetector( + onTap: () { + setState(() { + obscurePassphrase = !obscurePassphrase; + }); + }, + child: Icon( + obscurePassphrase ? Icons.visibility_off : Icons.visibility, + size: 24, + color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.6), + ), + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + SizedBox(height: 8), + TextFormField( + key: ValueKey('add_passphrase_bottom_sheet_widget_confirm_passphrase_textfield_key'), + controller: confirmPassphraseController, + obscureText: obscurePassphrase, + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 12), + filled: true, + fillColor: Theme.of(context).cardColor, + hintText: S.of(context).confirm_passphrase, + suffixIcon: GestureDetector( + onTap: () { + setState(() { + obscurePassphrase = !obscurePassphrase; + }); + }, + child: Icon( + obscurePassphrase ? Icons.visibility_off : Icons.visibility, + size: 24, + color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.6), + ), + ), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(16), + ), + ), + validator: (text) { + if (text == passphraseController.text) { + return null; + } + + return S.of(context).passphrases_doesnt_match; + }, + ), + SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.only(right: 8.0, top: 8.0), + child: PrimaryButton( + key: ValueKey('add_passphrase_bottom_sheet_widget_cancel_button_key'), + onPressed: () { + Navigator.pop(context); + }, + text: S.of(context).cancel, + color: Theme.of(context).cardColor, + textColor: Theme.of(context).extension()!.buttonTextColor, + ), + ), + ), + Flexible( + child: Container( + padding: const EdgeInsets.only(left: 8.0, top: 8.0), + child: PrimaryButton( + key: ValueKey('add_passphrase_bottom_sheet_widget_restore_button_key'), + onPressed: () { + Navigator.pop(context); + widget.onRestoreButtonPressed(passphraseController.text); + }, + text: S.of(context).restore, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ), + ), + ], + ), + ), + SizedBox(height: 24), + ], + ), + ), + ); + } +} diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index dec0be6b1..6e00ba4cc 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -43,6 +43,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { type == WalletType.solana || type == WalletType.tron, isButtonEnabled = false, + hasPassphrase = false, mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed, super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, type: type, isRecovery: true) { @@ -89,6 +90,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { @observable WalletRestoreMode mode; + @observable + bool hasPassphrase; + @observable bool isButtonEnabled; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index e68f6dc3f..4082e778c 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "أضف أموالاً مدفوعة مسبقًا إلى البطاقات (حتى ${value})", "add_new_node": "أضافة عقدة جديدة", "add_new_word": "أضف كلمة جديدة", + "add_passphrase": "أضف عبارة المرور", + "add_passphrase_warning_text": "أدخل عبارة المرور فقط إذا كنت قد استخدمت واحدة لهذه المحفظة في الماضي. إذا أدخلت عبارة الممر الخاطئ أو لم تستخدم عبارة تمريرة من قبل على هذه المحفظة ، فلن ترى أيًا من الأموال أو التاريخ الحالي.", "add_receiver": "أضف مستقبل آخر (اختياري)", "add_secret_code": " ﺔﻗﺩﺎﺼﻤﻟﺍ ﻖﻴﺒﻄﺗ ﻰﻟﺇ ﻱﺮﺴﻟﺍ ﺰﻣﺮﻟﺍ ﺍﺬﻫ ﻒﺿﺃ ﻭﺃ", "add_tip": "أضف بقشيش", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "تتطلب عمليات الإرسال إلى المحافظ الداخلية", "require_for_sends_to_non_contacts": "تتطلب لارسال لغير جهات الاتصال", "require_pin_after": "طلب PIN بعد", + "required_passphrase": "عبارة المرور", "rescan": "إعادة الفحص", "resend_code": "الرجاء إعادة إرسالها", "reset": "إعادة", "reset_password": "إعادة تعيين كلمة المرور", + "restore": "يعيد", "restore_active_seed": "السييد النشطة", "restore_address": "العنوان", "restore_bitcoin_description_from_keys": "قم باستعادة محفظتك من سلسلة WIF التي تم إنشاؤها من مفاتيحك الخاصة", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "يمكنك دائمًا عرض هذه البذرة مرة أخرى تحت", "wallet_group_empty_state_text_one": "يبدو أنه ليس لديك أي مجموعات محفظة متوافقة !\n\n انقر", "wallet_group_empty_state_text_two": "أدناه لجعل واحدة جديدة.", + "wallet_has_passphrase": "هذه المحفظة تحتوي على عبارة ممر", "wallet_keys": "سييد المحفظة / المفاتيح", "wallet_list_create_new_wallet": "إنشاء محفظة جديدة", "wallet_list_edit_group_name": "تحرير اسم المجموعة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 775cc8152..32f886335 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Добавете предплатени средства в картите (до ${value})", "add_new_node": "Добави нов node", "add_new_word": "Добавяне на нова дума", + "add_passphrase": "Добавете парола", + "add_passphrase_warning_text": "Въведете парола само ако сте използвали такава за този портфейл в миналото. Ако въведете грешна парола или не сте използвали парола преди в този портфейл, няма да видите нито един от съществуващите средства или история.", "add_receiver": "Добавяне на друг получател (не е задължително)", "add_secret_code": "Или добавете този таен код към приложение за удостоверяване", "add_tip": "Add Tip", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "Изискване за изпращане до вътрешни портфейли", "require_for_sends_to_non_contacts": "Изискване за изпращане до лица без контакт", "require_pin_after": "Въведете PIN след", + "required_passphrase": "Парола", "rescan": "Сканирай отново", "resend_code": "Повторно изпращане", "reset": "Нулиране", "reset_password": "Нулиране на парола", + "restore": "Възстановяване", "restore_active_seed": "Активиране на seed", "restore_address": "Адреси", "restore_bitcoin_description_from_keys": "Възстановяване на портфейл чрез WIF, изведен от Вашите private keys", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "Винаги можете да видите това семе отново под", "wallet_group_empty_state_text_one": "Изглежда, че нямате съвместими групи портфейли !\n\n tap", "wallet_group_empty_state_text_two": "по -долу, за да се направи нов.", + "wallet_has_passphrase": "Този портфейл има парола", "wallet_keys": "Seed/keys на портфейла", "wallet_list_create_new_wallet": "Създаване на нов портфейл", "wallet_list_edit_group_name": "Редактиране на име на групата", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 7f9e0fcf7..2ee9cb693 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Všechny předplacené prostředky na kartě (až ${value})", "add_new_node": "Přidat nový uzel", "add_new_word": "Přidat nové slovo", + "add_passphrase": "Přidejte přístupovou frázi", + "add_passphrase_warning_text": "Zadejte přístupovou frázi pouze tehdy, pokud jste ji v minulosti použili pro tuto peněženku. Pokud zadáte nesprávnou přístupovou frázi nebo jste na této peněžence nepoužili přístupovou frázi, neuvidíte žádnou existující fondy nebo historii.", "add_receiver": "Přidat dalšího příjemce (nepovinné)", "add_secret_code": "Nebo přidejte tento tajný kód do ověřovací aplikace", "add_tip": "Přidat spropitné", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "Vyžadovat pro odesílání do interních peněženek", "require_for_sends_to_non_contacts": "Vyžadovat pro odesílání nekontaktním osobám", "require_pin_after": "Vyžadovat PIN po", + "required_passphrase": "Passphrase", "rescan": "Znovu prohledat", "resend_code": "Prosím poslat znovu", "reset": "Vymazat", "reset_password": "Resetovat heslo", + "restore": "Obnovit", "restore_active_seed": "Aktivní seed", "restore_address": "Adresa", "restore_bitcoin_description_from_keys": "Obnovte svou peněženku pomocí vygenerovaného WIF řetězce z vašich soukromých klíčů", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "Toto semeno si můžete vždy znovu prohlédnout", "wallet_group_empty_state_text_one": "Vypadá to, že nemáte žádné kompatibilní skupiny peněženky !\n\n", "wallet_group_empty_state_text_two": "Níže vytvořit nový.", + "wallet_has_passphrase": "Tato peněženka má přístupovou frázi", "wallet_keys": "Seed/klíče peněženky", "wallet_list_create_new_wallet": "Vytvořit novou peněženku", "wallet_list_edit_group_name": "Upravit název skupiny", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index c5743cd8b..16f661e97 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Prepaid-Guthaben zu den Karten hinzufügen (bis zu ${value})", "add_new_node": "Neuen Knoten hinzufügen", "add_new_word": "Neues Wort hinzufügen", + "add_passphrase": "Fügen Sie Passphrase hinzu", + "add_passphrase_warning_text": "Geben Sie nur eine Passphrase ein, wenn Sie in der Vergangenheit eine für diese Brieftasche verwendet haben. Wenn Sie die falsche Passphrase eingeben oder in dieser Brieftasche noch keine Passphrase verwendet haben, werden Sie keine vorhandenen Gelder oder Geschichte sehen.", "add_receiver": "Fügen Sie einen weiteren Empfänger hinzu (optional)", "add_secret_code": "Oder fügen Sie diesen Geheimcode einer Authentifizierungs-App hinzu", "add_tip": "Tipp hinzufügen", @@ -622,10 +624,12 @@ "require_for_sends_to_internal_wallets": "Erforderlich für Sendungen an interne Wallets", "require_for_sends_to_non_contacts": "Erforderlich für Versendungen an Nichtkontakte", "require_pin_after": "PIN anfordern nach", + "required_passphrase": "Passphrase", "rescan": "Erneut scannen", "resend_code": "Bitte erneut senden", "reset": "Zurücksetzen", "reset_password": "Passwort zurücksetzen", + "restore": "Wiederherstellen", "restore_active_seed": "Aktiver Seed", "restore_address": "Adresse", "restore_bitcoin_description_from_keys": "Stellen Sie Ihre Wallet aus der generierten WIF-Zeichenfolge aus Ihren privaten Schlüsseln wieder her", @@ -1002,6 +1006,7 @@ "wallet_group_description_view_seed": "Sie können diesen Seed immer wieder untersuchen", "wallet_group_empty_state_text_one": "Sieht so aus, als hätten Sie keine kompatiblen Walletgruppen !\n\n TAP", "wallet_group_empty_state_text_two": "unten, um einen neuen zu machen.", + "wallet_has_passphrase": "Diese Brieftasche hat eine Passphrase", "wallet_keys": "Wallet-Seed/-Schlüssel", "wallet_list_create_new_wallet": "Neue Wallet erstellen", "wallet_list_edit_group_name": "Gruppenname bearbeiten", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 2072b149b..b045f944f 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Add prepaid funds to the cards (up to ${value})", "add_new_node": "Add new node", "add_new_word": "Add new word", + "add_passphrase": "Add Passphrase", + "add_passphrase_warning_text": "Only enter a passphrase if you have used one for this wallet in the past. If you enter the wrong passphrase or have not used a passphrase before on this wallet, you won't see any of existing funds or history.", "add_receiver": "Add another receiver (optional)", "add_secret_code": "Or, add this secret code to an authenticator app", "add_tip": "Add Tip", @@ -622,10 +624,12 @@ "require_for_sends_to_internal_wallets": "Require for sends to internal wallets", "require_for_sends_to_non_contacts": "Require for sends to non-contacts", "require_pin_after": "Require PIN after", + "required_passphrase": "Passphrase", "rescan": "Rescan", "resend_code": "Please resend it", "reset": "Reset", "reset_password": "Reset Password", + "restore": "Restore", "restore_active_seed": "Active seed", "restore_address": "Address", "restore_bitcoin_description_from_keys": "Restore your wallet from generated WIF string from your private keys", @@ -1000,6 +1004,7 @@ "wallet_group_description_view_seed": "You can always view this seed again under", "wallet_group_empty_state_text_one": "Looks like you don't have any compatible wallet groups!\n\nTap", "wallet_group_empty_state_text_two": "below to make a new one.", + "wallet_has_passphrase": "This wallet has a passphrase", "wallet_keys": "Wallet seed/keys", "wallet_list_create_new_wallet": "Create New Wallet", "wallet_list_edit_group_name": "Edit Group Name", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index bc3cbff48..d877b1d24 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Agregar fondos prepagos a las tarjetas (hasta ${value})", "add_new_node": "Agregar nuevo nodo", "add_new_word": "Agregar palabra nueva", + "add_passphrase": "Agregar frase de pases", + "add_passphrase_warning_text": "Solo ingrese una frase de pases si ha usado una para esta billetera en el pasado. Si ingresa a la frase de pases incorrecta o no ha utilizado una frase de pases antes en esta billetera, no verá ninguno de los fondos o historial existentes.", "add_receiver": "Agregar otro receptor (opcional)", "add_secret_code": "O agregue este código secreto a una aplicación de autenticación", "add_tip": "Agregar sugerencia", @@ -622,10 +624,12 @@ "require_for_sends_to_internal_wallets": "Requerido para envíos a billeteras internas", "require_for_sends_to_non_contacts": "Requerido para envíos a no contactos", "require_pin_after": "Requerir PIN después de", + "required_passphrase": "Frase", "rescan": "Reescanear", "resend_code": "Por favor reenvíalo", "reset": "Reiniciar", "reset_password": "Restablecer contraseña", + "restore": "Restaurar", "restore_active_seed": "Semilla activa", "restore_address": "Dirección", "restore_bitcoin_description_from_keys": "Restaure su billetera a partir de una cadena WIF generada a partir de sus claves privadas", @@ -1000,6 +1004,7 @@ "wallet_group_description_view_seed": "Siempre puedes ver esta semilla nuevamente debajo", "wallet_group_empty_state_text_one": "Parece que no tienes ningún grupo de billetera compatible !\n\n toque", "wallet_group_empty_state_text_two": "a continuación para hacer uno nuevo.", + "wallet_has_passphrase": "Esta billetera tiene una frase de pases", "wallet_keys": "Billetera semilla/claves", "wallet_list_create_new_wallet": "Crear nueva billetera", "wallet_list_edit_group_name": "Editar nombre de grupo", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 01510b7a9..e2a4617eb 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Ajouter des fonds prépayés aux cartes (jusqu'à ${value})", "add_new_node": "Ajouter un nouveau nœud", "add_new_word": "Ajouter un nouveau mot", + "add_passphrase": "Ajouter la phrase secrète", + "add_passphrase_warning_text": "Entrez une phrase secrète si vous en avez utilisé un pour ce portefeuille dans le passé. Si vous entrez dans la mauvaise phrase de passe ou si vous n'avez pas utilisé de phrase de passe auparavant sur ce portefeuille, vous ne verrez aucun fonds ou historique existant.", "add_receiver": "Ajouter un autre bénéficiaire (optionnel)", "add_secret_code": "Ou ajoutez ce code secret à une application d'authentification", "add_tip": "Ajouter un pourboire", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "Exiger pour les envois vers des portefeuilles (wallets) internes", "require_for_sends_to_non_contacts": "Exiger pour les envois hors contacts", "require_pin_after": "Code PIN requis après", + "required_passphrase": "Phrase secrète", "rescan": "Analyser la blockchain", "resend_code": "Veuillez le renvoyer", "reset": "Réinitialiser", "reset_password": "Réinitialiser le mot de passe", + "restore": "Restaurer", "restore_active_seed": "Phrase secrète (seed) active", "restore_address": "Adresse", "restore_bitcoin_description_from_keys": "Restaurer votre portefeuille (wallet) d'après la chaîne WIF générée d'après vos clefs privées", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "Vous pouvez toujours revoir cette graine sous", "wallet_group_empty_state_text_one": "On dirait que vous n'avez pas de groupes de portefeuilles compatibles !\n\n Tap", "wallet_group_empty_state_text_two": "Ci-dessous pour en faire un nouveau.", + "wallet_has_passphrase": "Ce portefeuille a une phrase secrète", "wallet_keys": "Phrase secrète (seed)/Clefs du portefeuille (wallet)", "wallet_list_create_new_wallet": "Créer un Nouveau Portefeuille (Wallet)", "wallet_list_edit_group_name": "Modifier le nom du groupe", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 7629eec5f..9c138de7e 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Ƙara kuɗin da aka riga aka biya a katunan (har zuwa ${value})", "add_new_node": "Ƙara sabon node", "add_new_word": "Ƙara kalma sabuwa", + "add_passphrase": "Addara fasphrase", + "add_passphrase_warning_text": "Kawai shigar da kalmar wucewa idan kun yi amfani da ɗaya don wannan walat a baya. Idan ka shigar da kalmar wucewa ko ba a yi amfani da kalmar wucewa ba kafin a wannan waljin, ba za ka ga wani kudaden da ake da su ba ko tarihi.", "add_receiver": "Ƙara wani mai karɓa (na zaɓi)", "add_secret_code": "Ko, ƙara wannan lambar sirrin zuwa ƙa'idar mai tabbatarwa", "add_tip": "Ƙara Tukwici", @@ -623,10 +625,12 @@ "require_for_sends_to_internal_wallets": "Bukatar aika zuwa wallet na ciki", "require_for_sends_to_non_contacts": "Bukatar aika zuwa waɗanda ba lambobin sadarwa ba", "require_pin_after": "Bukatar PIN bayan", + "required_passphrase": "Mashiganya", "rescan": "Rescan", "resend_code": "Da fatan za a sake aika shi", "reset": "Sake saiti", "reset_password": "Sake saita kalmar wucewa", + "restore": "Sabunta", "restore_active_seed": "iri mai aiki", "restore_address": "Address", "restore_bitcoin_description_from_keys": "Dawo da kwalinku daga WIF string dake generate daga maɓallan sirri", @@ -1001,6 +1005,7 @@ "wallet_group_description_view_seed": "Koyaushe zaka iya duba wannan zuriya", "wallet_group_empty_state_text_one": "Kamar dai ba ku da wata ƙungiya matattara !\n\n Taɓa", "wallet_group_empty_state_text_two": "da ke ƙasa don yin sabo.", + "wallet_has_passphrase": "Wannan walat ɗin yana da kalmar wucewa", "wallet_keys": "Iri/maɓalli na walat", "wallet_list_create_new_wallet": "Ƙirƙiri Sabon Wallet", "wallet_list_edit_group_name": "Shirya sunan rukuni", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 73d168d47..0acb517d5 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "कार्ड में प्रीपेड धनराशि जोड़ें (${value} तक)", "add_new_node": "नया नोड जोड़ें", "add_new_word": "नया शब्द जोड़ें", + "add_passphrase": "पासफ़्रेज़ जोड़ें", + "add_passphrase_warning_text": "यदि आपने अतीत में इस बटुए के लिए एक का उपयोग किया है, तो केवल एक पासफ्रेज़ दर्ज करें। यदि आप गलत पासफ्रेज़ में प्रवेश करते हैं या इस वॉलेट पर पहले पासफ्रेज़ का उपयोग नहीं करते हैं, तो आप मौजूदा फंड या इतिहास में से कोई भी नहीं देखेंगे।", "add_receiver": "एक और रिसीवर जोड़ें (वैकल्पिक)", "add_secret_code": "या, इस गुप्त कोड को प्रमाणक ऐप में जोड़ें", "add_tip": "टिप जोड़ें", @@ -623,10 +625,12 @@ "require_for_sends_to_internal_wallets": "आंतरिक वॉलेट में भेजने की आवश्यकता है", "require_for_sends_to_non_contacts": "गैर-संपर्कों को भेजने की आवश्यकता", "require_pin_after": "इसके बाद पिन आवश्यक है", + "required_passphrase": "पदबंध", "rescan": "पुन: स्कैन", "resend_code": "कृपया इसे फिर से भेजें", "reset": "रीसेट", "reset_password": "पासवर्ड रीसेट करें", + "restore": "पुनर्स्थापित करना", "restore_active_seed": "सक्रिय बीज", "restore_address": "पता", "restore_bitcoin_description_from_keys": "अपने निजी कुंजी से उत्पन्न WIF स्ट्रिंग से अपने वॉलेट को पुनर्स्थापित करें", @@ -1001,6 +1005,7 @@ "wallet_group_description_view_seed": "आप हमेशा इस बीज को फिर से देख सकते हैं", "wallet_group_empty_state_text_one": "लगता है कि आपके पास कोई संगत बटुआ समूह नहीं है!\n\nनल", "wallet_group_empty_state_text_two": "नीचे एक नया बनाने के लिए।", + "wallet_has_passphrase": "इस बटुए में एक पासफ़्रेज़ है", "wallet_keys": "बटुआ बीज / चाबियाँ", "wallet_list_create_new_wallet": "नया बटुआ बनाएँ", "wallet_list_edit_group_name": "समूह का नाम संपादित करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 75aac2682..4657ef754 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Dodajte unaprijed uplaćena sredstva na kartice (do ${value})", "add_new_node": "Dodaj novi node", "add_new_word": "Dodaj novu riječ", + "add_passphrase": "Dodajte prolaznu frazu", + "add_passphrase_warning_text": "U prošlosti unesite samo prolaznu frazu ako ste je koristili za ovaj novčanik. Ako uđete u pogrešnu lozu ili niste prije koristili prolaznu frazu na ovom novčaniku, nećete vidjeti nijedno postojeće fondove ili povijest.", "add_receiver": "Dodajte drugi prijemnik (izborno)", "add_secret_code": "Ili dodajte ovaj tajni kod u aplikaciju za autentifikaciju", "add_tip": "Dodaj savjet", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "Zahtijeva za slanje u interne novčanike", "require_for_sends_to_non_contacts": "Zahtijeva za slanje nekontaktima", "require_pin_after": "Zahtijevaj PIN nakon", + "required_passphrase": "Prolazna fraza", "rescan": "Ponovno skeniranje", "resend_code": "Molimo da ga ponovno pošaljete", "reset": "Resetiraj", "reset_password": "Poništi lozinku", + "restore": "Vratiti", "restore_active_seed": "Aktivan pristupni izraz", "restore_address": "Adresa", "restore_bitcoin_description_from_keys": "Oporavi novčanik pomoću WIF niza generiranog iz vlastitih privatnih ključeva (keys)", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "Uvijek možete ponovo pogledati ovo sjeme ispod", "wallet_group_empty_state_text_one": "Izgleda da nemate nikakve kompatibilne grupe novčanika !\n\n", "wallet_group_empty_state_text_two": "Ispod da napravite novi.", + "wallet_has_passphrase": "Ovaj novčanik ima prolaznu frazu", "wallet_keys": "Pristupni izraz/ključ novčanika", "wallet_list_create_new_wallet": "Izradi novi novčanik", "wallet_list_edit_group_name": "Uredi naziv grupe", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 09585df8a..4ec2ae678 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Ավելացնել նախավճար քարտերի վրա (մինչև ${value})", "add_new_node": "Ավելացնել նոր հանգույց", "add_new_word": "Ավելացնել նոր բառ", + "add_passphrase": "Ավելացնել գաղտնաբառ", + "add_passphrase_warning_text": "Մուտքագրեք միայն գաղտնաբառ, եթե նախկինում այս դրամապանակի համար օգտագործեք մեկը: Եթե ​​մուտքագրեք սխալ գաղտնաբառ կամ այս դրամապանակում նախկինում չեք օգտագործել գաղտնաբառ, ապա առկա միջոցներից կամ պատմություն չեք տեսնի:", "add_receiver": "Ավելացնել ևս մեկ ստացող (ընտրովի)", "add_secret_code": "Կամ ավելացրեք այս գաղտնի կոդը վավերացնող հավելվածում", "add_tip": "Ավելացնել Թեյավճար", @@ -620,10 +622,12 @@ "require_for_sends_to_internal_wallets": "Պահանջվում է ներքին դրամապանակներ ուղարկելու համար", "require_for_sends_to_non_contacts": "Պահանջվում է ոչ կոնտակտ անձանց ուղարկելու համար", "require_pin_after": "Պահանջվում է PIN-ը հետո", + "required_passphrase": "Փոշի", "rescan": "Վերասկանավորել", "resend_code": "Խնդրում ենք կրկին ուղարկել", "reset": "Վերասահմանել", "reset_password": "Վերասահմանել գաղտնաբառը", + "restore": "Վերականգնել", "restore_active_seed": "Ակտիվ սերմ", "restore_address": "Հասցե", "restore_bitcoin_description_from_keys": "Վերականգնեք ձեր դրամապանակը ձեր գախտնի բանալիներից ստացված WIF տողից", @@ -997,6 +1001,7 @@ "wallet_group_description_view_seed": "Միշտ կարող եք կրկին դիտել այս սերմը ներքեւում", "wallet_group_empty_state_text_one": "Կարծես թե որեւէ համատեղելի դրամապանակի խմբեր չունեք:\n\nԹակել", "wallet_group_empty_state_text_two": "ներքեւում `նորը կազմելու համար:", + "wallet_has_passphrase": "Այս դրամապանակն ունի գաղտնաբառ", "wallet_keys": "Դրամապանակի սերմ/բանալիներ", "wallet_list_create_new_wallet": "Ստեղծել Նոր Դրամապանակ", "wallet_list_edit_group_name": "Խմբագրել խմբի անվանումը", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index a3e6abd1f..be1eb8c35 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Tambahkan dana pra-bayar ke kartu (hingga ${value})", "add_new_node": "Tambah node baru", "add_new_word": "Tambahkan kata baru", + "add_passphrase": "Tambahkan frasa sandi", + "add_passphrase_warning_text": "Hanya masukkan frasa sandi jika Anda telah menggunakan satu untuk dompet ini di masa lalu. Jika Anda memasukkan frasa sandi yang salah atau belum menggunakan frasa sandi sebelumnya di dompet ini, Anda tidak akan melihat dana atau sejarah yang ada.", "add_receiver": "Tambahkan penerima lain (opsional)", "add_secret_code": "Atau, tambahkan kode rahasia ini ke aplikasi autentikator", "add_tip": "Tambahkan Tip", @@ -623,10 +625,12 @@ "require_for_sends_to_internal_wallets": "Diperlukan untuk mengirim ke dompet internal", "require_for_sends_to_non_contacts": "Wajibkan untuk mengirim ke non-kontak", "require_pin_after": "Meminta PIN setelah", + "required_passphrase": "Frasa sandi", "rescan": "Pindai ulang", "resend_code": "Silakan kirim ulang", "reset": "Reset", "reset_password": "Atur Ulang Kata Sandi", + "restore": "Memulihkan", "restore_active_seed": "Seed aktif", "restore_address": "Alamat", "restore_bitcoin_description_from_keys": "Pulihkan dompet Anda dari string WIF yang dihasilkan dari private keys Anda", @@ -1002,6 +1006,7 @@ "wallet_group_description_view_seed": "Anda selalu dapat melihat benih ini lagi di bawah", "wallet_group_empty_state_text_one": "Sepertinya Anda tidak memiliki grup dompet yang kompatibel !\n\n tap", "wallet_group_empty_state_text_two": "di bawah ini untuk membuat yang baru.", + "wallet_has_passphrase": "Dompet ini memiliki frasa sandi", "wallet_keys": "Seed/kunci dompet", "wallet_list_create_new_wallet": "Buat Dompet Baru", "wallet_list_edit_group_name": "Edit Nama Grup", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index b558501d4..427bf09cb 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Aggiungi fondi prepagati alle carte (fino a ${value})", "add_new_node": "Aggiungi nuovo nodo", "add_new_word": "Aggiungi nuova parola", + "add_passphrase": "Aggiungi passphrase", + "add_passphrase_warning_text": "Inserisci una passphrase solo se ne hai usato uno per questo portafoglio in passato. Se si inserisce la passphrase sbagliata o non hai prima utilizzato una passphrase su questo portafoglio, non vedrai nessuno dei fondi o della storia esistenti.", "add_receiver": "Aggiungi un altro ricevitore (opzionale)", "add_secret_code": "Oppure aggiungi questo codice segreto a un'app di autenticazione", "add_tip": "Aggiungi suggerimento", @@ -622,10 +624,12 @@ "require_for_sends_to_internal_wallets": "Richiedi per invii a portafogli interni", "require_for_sends_to_non_contacts": "Richiedi per invii a non contatti", "require_pin_after": "Richiedi PIN dopo", + "required_passphrase": "Passphrase", "rescan": "Scansiona di nuovo", "resend_code": "Per favore, invialo nuovamente", "reset": "Ripristina", "reset_password": "Reimposta password", + "restore": "Ripristinare", "restore_active_seed": "Seme attivo", "restore_address": "Indirizzo", "restore_bitcoin_description_from_keys": "Recupera il tuo portafoglio da una stringa WIF generata dalle tue chiavi private", @@ -1001,6 +1005,7 @@ "wallet_group_description_view_seed": "Puoi sempre visualizzare di nuovo questo seme sotto", "wallet_group_empty_state_text_one": "Sembra che tu non abbia alcun gruppo di portafoglio compatibile!\n\nPremi", "wallet_group_empty_state_text_two": "Di seguito per crearne uno nuovo.", + "wallet_has_passphrase": "Questo portafoglio ha una passphrase", "wallet_keys": "Seme Portafoglio /chiavi", "wallet_list_create_new_wallet": "Crea Nuovo Portafoglio", "wallet_list_edit_group_name": "Modifica nome del gruppo", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 933fb706a..c6395572e 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "プリペイド資金をカードに追加します(最大 ${value})", "add_new_node": "新しいノードを追加", "add_new_word": "新しい単語を追加", + "add_passphrase": "パスフレーズを追加します", + "add_passphrase_warning_text": "過去にこのウォレットに使用した場合にのみ、パスフレーズを入力してください。間違ったパスフレーズに入ったり、このウォレットでパスフレーズを使用したことがない場合、既存の資金や歴史はありません。", "add_receiver": "別のレシーバーを追加します(オプション)", "add_secret_code": "または、このシークレット コードを認証アプリに追加します", "add_tip": "ヒントを追加", @@ -622,10 +624,12 @@ "require_for_sends_to_internal_wallets": "内部ウォレットへの送信に必須", "require_for_sends_to_non_contacts": "非連絡先への送信に必須", "require_pin_after": "後に PIN が必要", + "required_passphrase": "パスフレーズ", "rescan": "再スキャン", "resend_code": "再送してください", "reset": "リセットする", "reset_password": "パスワードのリセット", + "restore": "復元する", "restore_active_seed": "アクティブシード", "restore_address": "住所", "restore_bitcoin_description_from_keys": "秘密鍵から生成されたWIF文字列からウォレットを復元します", @@ -1000,6 +1004,7 @@ "wallet_group_description_view_seed": "いつでもこの種を再び見ることができます", "wallet_group_empty_state_text_one": "互換性のあるウォレットグループがないようです!\n\nタップ", "wallet_group_empty_state_text_two": "以下に新しいものを作るために。", + "wallet_has_passphrase": "このウォレットにはパスフレーズがあります", "wallet_keys": "ウォレットシード/キー", "wallet_list_create_new_wallet": "新しいウォレットを作成", "wallet_list_edit_group_name": "グループ名を編集します", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 582ec8f45..6dffa5db4 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "카드에 선불 금액 추가(최대 ${value})", "add_new_node": "새 노드 추가", "add_new_word": "새로운 단어 추가", + "add_passphrase": "암호를 추가하십시오", + "add_passphrase_warning_text": "과거 에이 지갑에 사용한 경우에만 암호를 입력하십시오. 이 지갑에서 잘못된 암호를 입력하거나 암호를 사용하지 않은 경우 기존 자금이나 이력이 보이지 않을 것입니다.", "add_receiver": "다른 수신기 추가(선택 사항)", "add_secret_code": "또는 이 비밀 코드를 인증 앱에 추가하세요.", "add_tip": "팁 추가", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "내부 지갑으로 보내는 데 필요", "require_for_sends_to_non_contacts": "비접촉자에게 보내는 데 필요", "require_pin_after": "다음 이후에 PIN 필요", + "required_passphrase": "암호", "rescan": "재검색", "resend_code": "다시 보내주세요", "reset": "다시 놓기", "reset_password": "비밀번호 재설정", + "restore": "복원하다", "restore_active_seed": "활성 종자", "restore_address": "주소", "restore_bitcoin_description_from_keys": "개인 키에서 생성 된 WIF 문자열에서 지갑 복원", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "이 씨앗을 언제든지 다시 볼 수 있습니다", "wallet_group_empty_state_text_one": "호환 지갑 그룹이없는 것 같습니다 !\n\n TAP", "wallet_group_empty_state_text_two": "아래에서 새로운 것을 만들기 위해.", + "wallet_has_passphrase": "이 지갑에는 암호가 있습니다", "wallet_keys": "지갑 시드 / 키", "wallet_list_create_new_wallet": "새 월렛 만들기", "wallet_list_edit_group_name": "그룹 이름 편집", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 69eb337ee..b9b3ad37e 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "ကတ်များသို့ ကြိုတင်ငွေပေးငွေများ ထည့်ပါ (${value} အထိ)", "add_new_node": "နှာခေါင်း အသစ်ထည့်ပါ။", "add_new_word": "စကားလုံးအသစ်ထည့်ပါ။", + "add_passphrase": "passphrase ထည့်ပါ", + "add_passphrase_warning_text": "အကယ်. သင်သည်ယခင်ကဤပိုက်ဆံအိတ်အတွက်တစ်ခုသုံးခဲ့လျှင် passphrase တစ်ခုသာရိုက်ထည့်ပါ။ အကယ်. သင်သည်မှားယွင်းသော passphrase ကို 0 င်ရောက်ခြင်းသို့မဟုတ်ဤပိုက်ဆံအိတ်ပေါ်တွင် passphrase မသုံးပါကလက်ရှိရန်ပုံငွေများသို့မဟုတ်သမိုင်းကိုသင်မတွေ့ရပါ။", "add_receiver": "အခြားလက်ခံသူ ထည့်ပါ (ချန်လှပ်ထားနိုင်သည်)", "add_secret_code": "သို့မဟုတ် ဤလျှို့ဝှက်ကုဒ်ကို အထောက်အထားစိစစ်ခြင်းအက်ပ်တစ်ခုသို့ ထည့်ပါ။", "add_tip": "အကြံပြုချက်ထည့်ပါ။", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "အတွင်းပိုင်း ပိုက်ဆံအိတ်များသို့ ပေးပို့ရန် လိုအပ်သည်။", "require_for_sends_to_non_contacts": "အဆက်အသွယ်မရှိသူများထံ ပေးပို့ရန် လိုအပ်သည်။", "require_pin_after": "ပြီးနောက် PIN လိုအပ်ပါသည်။", + "required_passphrase": "စကားဝှက်PPRase", "rescan": "ပြန်စကင်န်လုပ်ပါ။", "resend_code": "ကျေးဇူးပြု၍ ပြန်ပို့ပါ။", "reset": "ပြန်လည်သတ်မှတ်ပါ။", "reset_password": "လျှို့ဝှတ်နံပါတ်အားမူလအတိုင်းပြန်လုပ်သည်", + "restore": "ပြန်လည်တည်ထောင်", "restore_active_seed": "တက်ကြွသောအစေ့", "restore_address": "လိပ်စာ", "restore_bitcoin_description_from_keys": "သင့်ကိုယ်ပိုင်သော့များမှ ထုတ်လုပ်ထားသော WIF စာကြောင်းမှ သင့်ပိုက်ဆံအိတ်ကို ပြန်လည်ရယူပါ။", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "သင်သည်ဤမျိုးစေ့ကိုနောက်တဖန်ရှုမြင်နိုင်သည်", "wallet_group_empty_state_text_one": "သင့်တွင်သဟဇာတဖြစ်သောပိုက်ဆံအိတ်အုပ်စုများမရှိပါ။ !\n\n ကိုအသာပုတ်ပါ", "wallet_group_empty_state_text_two": "အသစ်တစ်ခုကိုတစ်ခုလုပ်ဖို့အောက်တွင်ဖော်ပြထားသော။", + "wallet_has_passphrase": "ဒီပိုက်ဆံအိတ်က passphrase ရှိတယ်", "wallet_keys": "ပိုက်ဆံအိတ် အစေ့/သော့များ", "wallet_list_create_new_wallet": "Wallet အသစ်ဖန်တီးပါ။", "wallet_list_edit_group_name": "အုပ်စုအမည်ကိုတည်းဖြတ်ပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index da508e880..7c2881a39 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Voeg prepaid tegoed toe aan de kaarten (tot ${value})", "add_new_node": "Voeg een nieuw knooppunt toe", "add_new_word": "Nieuw woord toevoegen", + "add_passphrase": "Voeg wachtwoordzin toe", + "add_passphrase_warning_text": "Voer alleen een wachtwoordzin in als u er in het verleden een voor deze portemonnee hebt gebruikt. Als u de verkeerde wachtwoordzin invoert of nog niet eerder op deze portemonnee een wachtwoordzin hebt gebruikt, ziet u geen bestaande fondsen of geschiedenis.", "add_receiver": "Nog een ontvanger toevoegen (optioneel)", "add_secret_code": "Of voeg deze geheime code toe aan een authenticator-app", "add_tip": "Tip toevoegen", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "Vereist voor verzendingen naar interne portefeuilles", "require_for_sends_to_non_contacts": "Vereist voor verzendingen naar niet-contacten", "require_pin_after": "Pincode vereist na", + "required_passphrase": "Wachtwoordzin", "rescan": "Opnieuw scannen", "resend_code": "Stuur het alstublieft opnieuw", "reset": "Reset", "reset_password": "Wachtwoord resetten", + "restore": "Herstellen", "restore_active_seed": "Actief zaad", "restore_address": "Adres", "restore_bitcoin_description_from_keys": "Herstel uw portemonnee van de gegenereerde WIF-string van uw privésleutels", @@ -1000,6 +1004,7 @@ "wallet_group_description_view_seed": "Je kunt dit zaad altijd opnieuw bekijken", "wallet_group_empty_state_text_one": "Het lijkt erop dat je geen compatibele portemonnee -groepen hebt !\n\n TAP", "wallet_group_empty_state_text_two": "hieronder om een ​​nieuwe te maken.", + "wallet_has_passphrase": "Deze portemonnee heeft een wachtwoordzin", "wallet_keys": "Portemonnee zaad/sleutels", "wallet_list_create_new_wallet": "Maak een nieuwe portemonnee", "wallet_list_edit_group_name": "Groepsnaam bewerken", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 12765881c..3feece87f 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Dodaj przedpłacone środki do kart (do ${value})", "add_new_node": "Dodaj nowy węzeł", "add_new_word": "Dodaj nowe słowo", + "add_passphrase": "Dodaj hasło", + "add_passphrase_warning_text": "Wprowadź hasło tylko wtedy, gdy w przeszłości używałeś jednego do tego portfela. Jeśli wejdziesz do niewłaściwej hasła lub nie użyjesz wcześniejszego pensjonatu na tym portfelu, nie zobaczysz żadnego z istniejących funduszy ani historii.", "add_receiver": "Dodaj kolejnego odbiorcę (opcjonalnie)", "add_secret_code": "Możesz też dodać ten tajny kod do aplikacji uwierzytelniającej", "add_tip": "Dodaj wskazówkę", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "Wymagaj wysyłania do portfeli wewnętrznych", "require_for_sends_to_non_contacts": "Wymagaj wysyłania do osób niekontaktowych", "require_pin_after": "Wymagaj kodu PIN po", + "required_passphrase": "Fraza", "rescan": "Skanuj ponownie", "resend_code": "Wyślij go ponownie", "reset": "Wyczyść", "reset_password": "Zresetuj hasło", + "restore": "Przywrócić", "restore_active_seed": "Aktywne seedy", "restore_address": "Adres", "restore_bitcoin_description_from_keys": "Przywróć swój portfel z klucza prywatnego", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "Zawsze możesz ponownie zobaczyć to ziarno pod", "wallet_group_empty_state_text_one": "Wygląda na to, że nie masz żadnych kompatybilnych grup portfeli !\n\n Tap", "wallet_group_empty_state_text_two": "poniżej, aby zrobić nowy.", + "wallet_has_passphrase": "Ten portfel ma panie", "wallet_keys": "Klucze portfela", "wallet_list_create_new_wallet": "Utwórz nowy portfel", "wallet_list_edit_group_name": "Edytuj nazwę grupy", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index c91bb04a0..f20a39e2d 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Adicionar fundos pré-pagos aos cartões (até ${value})", "add_new_node": "Adicionar novo nó", "add_new_word": "Adicionar nova palavra", + "add_passphrase": "Adicione a senha", + "add_passphrase_warning_text": "Digite apenas uma senha se você usou uma para esta carteira no passado. Se você inserir a senha errada ou não já usou uma senha antes nesta carteira, não verá nenhum dos fundos ou histórico existentes.", "add_receiver": "Adicione outro receptor (opcional)", "add_secret_code": "Ou adicione este código secreto a um aplicativo autenticador", "add_tip": "Adicionar Dica", @@ -623,10 +625,12 @@ "require_for_sends_to_internal_wallets": "Exigir envios para carteiras internas", "require_for_sends_to_non_contacts": "Exigir para envios para não-contatos", "require_pin_after": "Exigir PIN após", + "required_passphrase": "Senha", "rescan": "Reescanear", "resend_code": "Por favor, reenvie", "reset": "Limpar", "reset_password": "Redefinir senha", + "restore": "Restaurar", "restore_active_seed": "Semente ativa", "restore_address": "Endereço", "restore_bitcoin_description_from_keys": "Restaure sua carteira a partir da string WIF gerada de suas chaves privadas", @@ -1002,6 +1006,7 @@ "wallet_group_description_view_seed": "Você sempre pode ver esta semente novamente em", "wallet_group_empty_state_text_one": "Parece que você não tem nenhum grupo de carteira compatível !\n\n Toque", "wallet_group_empty_state_text_two": "abaixo para fazer um novo.", + "wallet_has_passphrase": "Esta carteira tem uma senha", "wallet_keys": "Semente/chaves da carteira", "wallet_list_create_new_wallet": "Criar nova carteira", "wallet_list_edit_group_name": "Editar o nome do grupo", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 37750c3ce..6559c8d1d 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Добавить предоплаченные средства на карты (до ${value})", "add_new_node": "Добавить новую ноду", "add_new_word": "Добавить новое слово", + "add_passphrase": "Добавить пасфраз", + "add_passphrase_warning_text": "Введите фразу только в том случае, если вы использовали один для этого кошелька в прошлом. Если вы введете неверную пассисную фразу или не использовали пасфразу до этого кошелька, вы не увидите ни одного существующего фонда или истории.", "add_receiver": "Добавить получателя (необязательно)", "add_secret_code": "Или добавьте этот секретный код в приложение для аутентификации.", "add_tip": "Добавить подсказку", @@ -622,10 +624,12 @@ "require_for_sends_to_internal_wallets": "Требовать отправки на внутренние кошельки", "require_for_sends_to_non_contacts": "Требовать для отправки не контактам", "require_pin_after": "Требовать ПИН после", + "required_passphrase": "Пасфраза", "rescan": "Пересканировать", "resend_code": "Пожалуйста, отправьте еще раз", "reset": "Сброс", "reset_password": "Сбросить пароль", + "restore": "Восстановить", "restore_active_seed": "Активная мнемоническая фраза", "restore_address": "Адрес", "restore_bitcoin_description_from_keys": "Вы можете восстановить кошелёк с помощью WIF", @@ -1000,6 +1004,7 @@ "wallet_group_description_view_seed": "Вы всегда можете просматривать это семя снова под", "wallet_group_empty_state_text_one": "Похоже, у вас нет никаких совместимых групп кошелька !\n\n tap", "wallet_group_empty_state_text_two": "ниже, чтобы сделать новый.", + "wallet_has_passphrase": "Этот кошелек имеет фразу", "wallet_keys": "Мнемоническая фраза/ключи кошелька", "wallet_list_create_new_wallet": "Создать новый кошелёк", "wallet_list_edit_group_name": "Редактировать название группы", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index cbc007035..b42fc600b 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "เพิ่มเงินสำรองไว้บนบัตร (ถึง ${value})", "add_new_node": "เพิ่มโหนดใหม่", "add_new_word": "เพิ่มคำใหม่", + "add_passphrase": "เพิ่มวลีรหัสผ่าน", + "add_passphrase_warning_text": "ป้อนวลีรหัสผ่านเท่านั้นหากคุณเคยใช้สำหรับกระเป๋าเงินนี้ในอดีต หากคุณป้อนวลีรหัสผ่านที่ไม่ถูกต้องหรือไม่เคยใช้ข้อความรหัสผ่านมาก่อนในกระเป๋าเงินนี้คุณจะไม่เห็นเงินทุนหรือประวัติใด ๆ ที่มีอยู่", "add_receiver": "เพิ่มผู้รับอื่น ๆ (ตัวเลือก)", "add_secret_code": "หรือเพิ่มรหัสลับนี้ลงในแอปตรวจสอบความถูกต้อง", "add_tip": "เพิ่มคำแนะนำ", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "จำเป็นต้องส่งไปยังกระเป๋าเงินภายใน", "require_for_sends_to_non_contacts": "จำเป็นต้องส่งไปยังผู้ที่ไม่ได้ติดต่อ", "require_pin_after": "ต้องการ PIN หลังจาก", + "required_passphrase": "วรรณะ", "rescan": "สแกนใหม่", "resend_code": "โปรดส่งอีกครั้ง", "reset": "รีเซ็ต", "reset_password": "รีเซ็ตรหัสผ่าน", + "restore": "คืนค่า", "restore_active_seed": "ซีดที่ใช้งานอยู่", "restore_address": "ที่อยู่", "restore_bitcoin_description_from_keys": "กู้กระเป๋าของคุณจากสตริง WIF ที่สร้างขึ้นจากคีย์ส่วนตัวของคุณ", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "คุณสามารถดูเมล็ดพันธุ์นี้ได้อีกครั้งภายใต้", "wallet_group_empty_state_text_one": "ดูเหมือนว่าคุณจะไม่มีกลุ่มกระเป๋าเงินที่เข้ากันได้ !\n\n แตะ", "wallet_group_empty_state_text_two": "ด้านล่างเพื่อสร้างใหม่", + "wallet_has_passphrase": "กระเป๋าเงินนี้มีข้อความรหัสผ่าน", "wallet_keys": "ซีดของกระเป๋า/คีย์", "wallet_list_create_new_wallet": "สร้างกระเป๋าใหม่", "wallet_list_edit_group_name": "แก้ไขชื่อกลุ่ม", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 356eae8c5..1a881cbba 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Magdagdag ng mga prepaid na pondo sa card (hanggang sa ${value})", "add_new_node": "Magdagdag ng bagong node", "add_new_word": "Magdagdag ng bagong salita", + "add_passphrase": "Magdagdag ng passphrase", + "add_passphrase_warning_text": "Magpasok lamang ng isang passphrase kung ginamit mo ang isa para sa pitaka na ito sa nakaraan. Kung nagpasok ka ng maling passphrase o hindi pa gumamit ng isang passphrase bago sa pitaka na ito, hindi mo makikita ang alinman sa umiiral na pondo o kasaysayan.", "add_receiver": "Magdagdag ng isa pang tatanggap (opsyonal)", "add_secret_code": "O, idagdag ang sikretong code na ito sa isang authenticator app", "add_tip": "Magdagdag ng Tip", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "Nangangailangan para sa pagpapadala sa mga panloob na wallet", "require_for_sends_to_non_contacts": "Nangangailangan para sa pagpapadala sa mga hindi contact", "require_pin_after": "Nangangailangan ng PIN pagkatapos", + "required_passphrase": "Passphrase", "rescan": "Muling i-scan", "resend_code": "Mangyaring ipadala ito muli", "reset": "I-reset", "reset_password": "I-reset ang password", + "restore": "Ibalik", "restore_active_seed": "Aktibong seed", "restore_address": "Address", "restore_bitcoin_description_from_keys": "Ibalik ang iyong wallet mula sa nabuong WIF string mula sa iyong mga private key", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "Maaari mong palaging tingnan ang binhi na ito sa ilalim", "wallet_group_empty_state_text_one": "Mukhang wala kang anumang mga katugmang pangkat ng pitaka!\n\ntap", "wallet_group_empty_state_text_two": "sa ibaba upang gumawa ng bago.", + "wallet_has_passphrase": "Ang pitaka na ito ay may isang passphrase", "wallet_keys": "Wallet seed/keys", "wallet_list_create_new_wallet": "Lumikha ng bagong wallet", "wallet_list_edit_group_name": "I -edit ang Pangalan ng Grupo", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 5265e5f4e..53ef3574b 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Ön ödemeli kartlara para ekle (En fazla yüklenebilir tutar: ${value})", "add_new_node": "Yeni düğüm ekle", "add_new_word": "Yeni kelime ekle", + "add_passphrase": "Parola ekle", + "add_passphrase_warning_text": "Sadece geçmişte bu cüzdan için bir tane kullandıysanız bir parola girin. Yanlış parola girerseniz veya daha önce bu cüzdanda bir parola kullanmadıysanız, mevcut fonlardan veya geçmişi görmezsiniz.", "add_receiver": "Başka bir alıcı ekle (isteğe bağlı)", "add_secret_code": "Veya bu gizli kodu bir kimlik doğrulama uygulamasına ekleyin", "add_tip": "Bahşiş Ekle", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "Dahili cüzdanlara yapılan gönderimler için gereklilik", "require_for_sends_to_non_contacts": "Kişi olmayan kişilere göndermeler için gerekli kıl", "require_pin_after": "Şu kadar süre sonra PIN iste", + "required_passphrase": "Parola", "rescan": "Yeniden Tara", "resend_code": "Lütfen tekrar gönder", "reset": "Sıfırla", "reset_password": "Parolamı sıfırla", + "restore": "Eski haline getirmek", "restore_active_seed": "Tohumu aktifleştir", "restore_address": "Adres", "restore_bitcoin_description_from_keys": "Cüzdanını, oluşturulan WIF dizesinden veya özel anahtarlarından geri yükle", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "Bu tohumu her zaman tekrar görebilirsiniz", "wallet_group_empty_state_text_one": "Herhangi bir uyumlu cüzdan grubunuz yok gibi görünüyor !\n\n TAP", "wallet_group_empty_state_text_two": "Yeni bir tane yapmak için aşağıda.", + "wallet_has_passphrase": "Bu cüzdanın bir parola var", "wallet_keys": "Cüzdan tohumu/anahtarları", "wallet_list_create_new_wallet": "Yeni Cüzdan Oluştur", "wallet_list_edit_group_name": "Grup Adını Düzenle", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 99c94854b..1123bf1b8 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Додайте передплачені кошти на картки (до ${value})", "add_new_node": "Додати новий вузол", "add_new_word": "Добавити нове слово", + "add_passphrase": "Додати фразу", + "add_passphrase_warning_text": "Введіть парольну фразу, якщо ви використовували її для цього гаманця в минулому. Якщо ви вводите неправильну пасфразу або раніше не використовували парольну фразу на цьому гаманці, ви не побачите жодного з існуючих коштів чи історії.", "add_receiver": "Додати одержувача (необов'язково)", "add_secret_code": "Або додайте цей секретний код до програми автентифікації", "add_tip": "Додати підказку", @@ -622,10 +624,12 @@ "require_for_sends_to_internal_wallets": "Вимагати надсилання на внутрішні гаманці", "require_for_sends_to_non_contacts": "Вимагати для надсилання неконтактним особам", "require_pin_after": "Вимагати PIN після", + "required_passphrase": "Пропуск", "rescan": "Пересканувати", "resend_code": "Будь ласка, надішліть його повторно", "reset": "Скинути", "reset_password": "Скинути пароль", + "restore": "Відновити", "restore_active_seed": "Активна мнемонічна фраза", "restore_address": "Адреса", "restore_bitcoin_description_from_keys": "Ви можете відновити гаманець за допомогою WIF", @@ -1000,6 +1004,7 @@ "wallet_group_description_view_seed": "Ви завжди можете переглянути це насіння ще раз під", "wallet_group_empty_state_text_one": "Схоже, у вас немає сумісних груп гаманця !\n\n Торкніться", "wallet_group_empty_state_text_two": "нижче, щоб зробити новий.", + "wallet_has_passphrase": "Цей гаманець має фразу", "wallet_keys": "Мнемонічна фраза/ключі гаманця", "wallet_list_create_new_wallet": "Створити новий гаманець", "wallet_list_edit_group_name": "Назва групи редагування", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 152cd7d11..3c4dabda7 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "کارڈز میں پری پیڈ فنڈز شامل کریں (${value} تک)", "add_new_node": "نیا نوڈ شامل کریں۔", "add_new_word": "نیا لفظ شامل کریں۔", + "add_passphrase": "پاسفریز شامل کریں", + "add_passphrase_warning_text": "صرف ایک پاسفریز درج کریں اگر آپ نے ماضی میں اس پرس کے لئے ایک استعمال کیا ہو۔ اگر آپ غلط پاسفریس میں داخل ہوتے ہیں یا اس پرس سے پہلے پاس فیز کا استعمال نہیں کرتے ہیں تو ، آپ کو موجودہ فنڈز یا تاریخ میں سے کوئی بھی نظر نہیں آئے گا۔", "add_receiver": "دوسرا وصول کنندہ شامل کریں (اختیاری)", "add_secret_code": " ۔ﮟﯾﺮﮐ ﻞﻣﺎﺷ ﮟﯿﻣ ﭗﯾﺍ ﮦﺪﻨﻨﮐ ﻖﯾﺪﺼﺗ ﻮﮐ ﮈﻮﮐ ﮧﯿﻔﺧ ﺱﺍ ،ﺎﯾ", "add_tip": "ٹپ شامل کریں۔", @@ -623,10 +625,12 @@ "require_for_sends_to_internal_wallets": "اندرونی بٹوے پر بھیجنے کے لیے درکار ہے۔", "require_for_sends_to_non_contacts": "غیر رابطوں کو بھیجنے کی ضرورت ہے۔", "require_pin_after": "اس کے بعد PIN کی ضرورت ہے۔", + "required_passphrase": "پاسفریز", "rescan": "دوبارہ اسکین کریں۔", "resend_code": "براہ کرم اسے دوبارہ بھیجیں۔", "reset": "دوبارہ ترتیب دیں۔", "reset_password": "پاس ورڈ ری سیٹ", + "restore": "بحال کریں", "restore_active_seed": "فعال بیج", "restore_address": "پتہ", "restore_bitcoin_description_from_keys": "اپنی نجی کلیدوں سے تیار کردہ WIF سٹرنگ سے اپنے بٹوے کو بحال کریں۔", @@ -1001,6 +1005,7 @@ "wallet_group_description_view_seed": "آپ ہمیشہ اس بیج کو دوبارہ دیکھ سکتے ہیں", "wallet_group_empty_state_text_one": "ایسا لگتا ہے کہ آپ کے پاس کوئی مطابقت پذیر والیٹ گروپس نہیں ہیں !\n\n نل", "wallet_group_empty_state_text_two": "ایک نیا بنانے کے لئے ذیل میں.", + "wallet_has_passphrase": "اس پرس میں پاسفریس ہے", "wallet_keys": "بٹوے کے بیج / چابیاں", "wallet_list_create_new_wallet": "نیا والیٹ بنائیں", "wallet_list_edit_group_name": "گروپ کے نام میں ترمیم کریں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 56a9305bf..61bb0e5d9 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Thêm tiền trả trước vào thẻ (tối đa ${value})", "add_new_node": "Thêm nút mới", "add_new_word": "Thêm từ mới", + "add_passphrase": "Thêm cụm mật khẩu", + "add_passphrase_warning_text": "Chỉ nhập một cụm mật khẩu nếu bạn đã sử dụng một cái cho ví này trong quá khứ. Nếu bạn nhập sai cụm từ hoặc chưa sử dụng cụm mật khẩu trước đây trên ví này, bạn sẽ không thấy bất kỳ quỹ hoặc lịch sử hiện có nào.", "add_receiver": "Thêm người nhận khác (tùy chọn)", "add_secret_code": "Hoặc, thêm mã bí mật này vào ứng dụng xác thực", "add_tip": "Thêm tiền boa", @@ -619,10 +621,12 @@ "require_for_sends_to_internal_wallets": "Yêu cầu khi gửi đến ví nội bộ", "require_for_sends_to_non_contacts": "Yêu cầu khi gửi đến người không phải danh bạ", "require_pin_after": "Yêu cầu PIN sau", + "required_passphrase": "Cụm cụm", "rescan": "Quét lại", "resend_code": "Vui lòng gửi lại", "reset": "Đặt lại", "reset_password": "Đặt lại mật khẩu", + "restore": "Khôi phục", "restore_active_seed": "Hạt giống hoạt động", "restore_address": "Địa chỉ", "restore_bitcoin_description_from_keys": "Khôi phục ví của bạn từ chuỗi WIF được tạo từ khóa riêng của bạn", @@ -996,6 +1000,7 @@ "wallet_group_description_view_seed": "Bạn luôn có thể xem lại hạt giống này dưới", "wallet_group_empty_state_text_one": "Có vẻ như bạn không có bất kỳ nhóm ví tương thích nào !\n\n Tap", "wallet_group_empty_state_text_two": "Dưới đây để làm một cái mới.", + "wallet_has_passphrase": "Ví này có một cụm từ", "wallet_keys": "Hạt giống/khóa ví", "wallet_list_create_new_wallet": "Tạo ví mới", "wallet_list_edit_group_name": "Chỉnh sửa tên nhóm", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index c17f07eb6..6e9ebd16e 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "Ẹ fikún owó sí àwọn káàdì (kò tóbi ju ${value})", "add_new_node": "Fi apẹka kún", "add_new_word": "Fikún ọ̀rọ̀ títun", + "add_passphrase": "Ṣafikun Kọwe", + "add_passphrase_warning_text": "Tẹ ni iwe ikawe nikan ti o ba ti lo ọkan fun apamọwọ yii ni igba atijọ. Ti o ba tẹ iwe ọrọ kukuru ti ko tọ tabi ko lo iwe kukuru ṣaaju lori apamọwọ yii, iwọ kii yoo wo eyikeyi awọn owo tabi itan-akọọlẹ.", "add_receiver": "Fikún àdírẹ́sì mìíràn (ìyàn nìyí)", "add_secret_code": "Tabi, ṣafikun koodu aṣiri yii si ohun elo onijeri kan", "add_tip": "Fún owó àfikún", @@ -622,10 +624,12 @@ "require_for_sends_to_internal_wallets": "Beere fun fifiranṣẹ si awọn apamọwọ inu", "require_for_sends_to_non_contacts": "Beere fun fifiranṣẹ si awọn ti kii ṣe awọn olubasọrọ", "require_pin_after": "Ẹ nílò òǹkà ìdánimọ̀ àdáni láàárín", + "required_passphrase": "Kukurukọni", "rescan": "Tún Wá", "resend_code": "Ẹ jọ̀wọ́ tún un ránṣé", "reset": "Tún ṣe", "reset_password": "Tún ọ̀rọ̀ aṣínà ṣe", + "restore": "Mu pada", "restore_active_seed": "Hóró lọ́wọ́", "restore_address": "Àdírẹ́sì", "restore_bitcoin_description_from_keys": "Mú àpamọ́wọ́ yín padà láti ọ̀rọ̀ WIF t'á ti dá láti kọ́kọ́rọ́ àdáni yín", @@ -1000,6 +1004,7 @@ "wallet_group_description_view_seed": "O le nigbagbogbo wo irugbin yii lẹẹkansi labẹ", "wallet_group_empty_state_text_one": "O dabi pe o ko ni eyikeyi awọn ẹgbẹ ti o ni ibamu!\n\ntẹ ni kia kia", "wallet_group_empty_state_text_two": "ni isalẹ lati ṣe ọkan titun.", + "wallet_has_passphrase": "Apamọwọ yii ni iwe kukuru kan", "wallet_keys": "Hóró/kọ́kọ́rọ́ àpamọ́wọ́", "wallet_list_create_new_wallet": "Ṣe àpamọ́wọ́ títun", "wallet_list_edit_group_name": "Ṣatunṣe Orukọ Ẹgbẹ", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index d0bbe9c08..a9b3dac58 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -15,6 +15,8 @@ "add_fund_to_card": "向卡中添加预付资金(最多 ${value})", "add_new_node": "添加新节点", "add_new_word": "添加新词", + "add_passphrase": "添加密码", + "add_passphrase_warning_text": "仅当您过去曾经为此钱包使用一个时,才输入密码。如果您在此钱包上输入错误的密码短语或在此钱包上之前没有使用过密码,则不会看到任何现有的资金或历史记录。", "add_receiver": "添加另一個接收器(可選)", "add_secret_code": "或者,将此密码添加到身份验证器应用程序中", "add_tip": "添加提示", @@ -621,10 +623,12 @@ "require_for_sends_to_internal_wallets": "需要发送到内部钱包", "require_for_sends_to_non_contacts": "需要发送给非联系人", "require_pin_after": "之后需要 PIN", + "required_passphrase": "密码", "rescan": "重新扫描", "resend_code": "请重新发送", "reset": "重置", "reset_password": "重置密码", + "restore": "恢复", "restore_active_seed": "活动种子", "restore_address": "地址", "restore_bitcoin_description_from_keys": "从私钥中生成的WIF字符串恢复您钱包", @@ -999,6 +1003,7 @@ "wallet_group_description_view_seed": "您可以随时再次在下面查看此种子", "wallet_group_empty_state_text_one": "看起来您没有任何兼容的钱包组!\n\n tap", "wallet_group_empty_state_text_two": "下面是一个新的。", + "wallet_has_passphrase": "这个钱包有一个密码", "wallet_keys": "钱包种子/密钥", "wallet_list_create_new_wallet": "创建新钱包", "wallet_list_edit_group_name": "编辑组名称", From a2294c4a061c40194223600a12aabacca3b15bc4 Mon Sep 17 00:00:00 2001 From: cyan Date: Fri, 2 May 2025 14:30:39 +0200 Subject: [PATCH 11/90] fix(cw_monero): prevent monero wallet from breaking during rename (#2214) * fix(cw_monero): prevent monero wallet from breaking during rename * update to cleaned up monero.dart * fix: transaction screen not refreshing in monero * fix: wallets not opening until app restart after rename. * fix(cw_decred): wallet renaming throwing * fix: transaction not being shown after sending until 1st confirmation * fix(cw_monero): loop safeguard * fix: don't await wallet.fetchTransactions --- cw_decred/lib/wallet.dart | 21 +- cw_decred/lib/wallet_service.dart | 4 + cw_monero/lib/api/account_list.dart | 67 ++-- cw_monero/lib/api/coins_info.dart | 19 +- cw_monero/lib/api/subaddress_list.dart | 68 ++-- cw_monero/lib/api/transaction_history.dart | 193 +++++----- cw_monero/lib/api/wallet.dart | 157 ++++---- cw_monero/lib/api/wallet_manager.dart | 339 ++++++------------ cw_monero/lib/ledger.dart | 16 +- cw_monero/lib/monero_account_list.dart | 28 +- cw_monero/lib/monero_unspent.dart | 2 +- cw_monero/lib/monero_wallet.dart | 171 +++++---- cw_monero/lib/monero_wallet_service.dart | 57 +-- cw_monero/pubspec.lock | 4 +- cw_monero/pubspec.yaml | 2 +- cw_wownero/pubspec.lock | 4 +- cw_wownero/pubspec.yaml | 2 +- cw_zano/pubspec.lock | 4 +- cw_zano/pubspec.yaml | 2 +- ios/Podfile.lock | 56 ++- lib/core/wallet_loading_service.dart | 33 +- lib/monero/cw_monero.dart | 4 +- .../dashboard/dashboard_view_model.dart | 32 +- lib/view_model/send/send_view_model.dart | 4 +- scripts/prepare_moneroc.sh | 2 +- 25 files changed, 577 insertions(+), 714 deletions(-) diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index a63a5e2e5..ac70a4aaa 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +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'; @@ -602,7 +603,25 @@ abstract class DecredWalletBase throw "wallet already exists at $newDirPath"; } - await Directory(currentDirPath).rename(newDirPath); + 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 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); } @override diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index 161184b0a..e2313904e 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -118,6 +118,10 @@ class DecredWalletService extends WalletService< currentWalletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet ? testnet : mainnet; + if (libwallet == null) { + libwallet = await Libwallet.spawn(); + libwallet!.initLibdcrwallet("", "err"); + } final currentWallet = DecredWallet( currentWalletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index 3ceef5815..7f6a3f1aa 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -2,31 +2,31 @@ import 'dart:async'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_account_list.dart'; -import 'package:monero/monero.dart' as monero; +import 'package:monero/src/wallet2.dart'; +import 'package:monero/src/monero.dart'; -monero.wallet? wptr = null; -bool get isViewOnly => int.tryParse(monero.Wallet_secretSpendKey(wptr!)) == 0; +Wallet2Wallet? currentWallet = null; +bool get isViewOnly => int.tryParse(currentWallet!.secretSpendKey()) == 0; int _wlptrForW = 0; -monero.WalletListener? _wlptr = null; +Wallet2WalletListener? _wlptr = null; -monero.WalletListener? getWlptr() { - if (wptr == null) return null; - if (wptr!.address == _wlptrForW) return _wlptr!; - _wlptrForW = wptr!.address; - _wlptr = monero.MONERO_cw_getWalletListener(wptr!); +Wallet2WalletListener? getWlptr() { + if (currentWallet == null) return null; + _wlptrForW = currentWallet!.ffiAddress(); + _wlptr = currentWallet!.getWalletListener(); return _wlptr!; } -monero.SubaddressAccount? subaddressAccount; +Wallet2SubaddressAccount? subaddressAccount; bool isUpdating = false; void refreshAccounts() { try { isUpdating = true; - subaddressAccount = monero.Wallet_subaddressAccount(wptr!); - monero.SubaddressAccount_refresh(subaddressAccount!); + subaddressAccount = currentWallet!.subaddressAccount(); + subaddressAccount!.refresh(); isUpdating = false; } catch (e) { isUpdating = false; @@ -34,45 +34,28 @@ void refreshAccounts() { } } -List getAllAccount() { + List getAllAccount() { // final size = monero.Wallet_numSubaddressAccounts(wptr!); refreshAccounts(); - int size = monero.SubaddressAccount_getAll_size(subaddressAccount!); + int size = subaddressAccount!.getAll_size(); if (size == 0) { - monero.Wallet_addSubaddressAccount(wptr!); - monero.Wallet_status(wptr!); + currentWallet!.addSubaddressAccount(); + currentWallet!.status(); return []; } return List.generate(size, (index) { - return monero.SubaddressAccount_getAll_byIndex(subaddressAccount!, index: index); + return subaddressAccount!.getAll_byIndex(index); }); } -void addAccountSync({required String label}) { - monero.Wallet_addSubaddressAccount(wptr!, label: label); -} - -void setLabelForAccountSync({required int accountIndex, required String label}) { - monero.SubaddressAccount_setLabel(subaddressAccount!, accountIndex: accountIndex, label: label); - MoneroAccountListBase.cachedAccounts[wptr!.address] = []; - refreshAccounts(); -} - -void _addAccount(String label) => addAccountSync(label: label); - -void _setLabelForAccount(Map args) { - final label = args['label'] as String; - final accountIndex = args['accountIndex'] as int; - - setLabelForAccountSync(label: label, accountIndex: accountIndex); -} - -Future addAccount({required String label}) async { - _addAccount(label); +void addAccount({required String label}) { + currentWallet!.addSubaddressAccount(label: label); unawaited(store()); } -Future setLabelForAccount({required int accountIndex, required String label}) async { - _setLabelForAccount({'accountIndex': accountIndex, 'label': label}); - unawaited(store()); -} \ No newline at end of file +void setLabelForAccount({required int accountIndex, required String label}) { + subaddressAccount!.setLabel(accountIndex: accountIndex, label: label); + MoneroAccountListBase.cachedAccounts[currentWallet!.ffiAddress()] = []; + refreshAccounts(); + unawaited(store()); +} diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart index 83382f001..b08dbb1f7 100644 --- a/cw_monero/lib/api/coins_info.dart +++ b/cw_monero/lib/api/coins_info.dart @@ -3,17 +3,18 @@ import 'dart:isolate'; import 'package:cw_monero/api/account_list.dart'; import 'package:monero/monero.dart' as monero; +import 'package:monero/src/wallet2.dart'; import 'package:mutex/mutex.dart'; -monero.Coins? coins = null; +Wallet2Coins? coins = null; final coinsMutex = Mutex(); Future refreshCoins(int accountIndex) async { if (coinsMutex.isLocked) { return; } - coins = monero.Wallet_coins(wptr!); - final coinsPtr = coins!.address; + coins = currentWallet!.coins(); + final coinsPtr = coins!.ffiAddress(); await coinsMutex.acquire(); await Isolate.run(() => monero.Coins_refresh(Pointer.fromAddress(coinsPtr))); coinsMutex.release(); @@ -21,14 +22,14 @@ Future refreshCoins(int accountIndex) async { Future countOfCoins() async { await coinsMutex.acquire(); - final count = monero.Coins_count(coins!); + final count = coins!.count(); coinsMutex.release(); return count; } -Future getCoin(int index) async { +Future getCoin(int index) async { await coinsMutex.acquire(); - final coin = monero.Coins_coin(coins!, index); + final coin = coins!.coin(index); coinsMutex.release(); return coin; } @@ -37,7 +38,7 @@ Future getCoinByKeyImage(String keyImage) async { final count = await countOfCoins(); for (int i = 0; i < count; i++) { final coin = await getCoin(i); - final coinAddress = monero.CoinsInfo_keyImage(coin); + final coinAddress = coin.keyImage; if (keyImage == coinAddress) { return i; } @@ -47,14 +48,14 @@ Future getCoinByKeyImage(String keyImage) async { Future freezeCoin(int index) async { await coinsMutex.acquire(); - final coinsPtr = coins!.address; + final coinsPtr = coins!.ffiAddress(); await Isolate.run(() => monero.Coins_setFrozen(Pointer.fromAddress(coinsPtr), index: index)); coinsMutex.release(); } Future thawCoin(int index) async { await coinsMutex.acquire(); - final coinsPtr = coins!.address; + final coinsPtr = coins!.ffiAddress(); await Isolate.run(() => monero.Coins_thaw(Pointer.fromAddress(coinsPtr), index: index)); coinsMutex.release(); } diff --git a/cw_monero/lib/api/subaddress_list.dart b/cw_monero/lib/api/subaddress_list.dart index 5db8a5d5f..46bdc10bc 100644 --- a/cw_monero/lib/api/subaddress_list.dart +++ b/cw_monero/lib/api/subaddress_list.dart @@ -2,7 +2,8 @@ import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; -import 'package:monero/monero.dart' as monero; +import 'package:monero/monero.dart'; +import 'package:monero/src/monero.dart'; bool isUpdating = false; @@ -16,7 +17,7 @@ class SubaddressInfoMetadata { SubaddressInfoMetadata? subaddress = null; String getRawLabel({required int accountIndex, required int addressIndex}) { - return monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + return currentWallet!.getSubaddressLabel(accountIndex: accountIndex, addressIndex: addressIndex); } void refreshSubaddresses({required int accountIndex}) { @@ -46,7 +47,7 @@ class Subaddress { final int received; final int txCount; String get label { - final localLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + final localLabel = currentWallet!.getSubaddressLabel(accountIndex: accountIndex, addressIndex: addressIndex); if (localLabel.startsWith("#$addressIndex")) return localLabel; // don't duplicate the ID if it was user-providen return "#$addressIndex ${localLabel}".trim(); } @@ -66,26 +67,26 @@ int lastTxCount = 0; List ttDetails = []; List getAllSubaddresses() { - txhistory = monero.Wallet_history(wptr!); - final txCount = monero.TransactionHistory_count(txhistory!); - if (lastTxCount != txCount && lastWptr != wptr!.address) { + txhistory = currentWallet!.history(); + final txCount = txhistory!.count(); + if (lastTxCount != txCount && lastWptr != currentWallet!.ffiAddress()) { final List newttDetails = []; lastTxCount = txCount; - lastWptr = wptr!.address; + lastWptr = currentWallet!.ffiAddress(); for (var i = 0; i < txCount; i++) { - final tx = monero.TransactionHistory_transaction(txhistory!, index: i); - if (monero.TransactionInfo_direction(tx) == monero.TransactionInfo_Direction.Out) continue; - final subaddrs = monero.TransactionInfo_subaddrIndex(tx).split(","); - final account = monero.TransactionInfo_subaddrAccount(tx); + final tx = txhistory!.transaction(i); + if (tx.direction() == TransactionInfo_Direction.Out.index) continue; + final subaddrs = tx.subaddrIndex().split(","); + final account = tx.subaddrAccount(); newttDetails.add(TinyTransactionDetails( address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), - amount: monero.TransactionInfo_amount(tx), + amount: tx.amount(), )); } ttDetails.clear(); ttDetails.addAll(newttDetails); } - final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); + final size = currentWallet!.numSubaddresses(accountIndex: subaddress!.accountIndex); final list = List.generate(size, (index) { final ttDetailsLocal = ttDetails.where((element) { final address = getAddress( @@ -119,46 +120,17 @@ List getAllSubaddresses() { } int numSubaddresses(int subaccountIndex) { - return monero.Wallet_numSubaddresses(wptr!, accountIndex: subaccountIndex); -} - -void addSubaddressSync({required int accountIndex, required String label}) { - monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label); - refreshSubaddresses(accountIndex: accountIndex); -} - -void setLabelForSubaddressSync( - {required int accountIndex, required int addressIndex, required String label}) { - monero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label); -} - -void _addSubaddress(Map args) { - final label = args['label'] as String; - final accountIndex = args['accountIndex'] as int; - - addSubaddressSync(accountIndex: accountIndex, label: label); -} - -void _setLabelForSubaddress(Map args) { - final label = args['label'] as String; - final accountIndex = args['accountIndex'] as int; - final addressIndex = args['addressIndex'] as int; - - setLabelForSubaddressSync( - accountIndex: accountIndex, addressIndex: addressIndex, label: label); + return currentWallet!.numSubaddresses(accountIndex: subaccountIndex); } Future addSubaddress({required int accountIndex, required String label}) async { - _addSubaddress({'accountIndex': accountIndex, 'label': label}); + currentWallet!.addSubaddress(accountIndex: accountIndex, label: label); + refreshSubaddresses(accountIndex: accountIndex); await store(); } Future setLabelForSubaddress( - {required int accountIndex, required int addressIndex, required String label}) async { - _setLabelForSubaddress({ - 'accountIndex': accountIndex, - 'addressIndex': addressIndex, - 'label': label - }); + {required int accountIndex, required int addressIndex, required String label}) async { + currentWallet!.setSubaddressLabel(accountIndex: accountIndex, addressIndex: addressIndex, label: label); await store(); -} +} \ 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 8c7b1e902..4360149d2 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -9,36 +9,38 @@ import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; import 'package:ffi/ffi.dart'; +import 'package:monero/src/monero.dart'; import 'package:monero/monero.dart' as monero; +import 'package:monero/src/wallet2.dart'; import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen; import 'package:mutex/mutex.dart'; Map> txKeys = {}; String getTxKey(String txId) { - txKeys[wptr!.address] ??= {}; - if (txKeys[wptr!.address]![txId] != null) { - return txKeys[wptr!.address]![txId]!; + txKeys[currentWallet!.ffiAddress()] ??= {}; + if (txKeys[currentWallet!.ffiAddress()]![txId] != null) { + return txKeys[currentWallet!.ffiAddress()]![txId]!; } - final txKey = monero.Wallet_getTxKey(wptr!, txid: txId); - final status = monero.Wallet_status(wptr!); + final txKey = currentWallet!.getTxKey(txid: txId); + final status = currentWallet!.status(); if (status != 0) { - monero.Wallet_errorString(wptr!); - txKeys[wptr!.address]![txId] = ""; + currentWallet!.errorString(); + txKeys[currentWallet!.ffiAddress()]![txId] = ""; return ""; } - txKeys[wptr!.address]![txId] = txKey; + txKeys[currentWallet!.ffiAddress()]![txId] = txKey; return txKey; } final txHistoryMutex = Mutex(); -monero.TransactionHistory? txhistory; +Wallet2TransactionHistory? txhistory; bool isRefreshingTx = false; Future refreshTransactions() async { if (isRefreshingTx == true) return; isRefreshingTx = true; - txhistory ??= monero.Wallet_history(wptr!); - final ptr = txhistory!.address; + txhistory ??= currentWallet!.history(); + final ptr = txhistory!.ffiAddress(); await txHistoryMutex.acquire(); await Isolate.run(() { monero.TransactionHistory_refresh(Pointer.fromAddress(ptr)); @@ -48,14 +50,14 @@ Future refreshTransactions() async { isRefreshingTx = false; } -int countOfTransactions() => monero.TransactionHistory_count(txhistory!); +int countOfTransactions() => txhistory!.count(); Future> getAllTransactions() async { List dummyTxs = []; await txHistoryMutex.acquire(); - txhistory ??= monero.Wallet_history(wptr!); - final startAddress = txhistory!.address * wptr!.address; + txhistory ??= currentWallet!.history(); + final startAddress = txhistory!.ffiAddress() * currentWallet!.ffiAddress(); int size = countOfTransactions(); final list = []; for (int index = 0; index < size; index++) { @@ -63,21 +65,21 @@ Future> getAllTransactions() async { // Give main thread a chance to do other things. await Future.delayed(Duration.zero); } - if (txhistory!.address * wptr!.address != startAddress) { + if (txhistory!.ffiAddress() * currentWallet!.ffiAddress() != startAddress) { printV("Loop broken because txhistory!.address * wptr!.address != startAddress"); break; } - final txInfo = monero.TransactionHistory_transaction(txhistory!, index: index); - final txHash = monero.TransactionInfo_hash(txInfo); - txCache[wptr!.address] ??= {}; - txCache[wptr!.address]![txHash] = Transaction(txInfo: txInfo); - list.add(txCache[wptr!.address]![txHash]!); + final txInfo = txhistory!.transaction(index); + final txHash = txInfo.hash(); + txCache[currentWallet!.ffiAddress()] ??= {}; + txCache[currentWallet!.ffiAddress()]![txHash] = Transaction(txInfo: txInfo); + list.add(txCache[currentWallet!.ffiAddress()]![txHash]!); } txHistoryMutex.release(); - final accts = monero.Wallet_numSubaddressAccounts(wptr!); + final accts = currentWallet!.numSubaddressAccounts(); for (var i = 0; i < accts; i++) { - final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i); - final availBalance = monero.Wallet_unlockedBalance(wptr!, accountIndex: i); + final fullBalance = currentWallet!.balance(accountIndex: i); + final availBalance = currentWallet!.unlockedBalance(accountIndex: i); if (fullBalance > availBalance) { if (list.where((element) => element.accountIndex == i && element.isConfirmed == false).isEmpty) { dummyTxs.add( @@ -95,7 +97,7 @@ Future> getAllTransactions() async { isSpend: false, hash: "pending", key: "", - txInfo: Pointer.fromAddress(0), + txInfo: DummyTransaction(), )..timeStamp = DateTime.now() ); } @@ -105,16 +107,21 @@ Future> getAllTransactions() async { return list; } +class DummyTransaction implements Wallet2TransactionInfo { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + Map> txCache = {}; Future getTransaction(String txId) async { - if (txCache[wptr!.address] != null && txCache[wptr!.address]![txId] != null) { - return txCache[wptr!.address]![txId]!; + if (txCache[currentWallet!.ffiAddress()] != null && txCache[currentWallet!.ffiAddress()]![txId] != null) { + return txCache[currentWallet!.ffiAddress()]![txId]!; } await txHistoryMutex.acquire(); - final tx = monero.TransactionHistory_transactionById(txhistory!, txid: txId); + final tx = txhistory!.transactionById(txId); final txDart = Transaction(txInfo: tx); - txCache[wptr!.address] ??= {}; - txCache[wptr!.address]![txId] = txDart; + txCache[currentWallet!.ffiAddress()] ??= {}; + txCache[currentWallet!.ffiAddress()]![txId] = txDart; txHistoryMutex.release(); return txDart; } @@ -127,9 +134,9 @@ Future createTransactionSync( int accountIndex = 0, List preferredInputs = const []}) async { - final amt = amount == null ? 0 : monero.Wallet_amountFromString(amount); + final amt = amount == null ? 0 : currentWallet!.amountFromString(amount); - final waddr = wptr!.address; + final waddr = currentWallet!.ffiAddress(); // force reconnection in case the os killed the connection? // fixes failed to get block height error. @@ -149,7 +156,7 @@ Future createTransactionSync( final paymentIdAddr = paymentId_.address; final preferredInputsAddr = preferredInputs_.address; final spaddr = monero.defaultSeparator.address; - final pendingTx = Pointer.fromAddress(await Isolate.run(() { + final pendingTxPtr = Pointer.fromAddress(await Isolate.run(() { final tx = monero_gen.MoneroC(DynamicLibrary.open(monero.libPath)).MONERO_Wallet_createTransaction( Pointer.fromAddress(waddr), Pointer.fromAddress(addraddr).cast(), @@ -163,15 +170,16 @@ Future createTransactionSync( ); return tx.address; })); + final Wallet2PendingTransaction pendingTx = MoneroPendingTransaction(pendingTxPtr); calloc.free(address_); calloc.free(paymentId_); calloc.free(preferredInputs_); final String? error = (() { - final status = monero.PendingTransaction_status(pendingTx); + final status = pendingTx.status(); if (status == 0) { return null; } - return monero.PendingTransaction_errorString(pendingTx); + return pendingTx.errorString(); })(); if (error != null) { @@ -182,10 +190,10 @@ Future createTransactionSync( throw CreationTransactionException(message: message); } - final rAmt = monero.PendingTransaction_amount(pendingTx); - final rFee = monero.PendingTransaction_fee(pendingTx); - final rHash = monero.PendingTransaction_txid(pendingTx, ''); - final rHex = monero.PendingTransaction_hex(pendingTx, ''); + final rAmt = pendingTx.amount(); + final rFee = pendingTx.fee(); + final rHash = pendingTx.txid(''); + final rHex = pendingTx.hex(''); final rTxKey = rHash; return PendingTransactionDescription( @@ -194,7 +202,7 @@ Future createTransactionSync( hash: rHash, hex: rHex, txKey: rTxKey, - pointerAddress: pendingTx.address, + pointerAddress: pendingTx.ffiAddress(), ); } @@ -206,9 +214,9 @@ Future createTransactionMultDest( List preferredInputs = const []}) async { final dstAddrs = outputs.map((e) => e.address).toList(); - final amounts = outputs.map((e) => monero.Wallet_amountFromString(e.amount)).toList(); + final amounts = outputs.map((e) => currentWallet!.amountFromString(e.amount)).toList(); - final waddr = wptr!.address; + final waddr = currentWallet!.ffiAddress(); // force reconnection in case the os killed the connection Isolate.run(() async { @@ -227,49 +235,50 @@ Future createTransactionMultDest( ).address; })); - if (monero.PendingTransaction_status(txptr) != 0) { - throw CreationTransactionException(message: monero.PendingTransaction_errorString(txptr)); + final Wallet2PendingTransaction tx = MoneroPendingTransaction(txptr); + + if (tx.status() != 0) { + throw CreationTransactionException(message: tx.errorString()); } return PendingTransactionDescription( - amount: monero.PendingTransaction_amount(txptr), - fee: monero.PendingTransaction_fee(txptr), - hash: monero.PendingTransaction_txid(txptr, ''), - hex: monero.PendingTransaction_hex(txptr, ''), - txKey: monero.PendingTransaction_txid(txptr, ''), - pointerAddress: txptr.address, + amount: tx.amount(), + fee: tx.fee(), + hash: tx.txid(''), + hex: tx.hex(''), + txKey: tx.txid(''), + pointerAddress: tx.ffiAddress(), ); } String? commitTransactionFromPointerAddress({required int address, required bool useUR}) => - commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address), useUR: useUR); + commitTransaction(tx: MoneroPendingTransaction(Pointer.fromAddress(address)), useUR: useUR); -String? commitTransaction({required monero.PendingTransaction transactionPointer, required bool useUR}) { - final transactionPointerAddress = transactionPointer.address; +String? commitTransaction({required Wallet2PendingTransaction tx, required bool useUR}) { final txCommit = useUR - ? monero.PendingTransaction_commitUR(transactionPointer, 120) + ? tx.commitUR(120) : Isolate.run(() { monero.PendingTransaction_commit( - Pointer.fromAddress(transactionPointerAddress), + Pointer.fromAddress(tx.ffiAddress()), filename: '', overwrite: false, ); }); String? error = (() { - final status = monero.PendingTransaction_status(transactionPointer.cast()); + final status = tx.status(); if (status == 0) { return null; } - return monero.PendingTransaction_errorString(transactionPointer.cast()); + return tx.errorString(); })(); if (error == null) { error = (() { - final status = monero.Wallet_status(wptr!); + final status = currentWallet!.status(); if (status == 0) { return null; } - return monero.Wallet_errorString(wptr!); + return currentWallet!.errorString(); })(); } @@ -283,43 +292,9 @@ String? commitTransaction({required monero.PendingTransaction transactionPointer } } -Future _createTransactionSync(Map args) async { - final address = args['address'] as String; - final paymentId = args['paymentId'] as String; - final amount = args['amount'] as String?; - final priorityRaw = args['priorityRaw'] as int; - final accountIndex = args['accountIndex'] as int; - final preferredInputs = args['preferredInputs'] as List; - - return createTransactionSync( - address: address, - paymentId: paymentId, - amount: amount, - priorityRaw: priorityRaw, - accountIndex: accountIndex, - preferredInputs: preferredInputs); -} - -Future createTransaction( - {required String address, - required int priorityRaw, - String? amount, - String paymentId = '', - int accountIndex = 0, - List preferredInputs = const []}) async => - _createTransactionSync({ - 'address': address, - 'paymentId': paymentId, - 'amount': amount, - 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex, - 'preferredInputs': preferredInputs - }); - class Transaction { final String displayLabel; - late final String subaddressLabel = monero.Wallet_getSubaddressLabel( - wptr!, + late final String subaddressLabel = currentWallet!.getSubaddressLabel( accountIndex: accountIndex, addressIndex: addressIndex, ); @@ -372,26 +347,26 @@ class Transaction { // final SubAddress? subAddress; // List transfers = []; // final int txIndex; - final monero.TransactionInfo txInfo; + final Wallet2TransactionInfo txInfo; Transaction({ required this.txInfo, - }) : displayLabel = monero.TransactionInfo_label(txInfo), - hash = monero.TransactionInfo_hash(txInfo), + }) : displayLabel = txInfo.label(), + hash = txInfo.hash(), timeStamp = DateTime.fromMillisecondsSinceEpoch( - monero.TransactionInfo_timestamp(txInfo) * 1000, + txInfo.timestamp() * 1000, ), - isSpend = monero.TransactionInfo_direction(txInfo) == - monero.TransactionInfo_Direction.Out, - amount = monero.TransactionInfo_amount(txInfo), - paymentId = monero.TransactionInfo_paymentId(txInfo), - accountIndex = monero.TransactionInfo_subaddrAccount(txInfo), - addressIndex = int.tryParse(monero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, - addressIndexList = monero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), - blockheight = monero.TransactionInfo_blockHeight(txInfo), - confirmations = monero.TransactionInfo_confirmations(txInfo), - fee = monero.TransactionInfo_fee(txInfo), - description = monero.TransactionInfo_description(txInfo), - key = getTxKey(monero.TransactionInfo_hash(txInfo)); + isSpend = txInfo.direction() == + monero.TransactionInfo_Direction.Out.index, + amount = txInfo.amount(), + paymentId = txInfo.paymentId(), + accountIndex = txInfo.subaddrAccount(), + addressIndex = int.tryParse(txInfo.subaddrIndex().split(", ")[0]) ?? 0, + addressIndexList = txInfo.subaddrIndex().split(", ").map((e) => int.tryParse(e) ?? 0).toList(), + blockheight = txInfo.blockHeight(), + confirmations = txInfo.confirmations(), + fee = txInfo.fee(), + description = txInfo.description(), + key = getTxKey(txInfo.hash()); Transaction.dummy({ required this.displayLabel, diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 7e64c7f08..87abdd95b 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -5,8 +5,6 @@ import 'dart:isolate'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; -import 'package:cw_monero/api/wallet_manager.dart'; -import 'package:flutter/foundation.dart'; import 'package:monero/monero.dart' as monero; import 'package:mutex/mutex.dart'; import 'package:polyseed/polyseed.dart'; @@ -15,36 +13,37 @@ bool debugMonero = false; int getSyncingHeight() { // final height = monero.MONERO_cw_WalletListener_height(getWlptr()); - final h2 = monero.Wallet_blockChainHeight(wptr!); + if (currentWallet == null) return 0; + final h2 = currentWallet!.blockChainHeight(); // printV("height: $height / $h2"); return h2; } bool isNeededToRefresh() { - final wlptr = getWlptr(); - if (wlptr == null) return false; - final ret = monero.MONERO_cw_WalletListener_isNeedToRefresh(wlptr); - monero.MONERO_cw_WalletListener_resetNeedToRefresh(wlptr); + final wl = getWlptr(); + if (wl == null) return false; + final ret = wl.isNeedToRefresh(); + wl.resetNeedToRefresh(); return ret; } bool isNewTransactionExist() { final wlptr = getWlptr(); if (wlptr == null) return false; - final ret = monero.MONERO_cw_WalletListener_isNewTransactionExist(wlptr); - monero.MONERO_cw_WalletListener_resetIsNewTransactionExist(wlptr); + final ret = wlptr.isNewTransactionExist(); + wlptr.resetIsNewTransactionExist(); return ret; } -String getFilename() => monero.Wallet_filename(wptr!); +String getFilename() => currentWallet!.filename(); String getSeed() { // monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); final cakepolyseed = - monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); + currentWallet!.getCacheAttribute(key: "cakewallet.seed"); final cakepassphrase = getPassphrase(); - final weirdPolyseed = monero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase); + final weirdPolyseed = currentWallet!.getPolyseed(passphrase: cakepassphrase); if (weirdPolyseed != "") return weirdPolyseed; if (cakepolyseed != "") { @@ -63,7 +62,7 @@ String getSeed() { return cakepolyseed; } - final bip39 = monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed.bip39"); + final bip39 = currentWallet!.getCacheAttribute(key: "cakewallet.seed.bip39"); if(bip39.isNotEmpty) return bip39; @@ -85,29 +84,29 @@ String? getSeedLanguage(String? language) { String getSeedLegacy(String? language) { final cakepassphrase = getPassphrase(); language = getSeedLanguage(language); - var legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase); - if (monero.Wallet_status(wptr!) != 0) { - if (monero.Wallet_errorString(wptr!).contains("seed_language")) { - monero.Wallet_setSeedLanguage(wptr!, language: "English"); - legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase); + var legacy = currentWallet!.seed(seedOffset: cakepassphrase); + if (currentWallet!.status() != 0) { + if (currentWallet!.errorString().contains("seed_language")) { + currentWallet!.setSeedLanguage(language: "English"); + legacy = currentWallet!.seed(seedOffset: cakepassphrase); } } if (language != null) { - monero.Wallet_setSeedLanguage(wptr!, language: language); - final status = monero.Wallet_status(wptr!); + currentWallet!.setSeedLanguage(language: language); + final status = currentWallet!.status(); if (status != 0) { - final err = monero.Wallet_errorString(wptr!); + final err = currentWallet!.errorString(); if (legacy.isNotEmpty) { return "$err\n\n$legacy"; } return err; } - legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase); + legacy = currentWallet!.seed(seedOffset: cakepassphrase); } - if (monero.Wallet_status(wptr!) != 0) { - final err = monero.Wallet_errorString(wptr!); + if (currentWallet!.status() != 0) { + final err = currentWallet!.errorString(); if (legacy.isNotEmpty) { return "$err\n\n$legacy"; } @@ -117,7 +116,7 @@ String getSeedLegacy(String? language) { } String getPassphrase() { - return monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.passphrase"); + return currentWallet!.getCacheAttribute(key: "cakewallet.passphrase"); } Map>> addressCache = {}; @@ -125,31 +124,31 @@ Map>> addressCache = {}; String getAddress({int accountIndex = 0, int addressIndex = 0}) { // 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 (monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { - if (monero.Wallet_numSubaddressAccounts(wptr!) < accountIndex) { - monero.Wallet_addSubaddressAccount(wptr!); + if (currentWallet!.numSubaddresses(accountIndex: accountIndex)-1 < addressIndex) { + if (currentWallet!.numSubaddressAccounts() < accountIndex) { + currentWallet!.addSubaddressAccount(); } else { - monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); + currentWallet!.addSubaddress(accountIndex: accountIndex); } } - addressCache[wptr!.address] ??= {}; - addressCache[wptr!.address]![accountIndex] ??= {}; - addressCache[wptr!.address]![accountIndex]![addressIndex] ??= monero.Wallet_address(wptr!, + addressCache[currentWallet!.ffiAddress()] ??= {}; + addressCache[currentWallet!.ffiAddress()]![accountIndex] ??= {}; + addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex] ??= currentWallet!.address( accountIndex: accountIndex, addressIndex: addressIndex); - return addressCache[wptr!.address]![accountIndex]![addressIndex]!; + return addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex]!; } int getFullBalance({int accountIndex = 0}) => - monero.Wallet_balance(wptr!, accountIndex: accountIndex); + currentWallet!.balance(accountIndex: accountIndex); int getUnlockedBalance({int accountIndex = 0}) => - monero.Wallet_unlockedBalance(wptr!, accountIndex: accountIndex); + currentWallet!.unlockedBalance(accountIndex: accountIndex); -int getCurrentHeight() => monero.Wallet_blockChainHeight(wptr!); +int getCurrentHeight() => currentWallet!.blockChainHeight(); -int getNodeHeightSync() => monero.Wallet_daemonBlockChainHeight(wptr!); +int getNodeHeightSync() => currentWallet!.daemonBlockChainHeight(); -bool isConnectedSync() => monero.Wallet_connected(wptr!) != 0; +bool isConnectedSync() => currentWallet!.connected() != 0; Future setupNodeSync( {required String address, @@ -168,7 +167,7 @@ Future setupNodeSync( daemonPassword: $password ?? '' } '''); - final addr = wptr!.address; + final addr = currentWallet!.ffiAddress(); printV("init: start"); await Isolate.run(() { monero.Wallet_init(Pointer.fromAddress(addr), @@ -180,10 +179,10 @@ Future setupNodeSync( }); printV("init: end"); - final status = monero.Wallet_status(wptr!); + final status = currentWallet!.status(); if (status != 0) { - final error = monero.Wallet_errorString(wptr!); + final error = currentWallet!.errorString(); if (error != "no tx keys found for this txid") { printV("error: $error"); throw SetupWalletException(message: error); @@ -191,8 +190,8 @@ Future setupNodeSync( } if (true) { - monero.Wallet_init3( - wptr!, argv0: '', + currentWallet!.init3( + argv0: '', defaultLogBaseName: 'moneroc', console: true, logPath: '', @@ -203,19 +202,19 @@ Future setupNodeSync( } void startRefreshSync() { - monero.Wallet_refreshAsync(wptr!); - monero.Wallet_startRefresh(wptr!); + currentWallet!.refreshAsync(); + currentWallet!.startRefresh(); } void setRefreshFromBlockHeight({required int height}) { - monero.Wallet_setRefreshFromBlockHeight(wptr!, + currentWallet!.setRefreshFromBlockHeight( refresh_from_block_height: height); } void setRecoveringFromSeed({required bool isRecovery}) { - monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery); - monero.Wallet_store(wptr!); + currentWallet!.setRecoveringFromSeed(recoveringFromSeed: isRecovery); + currentWallet!.store(); } final storeMutex = Mutex(); @@ -224,18 +223,18 @@ final storeMutex = Mutex(); int lastStorePointer = 0; int lastStoreHeight = 0; void storeSync({bool force = false}) async { - final addr = wptr!.address; + final addr = currentWallet!.ffiAddress(); final synchronized = await Isolate.run(() { return monero.Wallet_synchronized(Pointer.fromAddress(addr)); }); - if (lastStorePointer == wptr!.address && - lastStoreHeight + 5000 > monero.Wallet_blockChainHeight(wptr!) && + if (lastStorePointer == addr && + lastStoreHeight + 5000 > currentWallet!.blockChainHeight() && !synchronized && !force) { return; } - lastStorePointer = wptr!.address; - lastStoreHeight = monero.Wallet_blockChainHeight(wptr!); + lastStorePointer = currentWallet!.ffiAddress(); + lastStoreHeight = currentWallet!.blockChainHeight(); await storeMutex.acquire(); await Isolate.run(() { monero.Wallet_store(Pointer.fromAddress(addr)); @@ -244,25 +243,25 @@ void storeSync({bool force = false}) async { } void setPasswordSync(String password) { - monero.Wallet_setPassword(wptr!, password: password); + currentWallet!.setPassword(password: password); - final status = monero.Wallet_status(wptr!); + final status = currentWallet!.status(); if (status != 0) { - throw Exception(monero.Wallet_errorString(wptr!)); + throw Exception(currentWallet!.errorString()); } } void closeCurrentWallet() { - monero.Wallet_stop(wptr!); + currentWallet!.stop(); } -String getSecretViewKey() => monero.Wallet_secretViewKey(wptr!); +String getSecretViewKey() => currentWallet!.secretViewKey(); -String getPublicViewKey() => monero.Wallet_publicViewKey(wptr!); +String getPublicViewKey() => currentWallet!.publicViewKey(); -String getSecretSpendKey() => monero.Wallet_secretSpendKey(wptr!); +String getSecretSpendKey() => currentWallet!.secretSpendKey(); -String getPublicSpendKey() => monero.Wallet_publicSpendKey(wptr!); +String getPublicSpendKey() => currentWallet!.publicSpendKey(); class SyncListener { SyncListener(this.onNewBlock, this.onNewTransaction) @@ -360,52 +359,32 @@ Future _setupNodeSync(Map args) async { socksProxyAddress: socksProxyAddress); } -bool _isConnected(Object _) => isConnectedSync(); - -int _getNodeHeight(Object _) => getNodeHeightSync(); - void startRefresh() => startRefreshSync(); -Future setupNode( - {required String address, - String? login, - String? password, - bool useSSL = false, - String? socksProxyAddress, - bool isLightWallet = false}) async => - _setupNodeSync({ - 'address': address, - 'login': login, - 'password': password, - 'useSSL': useSSL, - 'isLightWallet': isLightWallet, - 'socksProxyAddress': socksProxyAddress - }); - Future store() async => _storeSync(0); -Future isConnected() async => _isConnected(0); +Future isConnected() async => isConnectedSync(); -Future getNodeHeight() async => _getNodeHeight(0); +Future getNodeHeight() async => getNodeHeightSync(); -void rescanBlockchainAsync() => monero.Wallet_rescanBlockchainAsync(wptr!); +void rescanBlockchainAsync() => currentWallet!.rescanBlockchainAsync(); String getSubaddressLabel(int accountIndex, int addressIndex) { - return monero.Wallet_getSubaddressLabel(wptr!, + return currentWallet!.getSubaddressLabel( accountIndex: accountIndex, addressIndex: addressIndex); } Future setTrustedDaemon(bool trusted) async => - monero.Wallet_setTrustedDaemon(wptr!, arg: trusted); + currentWallet!.setTrustedDaemon(arg: trusted); -Future trustedDaemon() async => monero.Wallet_trustedDaemon(wptr!); +Future trustedDaemon() async => currentWallet!.trustedDaemon(); String signMessage(String message, {String address = ""}) { - return monero.Wallet_signMessage(wptr!, message: message, address: address); + return currentWallet!.signMessage(message: message, address: address); } bool verifyMessage(String message, String address, String signature) { - return monero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature); + return currentWallet!.verifySignedMessage(message: message, address: address, signature: signature); } Map> debugCallLength() => monero.debugCallLength; diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index b43773447..9b369c155 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -12,6 +12,8 @@ import 'package:cw_monero/api/transaction_history.dart'; import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/ledger.dart'; import 'package:flutter/foundation.dart'; +import 'package:monero/src/monero.dart'; +import 'package:monero/src/wallet2.dart'; import 'package:monero/monero.dart' as monero; class MoneroCException implements Exception { @@ -24,9 +26,10 @@ class MoneroCException implements Exception { } void checkIfMoneroCIsFine() { - final cppCsCpp = monero.MONERO_checksum_wallet2_api_c_cpp(); - final cppCsH = monero.MONERO_checksum_wallet2_api_c_h(); - final cppCsExp = monero.MONERO_checksum_wallet2_api_c_exp(); + final checksum = MoneroWalletChecksum(); + final cppCsCpp = checksum.checksum_wallet2_api_c_cpp(); + final cppCsH = checksum.checksum_wallet2_api_c_h(); + final cppCsExp = checksum.checksum_wallet2_api_c_exp(); final dartCsCpp = monero.wallet2_api_c_cpp_sha256; final dartCsH = monero.wallet2_api_c_h_sha256; @@ -44,36 +47,35 @@ void checkIfMoneroCIsFine() { throw MoneroCException("monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'"); } } -monero.WalletManager? _wmPtr; -final monero.WalletManager wmPtr = Pointer.fromAddress((() { +Wallet2WalletManager? _wmPtr; +Wallet2WalletManager wmPtr = (() { try { // Problems with the wallet? Crashes? Lags? this will print all calls to xmr // codebase, so it will be easier to debug what happens. At least easier // than plugging gdb in. Especially on windows/android. monero.printStarts = false; if (kDebugMode && debugMonero) { - monero.WalletManagerFactory_setLogLevel(4); + MoneroWalletManagerFactory().setLogLevel(4); } - _wmPtr ??= monero.WalletManagerFactory_getWalletManager(); + _wmPtr ??= MoneroWalletManagerFactory().getWalletManager(); if (kDebugMode && debugMonero) { - monero.WalletManagerFactory_setLogLevel(4); + MoneroWalletManagerFactory().setLogLevel(4); } printV("ptr: $_wmPtr"); } catch (e) { printV(e); rethrow; } - return _wmPtr!.address; -})()); + return _wmPtr!; +})(); -void createWalletPointer() { - final newWptr = monero.WalletManager_createWallet(wmPtr, +Wallet2Wallet createWalletPointer() { + final newWptr = wmPtr.createWallet( path: "", password: "", language: "", networkType: 0); - - wptr = newWptr; + return newWptr; } -void createWalletSync( +void createWallet( {required String path, required String password, required String language, @@ -81,28 +83,25 @@ void createWalletSync( int nettype = 0}) { txhistory = null; language = getSeedLanguage(language)!; - final newWptr = monero.WalletManager_createWallet(wmPtr, + final newW = wmPtr.createWallet( path: path, password: password, language: language, networkType: 0); - int status = monero.Wallet_status(newWptr); + int status = newW.status(); if (status != 0) { - throw WalletCreationException(message: monero.Wallet_errorString(newWptr)); + throw WalletCreationException(message: newW.errorString()); } - setupBackgroundSync(password, newWptr); + setupBackgroundSync(password, newW); - wptr = newWptr; - monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); - monero.Wallet_store(wptr!, path: path); - openedWalletsByPath[path] = wptr!; + currentWallet = newW; + currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase); + currentWallet!.store(path: path); + openedWalletsByPath[path] = currentWallet!; _lastOpenedWallet = path; - - // is the line below needed? - // setupNodeSync(address: "node.moneroworld.com:18089"); } -bool isWalletExistSync({required String path}) { - return monero.WalletManager_walletExists(wmPtr, path); +bool isWalletExist({required String path}) { + return wmPtr.walletExists(path); } void restoreWalletFromSeedSync( @@ -113,8 +112,7 @@ void restoreWalletFromSeedSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - final newWptr = monero.WalletManager_recoveryWallet( - wmPtr, + final newW = wmPtr.recoveryWallet( path: path, password: password, mnemonic: seed, @@ -123,10 +121,10 @@ void restoreWalletFromSeedSync( networkType: 0, ); - final status = monero.Wallet_status(newWptr); + final status = newW.status(); if (status != 0) { - final error = monero.Wallet_errorString(newWptr); + final error = newW.errorString(); if (error.contains('word list failed verification')) { throw WalletRestoreFromSeedException( message: "Seed verification failed, please make sure you entered the correct seed with the correct words order", @@ -134,20 +132,20 @@ void restoreWalletFromSeedSync( } throw WalletRestoreFromSeedException(message: error); } - wptr = newWptr; + currentWallet = newW; setRefreshFromBlockHeight(height: restoreHeight); - setupBackgroundSync(password, newWptr); + setupBackgroundSync(password, newW); - monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); + currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase); - openedWalletsByPath[path] = wptr!; + openedWalletsByPath[path] = currentWallet!; - monero.Wallet_store(wptr!); + currentWallet!.store(path: path); _lastOpenedWallet = path; } -void restoreWalletFromKeysSync( +void restoreWalletFromKeys( {required String path, required String password, required String language, @@ -157,8 +155,8 @@ void restoreWalletFromKeysSync( int nettype = 0, int restoreHeight = 0}) { txhistory = null; - var newWptr = (spendKey != "") - ? monero.WalletManager_createDeterministicWalletFromSpendKey(wmPtr, + var newW = (spendKey != "") + ? wmPtr.createDeterministicWalletFromSpendKey( path: path, password: password, language: language, @@ -166,8 +164,7 @@ void restoreWalletFromKeysSync( newWallet: true, // TODO(mrcyjanek): safe to remove restoreHeight: restoreHeight) - : monero.WalletManager_createWalletFromKeys( - wmPtr, + : wmPtr.createWalletFromKeys( path: path, password: password, restoreHeight: restoreHeight, @@ -177,22 +174,21 @@ void restoreWalletFromKeysSync( nettype: 0, ); - int status = monero.Wallet_status(newWptr); + int status = newW.status(); if (status != 0) { throw WalletRestoreFromKeysException( - message: monero.Wallet_errorString(newWptr)); + message: newW.errorString()); } // CW-712 - Try to restore deterministic wallet first, if the view key doesn't // match the view key provided if (spendKey != "") { - final viewKeyRestored = monero.Wallet_secretViewKey(newWptr); + final viewKeyRestored = newW.secretViewKey(); if (viewKey != viewKeyRestored && viewKey != "") { - monero.WalletManager_closeWallet(wmPtr, newWptr, false); + wmPtr.closeWallet(newW, false); File(path).deleteSync(); File(path + ".keys").deleteSync(); - newWptr = monero.WalletManager_createWalletFromKeys( - wmPtr, + newW = wmPtr.createWalletFromKeys( path: path, password: password, restoreHeight: restoreHeight, @@ -201,19 +197,19 @@ void restoreWalletFromKeysSync( spendKeyString: spendKey, nettype: 0, ); - int status = monero.Wallet_status(newWptr); + int status = newW.status(); if (status != 0) { throw WalletRestoreFromKeysException( - message: monero.Wallet_errorString(newWptr)); + message: newW.errorString()); } - setupBackgroundSync(password, newWptr); + setupBackgroundSync(password, newW); } } - wptr = newWptr; + currentWallet = newW; - openedWalletsByPath[path] = wptr!; + openedWalletsByPath[path] = currentWallet!; _lastOpenedWallet = path; } @@ -228,8 +224,7 @@ void restoreWalletFromPolyseedWithOffset( int nettype = 0}) { txhistory = null; - final newWptr = monero.WalletManager_createWalletFromPolyseed( - wmPtr, + final newW = wmPtr.createWalletFromPolyseed( path: path, password: password, networkType: nettype, @@ -240,24 +235,24 @@ void restoreWalletFromPolyseedWithOffset( kdfRounds: 1, ); - int status = monero.Wallet_status(newWptr); + int status = newW.status(); if (status != 0) { - final err = monero.Wallet_errorString(newWptr); + final err = newW.errorString(); printV("err: $err"); throw WalletRestoreFromKeysException(message: err); } - wptr = newWptr; + currentWallet = newW; - monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); - monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: seedOffset); - monero.Wallet_store(wptr!); + currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed); + currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: seedOffset); + currentWallet!.store(path: path); - setupBackgroundSync(password, newWptr); + setupBackgroundSync(password, currentWallet!); storeSync(); - openedWalletsByPath[path] = wptr!; + openedWalletsByPath[path] = currentWallet!; } @@ -282,8 +277,7 @@ void restoreWalletFromSpendKeySync( // ); txhistory = null; - final newWptr = monero.WalletManager_createDeterministicWalletFromSpendKey( - wmPtr, + final newW = wmPtr.createDeterministicWalletFromSpendKey( path: path, password: password, language: language, @@ -292,23 +286,23 @@ void restoreWalletFromSpendKeySync( restoreHeight: restoreHeight, ); - int status = monero.Wallet_status(newWptr); + int status = newW.status(); if (status != 0) { - final err = monero.Wallet_errorString(newWptr); + final err = newW.errorString(); printV("err: $err"); throw WalletRestoreFromKeysException(message: err); } - wptr = newWptr; + currentWallet = newW; - monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed); storeSync(); - setupBackgroundSync(password, newWptr); + setupBackgroundSync(password, currentWallet!); - openedWalletsByPath[path] = wptr!; + openedWalletsByPath[path] = currentWallet!; _lastOpenedWallet = path; } @@ -321,41 +315,42 @@ Future restoreWalletFromHardwareWallet( int nettype = 0, int restoreHeight = 0}) async { txhistory = null; - + final wmPtr = MoneroWalletManagerFactory().getWalletManager().ffiAddress(); final newWptrAddr = await Isolate.run(() { - return monero.WalletManager_createWalletFromDevice(wmPtr, + return monero.WalletManager_createWalletFromDevice(Pointer.fromAddress(wmPtr), path: path, password: password, restoreHeight: restoreHeight, deviceName: deviceName) .address; }); - final newWptr = Pointer.fromAddress(newWptrAddr); + final newW = MoneroWallet(Pointer.fromAddress(newWptrAddr)); - final status = monero.Wallet_status(newWptr); + final status = newW.status(); if (status != 0) { - final error = monero.Wallet_errorString(newWptr); + final error = newW.errorString(); throw WalletRestoreFromSeedException(message: error); } - wptr = newWptr; + currentWallet = newW; + currentWallet!.store(path: path); _lastOpenedWallet = path; - openedWalletsByPath[path] = wptr!; + openedWalletsByPath[path] = currentWallet!; } -Map openedWalletsByPath = {}; +Map openedWalletsByPath = {}; Future loadWallet( {required String path, required String password, int nettype = 0}) async { if (openedWalletsByPath[path] != null) { txhistory = null; - wptr = openedWalletsByPath[path]!; + currentWallet = openedWalletsByPath[path]!; return; } - if (wptr == null || path != _lastOpenedWallet) { - if (wptr != null) { - final addr = wptr!.address; + if (currentWallet == null || path != _lastOpenedWallet) { + if (currentWallet != null) { + final addr = currentWallet!.ffiAddress(); Isolate.run(() { monero.Wallet_store(Pointer.fromAddress(addr)); }); @@ -366,19 +361,24 @@ Future loadWallet( /// 0: Software Wallet /// 1: Ledger /// 2: Trezor - late final deviceType; + var deviceType = 0; if (Platform.isAndroid || Platform.isIOS) { - deviceType = monero.WalletManager_queryWalletDevice( - wmPtr, + deviceType = wmPtr.queryWalletDevice( keysFileName: "$path.keys", password: password, kdfRounds: 1, ); - final status = monero.WalletManager_errorString(wmPtr); + final status = wmPtr.errorString(); if (status != "") { printV("loadWallet:"+status); - throw WalletOpeningException(message: status); + // This is most likely closeWallet call leaking error. This is fine. + if (status.contains("failed to save file")) { + printV("loadWallet: error leaked: $status"); + deviceType = 0; + } else { + throw WalletOpeningException(message: status); + } } } else { deviceType = 0; @@ -388,107 +388,47 @@ Future loadWallet( if (gLedger == null) { throw Exception("Tried to open a ledger wallet with no ledger connected"); } - final dummyWPtr = wptr ?? - monero.WalletManager_openWallet(wmPtr, path: '', password: ''); + final dummyWPtr = (currentWallet ?? + wmPtr.openWallet(path: '', password: '')); enableLedgerExchange(dummyWPtr, gLedger!); } - final addr = wmPtr.address; + final addr = wmPtr.ffiAddress(); final newWptrAddr = await Isolate.run(() { return monero.WalletManager_openWallet(Pointer.fromAddress(addr), path: path, password: password) .address; }); - final newWptr = Pointer.fromAddress(newWptrAddr); + final newW = MoneroWallet(Pointer.fromAddress(newWptrAddr)); - int status = monero.Wallet_status(newWptr); + int status = newW.status(); if (status != 0) { - final err = monero.Wallet_errorString(newWptr); + final err = newW.errorString(); printV("loadWallet:"+err); throw WalletOpeningException(message: err); } if (deviceType == 0) { - setupBackgroundSync(password, newWptr); + setupBackgroundSync(password, newW); } - wptr = newWptr; + currentWallet = newW; _lastOpenedWallet = path; - openedWalletsByPath[path] = wptr!; + openedWalletsByPath[path] = currentWallet!; } } -void setupBackgroundSync(String password, Pointer? wptrOverride) { - if (isViewOnlyBySpendKey(wptrOverride)) { +void setupBackgroundSync(String password, Wallet2Wallet wallet) { + if (isViewOnlyBySpendKey(wallet)) { return; } - monero.Wallet_setupBackgroundSync(wptrOverride ?? wptr!, backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); - if (monero.Wallet_status(wptrOverride ?? wptr!) != 0) { + wallet.setupBackgroundSync(backgroundSyncType: 2, walletPassword: password, backgroundCachePassword: ''); + if (wallet.status() != 0) { // We simply ignore the error. - printV("setupBackgroundSync: ${monero.Wallet_errorString(wptrOverride ?? wptr!)}"); + printV("setupBackgroundSync: ${wallet.errorString()}"); } } -void _createWallet(Map args) { - final path = args['path'] as String; - final password = args['password'] as String; - final language = args['language'] as String; - final passphrase = args['passphrase'] as String; - - createWalletSync(path: path, password: password, language: language, passphrase: passphrase); -} - -void _restoreFromSeed(Map args) { - final path = args['path'] as String; - final password = args['password'] as String; - final passphrase = args['passphrase'] as String; - final seed = args['seed'] as String; - final restoreHeight = args['restoreHeight'] as int; - - return restoreWalletFromSeedSync( - path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight); -} - -void _restoreFromKeys(Map args) { - final path = args['path'] as String; - final password = args['password'] as String; - final language = args['language'] as String; - final restoreHeight = args['restoreHeight'] as int; - final address = args['address'] as String; - final viewKey = args['viewKey'] as String; - final spendKey = args['spendKey'] as String; - - restoreWalletFromKeysSync( - path: path, - password: password, - language: language, - restoreHeight: restoreHeight, - address: address, - viewKey: viewKey, - spendKey: spendKey); -} - -void _restoreFromSpendKey(Map args) { - final path = args['path'] as String; - final password = args['password'] as String; - final seed = args['seed'] as String; - final language = args['language'] as String; - final spendKey = args['spendKey'] as String; - final restoreHeight = args['restoreHeight'] as int; - - restoreWalletFromSpendKeySync( - path: path, - password: password, - seed: seed, - language: language, - restoreHeight: restoreHeight, - spendKey: spendKey); -} - -Future _openWallet(Map args) async => loadWallet( - path: args['path'] as String, password: args['password'] as String); - -bool _isWalletExist(String path) => isWalletExistSync(path: path); Future openWallet( {required String path, @@ -496,77 +436,4 @@ Future openWallet( int nettype = 0}) async => loadWallet(path: path, password: password, nettype: nettype); -Future openWalletAsync(Map args) async => - _openWallet(args); - -Future createWallet( - {required String path, - required String password, - required String language, - required String passphrase, - int nettype = 0}) async => - _createWallet({ - 'path': path, - 'password': password, - 'language': language, - 'passphrase': passphrase, - 'nettype': nettype - }); - -void restoreFromSeed( - {required String path, - required String password, - required String passphrase, - required String seed, - int nettype = 0, - int restoreHeight = 0}) => - _restoreFromSeed({ - 'path': path, - 'password': password, - 'passphrase': passphrase, - 'seed': seed, - 'nettype': nettype, - 'restoreHeight': restoreHeight - }); - -Future restoreFromKeys( - {required String path, - required String password, - required String language, - required String address, - required String viewKey, - required String spendKey, - int nettype = 0, - int restoreHeight = 0}) async => - _restoreFromKeys({ - 'path': path, - 'password': password, - 'language': language, - 'address': address, - 'viewKey': viewKey, - 'spendKey': spendKey, - 'nettype': nettype, - 'restoreHeight': restoreHeight - }); - -Future restoreFromSpendKey( - {required String path, - required String password, - required String seed, - required String language, - required String spendKey, - int nettype = 0, - int restoreHeight = 0}) async => - _restoreFromSpendKey({ - 'path': path, - 'password': password, - 'seed': seed, - 'language': language, - 'spendKey': spendKey, - 'nettype': nettype, - 'restoreHeight': restoreHeight - }); - -bool isWalletExist({required String path}) => _isWalletExist(path); - -bool isViewOnlyBySpendKey(Pointer? wptrOverride) => int.tryParse(monero.Wallet_secretSpendKey(wptrOverride ?? wptr!)) == 0; +bool isViewOnlyBySpendKey(Wallet2Wallet? wallet) => int.tryParse((wallet??currentWallet!).secretSpendKey()) == 0; diff --git a/cw_monero/lib/ledger.dart b/cw_monero/lib/ledger.dart index 219d45bb5..d931b58d0 100644 --- a/cw_monero/lib/ledger.dart +++ b/cw_monero/lib/ledger.dart @@ -7,26 +7,26 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:ffi/ffi.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus_dart.dart'; -import 'package:monero/monero.dart' as monero; +import 'package:monero/src/wallet2.dart'; LedgerConnection? gLedger; Timer? _ledgerExchangeTimer; Timer? _ledgerKeepAlive; -void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) { +void enableLedgerExchange(Wallet2Wallet wallet, LedgerConnection connection) { _ledgerExchangeTimer?.cancel(); _ledgerExchangeTimer = Timer.periodic(Duration(milliseconds: 1), (_) async { - final ledgerRequestLength = monero.Wallet_getSendToDeviceLength(ptr); - final ledgerRequest = monero.Wallet_getSendToDevice(ptr) + final ledgerRequestLength = wallet.getSendToDeviceLength(); + final ledgerRequest = wallet.getSendToDevice() .cast() .asTypedList(ledgerRequestLength); if (ledgerRequestLength > 0) { _ledgerKeepAlive?.cancel(); final Pointer emptyPointer = malloc(0); - monero.Wallet_setDeviceSendData( - ptr, emptyPointer.cast(), 0); + wallet.setDeviceSendData( + emptyPointer.cast(), 0); malloc.free(emptyPointer); _logLedgerCommand(ledgerRequest, false); @@ -45,8 +45,8 @@ void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) { result.asTypedList(response.length)[i] = response[i]; } - monero.Wallet_setDeviceReceivedData( - ptr, result.cast(), response.length); + wallet.setDeviceReceivedData( + result.cast(), response.length); malloc.free(result); keepAlive(connection); } diff --git a/cw_monero/lib/monero_account_list.dart b/cw_monero/lib/monero_account_list.dart index c9a48a939..4190a4a1e 100644 --- a/cw_monero/lib/monero_account_list.dart +++ b/cw_monero/lib/monero_account_list.dart @@ -4,7 +4,7 @@ import 'package:cw_monero/api/wallet_manager.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/account.dart'; import 'package:cw_monero/api/account_list.dart' as account_list; -import 'package:monero/monero.dart' as monero; +import 'package:monero/src/monero.dart'; part 'monero_account_list.g.dart'; @@ -50,32 +50,32 @@ abstract class MoneroAccountListBase with Store { List getAll() { final allAccounts = account_list.getAllAccount(); final currentCount = allAccounts.length; - cachedAccounts[account_list.wptr!.address] ??= []; + cachedAccounts[account_list.currentWallet!.ffiAddress()] ??= []; - if (cachedAccounts[account_list.wptr!.address]!.length == currentCount) { - return cachedAccounts[account_list.wptr!.address]!; + if (cachedAccounts[account_list.currentWallet!.ffiAddress()]!.length == currentCount) { + return cachedAccounts[account_list.currentWallet!.ffiAddress()]!; } - cachedAccounts[account_list.wptr!.address] = allAccounts.map((accountRow) { - final balance = monero.SubaddressAccountRow_getUnlockedBalance(accountRow); + cachedAccounts[account_list.currentWallet!.ffiAddress()] = allAccounts.map((accountRow) { + final balance = accountRow.getUnlockedBalance(); return Account( - id: monero.SubaddressAccountRow_getRowId(accountRow), - label: monero.SubaddressAccountRow_getLabel(accountRow), - balance: moneroAmountToString(amount: monero.Wallet_amountFromString(balance)), + id: accountRow.getRowId(), + label: accountRow.getLabel(), + balance: moneroAmountToString(amount: account_list.currentWallet!.amountFromString(balance)), ); }).toList(); - return cachedAccounts[account_list.wptr!.address]!; + return cachedAccounts[account_list.currentWallet!.ffiAddress()]!; } - Future addAccount({required String label}) async { - await account_list.addAccount(label: label); + void addAccount({required String label}) { + account_list.addAccount(label: label); update(); } - Future setLabelAccount({required int accountIndex, required String label}) async { - await account_list.setLabelForAccount(accountIndex: accountIndex, label: label); + void setLabelAccount({required int accountIndex, required String label}) { + account_list.setLabelForAccount(accountIndex: accountIndex, label: label); update(); } diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart index 224e33b2a..a88b98323 100644 --- a/cw_monero/lib/monero_unspent.dart +++ b/cw_monero/lib/monero_unspent.dart @@ -1,7 +1,7 @@ import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_monero/api/coins_info.dart'; -import 'package:monero/monero.dart' as monero; +import 'package:monero/src/monero.dart'; class MoneroUnspent extends Unspent { static Future fromUnspent(String address, String hash, String keyImage, int value, bool isFrozen, bool isUnlocked) async { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 69c894908..7a4d943fe 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -39,6 +39,7 @@ import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; +import 'package:monero/src/monero.dart' as m; import 'package:monero/monero.dart' as monero; part 'monero_wallet.g.dart'; @@ -84,7 +85,7 @@ abstract class MoneroWalletBase extends WalletBase isEnabledAutoGenerateSubaddress, (bool enabled) { @@ -139,7 +140,7 @@ abstract class MoneroWalletBase extends WalletBase - transactionHistory.transactions.values.firstOrNull?.height ?? monero.Wallet_getRefreshFromBlockHeight(wptr!); + transactionHistory.transactions.values.firstOrNull?.height ?? currentWallet?.getRefreshFromBlockHeight(); monero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; @@ -169,7 +170,7 @@ abstract class MoneroWalletBase extends WalletBase connectToNode({required Node node}) async { try { syncStatus = ConnectingSyncStatus(); - await monero_wallet.setupNode( + await monero_wallet.setupNodeSync( address: node.uri.toString(), login: node.login, password: node.password, @@ -237,10 +247,10 @@ abstract class MoneroWalletBase extends WalletBase stopSync() async { if (isBackgroundSyncRunning) { printV("Stopping background sync"); - monero.Wallet_store(wptr!); - monero.Wallet_stopBackgroundSync(wptr!, ''); - monero_wallet.store(); + currentWallet!.store(); + currentWallet!.stopBackgroundSync(''); + currentWallet!.store(); isBackgroundSyncRunning = false; } await save(); @@ -269,9 +279,9 @@ abstract class MoneroWalletBase extends WalletBase stopBackgroundSync(String password) async { if (isBackgroundSyncRunning) { printV("Stopping background sync"); - monero.Wallet_store(wptr!); - monero.Wallet_stopBackgroundSync(wptr!, password); - monero.Wallet_store(wptr!); + currentWallet!.store(); + currentWallet!.stopBackgroundSync(password); + currentWallet!.store(); isBackgroundSyncRunning = false; } } @@ -308,44 +318,44 @@ abstract class MoneroWalletBase extends WalletBase submitTransactionUR(String ur) async { - final retStatus = monero.Wallet_submitTransactionUR(wptr!, ur); - final status = monero.Wallet_status(wptr!); + final retStatus = currentWallet!.submitTransactionUR(ur); + final status = currentWallet!.status(); if (status != 0) { - final err = monero.Wallet_errorString(wptr!); + final err = currentWallet!.errorString(); throw MoneroTransactionCreationException("unable to broadcast signed transaction: $err"); } return retStatus; } bool importKeyImagesUR(String ur) { - final retStatus = monero.Wallet_importKeyImagesUR(wptr!, ur); - final status = monero.Wallet_status(wptr!); + final retStatus = currentWallet!.importKeyImagesUR(ur); + final status = currentWallet!.status(); if (status != 0) { - final err = monero.Wallet_errorString(wptr!); + final err = currentWallet!.errorString(); throw Exception("unable to import key images: $err"); } return retStatus; } String exportOutputsUR(bool all) { - final str = monero.Wallet_exportOutputsUR(wptr!, all: all); - final status = monero.Wallet_status(wptr!); + final str = currentWallet!.exportOutputsUR(all: all); + final status = currentWallet!.status(); if (status != 0) { - final err = monero.Wallet_errorString(wptr!); + final err = currentWallet!.errorString(); throw MoneroTransactionCreationException("unable to export UR: $err"); } return str; } bool needExportOutputs(int amount) { - if (int.tryParse(monero.Wallet_secretSpendKey(wptr!)) != 0) { + if (int.tryParse(currentWallet!.secretSpendKey()) != 0) { return false; } // viewOnlyBalance - balance that we can spend // TODO(mrcyjanek): remove hasUnknownKeyImages when we cleanup coin control - return (monero.Wallet_viewOnlyBalance(wptr!, + return (currentWallet!.viewOnlyBalance( accountIndex: walletAddresses.account!.id) < amount) || - monero.Wallet_hasUnknownKeyImages(wptr!); + currentWallet!.hasUnknownKeyImages(); } @override @@ -425,12 +435,13 @@ abstract class MoneroWalletBase extends WalletBase _askForUpdateTransactionHistory() async => - await updateTransactions(); - int _getUnlockedBalance() => monero_wallet.getUnlockedBalance( accountIndex: walletAddresses.account!.id); @@ -852,13 +874,13 @@ abstract class MoneroWalletBase extends WalletBase.delayed(Duration(seconds: 1)); } catch (e) { @@ -917,8 +939,7 @@ abstract class MoneroWalletBase extends WalletBase info.id == WalletBase.idFor(name, getType())); final wallet = MoneroWallet( @@ -217,13 +220,23 @@ class MoneroWalletService extends WalletService< if (openedWalletsByPath["$path/$wallet"] != null) { // NOTE: this is realistically only required on windows. printV("closing wallet"); - final wmaddr = wmPtr.address; - final waddr = openedWalletsByPath["$path/$wallet"]!.address; - // await Isolate.run(() { - monero.WalletManager_closeWallet( - Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), false); - // }); + final w = openedWalletsByPath["$path/$wallet"]!; + final wmaddr = wmPtr.ffiAddress(); + final waddr = w.ffiAddress(); openedWalletsByPath.remove("$path/$wallet"); + if (Platform.isWindows) { + await Isolate.run(() { + monero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); + monero.WalletManager_errorString(Pointer.fromAddress(wmaddr)); + }); + } else { + unawaited(Isolate.run(() { + monero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); + monero.WalletManager_errorString(Pointer.fromAddress(wmaddr)); + })); + } printV("wallet closed"); } @@ -263,7 +276,7 @@ class MoneroWalletService extends WalletService< {bool? isTestnet}) async { try { final path = await pathForWallet(name: credentials.name, type: getType()); - await monero_wallet_manager.restoreFromKeys( + monero_wallet_manager.restoreWalletFromKeys( path: path, password: credentials.password!, language: credentials.language, @@ -293,9 +306,13 @@ class MoneroWalletService extends WalletService< final password = credentials.password; final height = credentials.height; - if (wptr == null) monero_wallet_manager.createWalletPointer(); + if (currentWallet == null) { + final tmpWptr = monero_wallet_manager.createWalletPointer(); + enableLedgerExchange(tmpWptr, credentials.ledgerConnection); + } else { + enableLedgerExchange(currentWallet!, credentials.ledgerConnection); + } - enableLedgerExchange(wptr!, credentials.ledgerConnection); await monero_wallet_manager.restoreWalletFromHardwareWallet( path: path, password: password!, @@ -352,7 +369,7 @@ class MoneroWalletService extends WalletService< try { final path = await pathForWallet(name: credentials.name, type: getType()); - monero_wallet_manager.restoreFromSeed( + monero_wallet_manager.restoreWalletFromSeedSync( path: path, password: credentials.password!, passphrase: credentials.passphrase, @@ -393,7 +410,7 @@ class MoneroWalletService extends WalletService< walletInfo.isRecovery = true; walletInfo.restoreHeight = height; - monero_wallet_manager.restoreFromSeed( + monero_wallet_manager.restoreWalletFromSeedSync( path: path, password: password, passphrase: '', @@ -401,12 +418,12 @@ class MoneroWalletService extends WalletService< restoreHeight: height, ); - monero.Wallet_setCacheAttribute(wptr!, + currentWallet!.setCacheAttribute( key: "cakewallet.seed.bip39", value: mnemonic); - monero.Wallet_setCacheAttribute(wptr!, + currentWallet!.setCacheAttribute( key: "cakewallet.passphrase", value: passphrase ?? ''); - monero.Wallet_store(wptr!); + currentWallet!.store(); final wallet = MoneroWallet( walletInfo: walletInfo, @@ -472,7 +489,7 @@ class MoneroWalletService extends WalletService< walletInfo.isRecovery = true; walletInfo.restoreHeight = height; - await monero_wallet_manager.restoreFromSpendKey( + monero_wallet_manager.restoreWalletFromSpendKeySync( path: path, password: password, seed: seed, @@ -481,8 +498,8 @@ class MoneroWalletService extends WalletService< spendKey: spendKey); - monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); - monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??''); + currentWallet!.setCacheAttribute(key: "cakewallet.seed", value: seed); + currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase??''); final wallet = MoneroWallet( walletInfo: walletInfo, @@ -529,7 +546,7 @@ class MoneroWalletService extends WalletService< if (walletFilesExist(path)) await repairOldAndroidWallet(name); await monero_wallet_manager - .openWalletAsync({'path': path, 'password': password}); + .openWallet(path: path, password: password); final walletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); final wallet = MoneroWallet( diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 278226b07..e48cf1dca 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -573,8 +573,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "84e52393e395d75f449bcd81e23028889538118f" - resolved-ref: "84e52393e395d75f449bcd81e23028889538118f" + ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 + resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 0e1537ee0..90d0b85ab 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 84e52393e395d75f449bcd81e23028889538118f + ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 path: impls/monero.dart mutex: ^3.1.0 ledger_flutter_plus: ^1.4.1 diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 404060e43..66fd1fc56 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -480,8 +480,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "84e52393e395d75f449bcd81e23028889538118f" - resolved-ref: "84e52393e395d75f449bcd81e23028889538118f" + ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 + resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index b35f36071..7b9ec4c41 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 84e52393e395d75f449bcd81e23028889538118f # monero_c hash + ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 # monero_c hash path: impls/monero.dart mutex: ^3.1.0 diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock index 50835ed3f..b5486e0c1 100644 --- a/cw_zano/pubspec.lock +++ b/cw_zano/pubspec.lock @@ -485,8 +485,8 @@ packages: dependency: "direct main" description: path: "impls/monero.dart" - ref: "84e52393e395d75f449bcd81e23028889538118f" - resolved-ref: "84e52393e395d75f449bcd81e23028889538118f" + ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 + resolved-ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 url: "https://github.com/mrcyjanek/monero_c" source: git version: "0.0.0" diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml index 9bf9c66eb..ff2c1e9a6 100644 --- a/cw_zano/pubspec.yaml +++ b/cw_zano/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: monero: git: url: https://github.com/mrcyjanek/monero_c - ref: 84e52393e395d75f449bcd81e23028889538118f # monero_c hash + ref: b335585a7fb94b315eb52bd88f2da6d3489fa508 # monero_c hash path: impls/monero.dart dev_dependencies: flutter_test: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 50ca9d6de..3d39d589e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -208,42 +208,40 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d + connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 - cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a - cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 - device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 + cw_decred: a02cf30175a46971c1e2fa22c48407534541edc6 + cw_mweb: 3aea2fb35b2bd04d8b2d21b83216f3b8fb768d85 + device_display_brightness: 04374ebd653619292c1d996f00f42877ea19f17f + device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 + devicelocale: bd64aa714485a8afdaded0892c1e7d5b7f680cf8 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3 + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 - flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb - flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f - flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 - flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f - in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f + flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4 + flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - reown_yttrium: c0e87e5965fa60a3559564cc35cffbba22976089 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 - sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 + sensitive_clipboard: 161e9abc3d56b3131309d8a321eb4690a803c16b + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sp_scanner: b1bc9321690980bdb44bba7ec85d5543e716d1b5 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e - YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966 + uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152 + universal_ble: ff19787898040d721109c6324472e5dd4bc86adc + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 49f366808..382b1d6c2 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -32,23 +32,28 @@ class WalletLoadingService { Future renameWallet(WalletType type, String name, String newName, {String? password}) async { - final walletService = walletServiceFactory.call(type); - final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name)); + try { + final walletService = walletServiceFactory.call(type); + final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name)); - // Save the current wallet's password to the new wallet name's key - await keyService.saveWalletPassword(walletName: newName, password: walletPassword); - // Delete previous wallet name from keyService to keep only new wallet's name - // otherwise keeps duplicate (old and new names) - await keyService.deleteWalletPassword(walletName: name); + // Save the current wallet's password to the new wallet name's key + await keyService.saveWalletPassword(walletName: newName, password: walletPassword); - await walletService.rename(name, walletPassword, newName); + await walletService.rename(name, walletPassword, newName); + // Delete previous wallet name from keyService to keep only new wallet's name + // otherwise keeps duplicate (old and new names) + await keyService.deleteWalletPassword(walletName: name); - // set shared preferences flag based on previous wallet name - if (type == WalletType.monero) { - final oldNameKey = PreferencesKey.moneroWalletUpdateV1Key(name); - final isPasswordUpdated = sharedPreferences.getBool(oldNameKey) ?? false; - final newNameKey = PreferencesKey.moneroWalletUpdateV1Key(newName); - await sharedPreferences.setBool(newNameKey, isPasswordUpdated); + // set shared preferences flag based on previous wallet name + if (type == WalletType.monero) { + final oldNameKey = PreferencesKey.moneroWalletUpdateV1Key(name); + final isPasswordUpdated = sharedPreferences.getBool(oldNameKey) ?? false; + final newNameKey = PreferencesKey.moneroWalletUpdateV1Key(newName); + await sharedPreferences.setBool(newNameKey, isPasswordUpdated); + } + } catch (error, stack) { + await ExceptionHandler.resetLastPopupDate(); + await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack)); } } diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index b6be123c2..329c87338 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -39,14 +39,14 @@ class CWMoneroAccountList extends MoneroAccountList { @override Future addAccount(Object wallet, {required String label}) async { final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.accountList.addAccount(label: label); + moneroWallet.walletAddresses.accountList.addAccount(label: label); } @override Future setLabelAccount(Object wallet, {required int accountIndex, required String label}) async { final moneroWallet = wallet as MoneroWallet; - await moneroWallet.walletAddresses.accountList + moneroWallet.walletAddresses.accountList .setLabelAccount(accountIndex: accountIndex, label: label); } } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 00353f117..b60533130 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -271,10 +271,19 @@ abstract class DashboardViewModelBase with Store { }); _transactionDisposer?.reaction.dispose(); - _transactionDisposer = reaction( - (_) => appStore.wallet!.transactionHistory.transactions.length, - _transactionDisposerCallback, - ); + _transactionDisposer = reaction((_) { + final length = appStore.wallet!.transactionHistory.transactions.length; + if (length == 0) { + return 0; + } + int confirmations = 1; + if (![WalletType.solana, WalletType.tron].contains(wallet.type)) { + try { + confirmations = appStore.wallet!.transactionHistory.transactions.values.first.confirmations + 1; + } catch (_) {} + } + return length * confirmations; + }, _transactionDisposerCallback); if (hasSilentPayments) { silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet); @@ -891,8 +900,19 @@ abstract class DashboardViewModelBase with Store { _transactionDisposer?.reaction.dispose(); - _transactionDisposer = reaction((_) => appStore.wallet!.transactionHistory.transactions.length, - _transactionDisposerCallback); + _transactionDisposer = reaction((_) { + final length = appStore.wallet!.transactionHistory.transactions.length; + if (length == 0) { + return 0; + } + int confirmations = 1; + if (![WalletType.solana, WalletType.tron].contains(wallet.type)) { + try { + confirmations = appStore.wallet!.transactionHistory.transactions.values.first.confirmations + 1; + } catch (_) {} + } + return length * confirmations; + }, _transactionDisposerCallback); } @action diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index b16edabd2..01905ba34 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart'; @@ -591,7 +593,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } final sharedPreferences = await SharedPreferences.getInstance(); await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), DateTime.now().add(Duration(minutes: 1)).toIso8601String()); - + unawaited(wallet.fetchTransactions()); state = TransactionCommitted(); } catch (e) { state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 133c0ab2f..b49ad2c34 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -8,7 +8,7 @@ if [[ ! -d "monero_c/.git" ]]; then git clone https://github.com/mrcyjanek/monero_c --branch master monero_c cd monero_c - git checkout 84e52393e395d75f449bcd81e23028889538118f + git checkout b335585a7fb94b315eb52bd88f2da6d3489fa508 git reset --hard git submodule update --init --force --recursive ./apply_patches.sh monero From eccc94a019b9c6dcc6473640605eb14aa10d3c3f Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 5 May 2025 11:50:04 +0300 Subject: [PATCH 12/90] temp. ignore secure storage iOS issue --- lib/utils/exception_handler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 7f6571f3f..e5b1c3579 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -225,7 +225,7 @@ class ExceptionHandler { // just ignoring until we find a solution to this issue or migrate from flutter secure storage "core/auth_service.dart:64", "core/key_service.dart:14", - "core/wallet_loading_service.dart:131", + "core/wallet_loading_service.dart:134", ]; static Future _addDeviceInfo(File file) async { From b7473594cb30b317602b14c8bf9b15e5443c5b14 Mon Sep 17 00:00:00 2001 From: Papakia <111633267+Papakia887@users.noreply.github.com> Date: Mon, 5 May 2025 18:55:11 +0900 Subject: [PATCH 13/90] Update strings_ko.arb (#2250) --- res/values/strings_ko.arb | 1582 ++++++++++++++++++------------------- 1 file changed, 791 insertions(+), 791 deletions(-) diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 6dffa5db4..e5ffe877e 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -1,5 +1,5 @@ { - "about_cake_pay": "Cake Pay를 사용하면 미국 내 150,000개 이상의 가맹점에서 즉시 사용할 수 있는 가상 자산이 포함된 기프트 카드를 쉽게 구입할 수 있습니다.", + "about_cake_pay": "Cake Pay를 사용하면 가상 자산으로 기프트 카드를 쉽게 구매하여 미국 내 150,000개 이상의 가맹점에서 즉시 사용할 수 있습니다.", "account": "계정", "accounts": "계정", "accounts_subaddresses": "계정 및 하위 주소", @@ -7,751 +7,752 @@ "active": "활성", "active_cards": "활성 카드", "activeConnectionsPrompt": "활성 연결이 여기에 표시됩니다", - "add": "더하다", - "add_contact": "주소록에 추가", - "add_contact_to_address_book": "이 연락처를 주소록에 추가 하시겠습니까?", - "add_custom_node": "새 사용자 정의 노드 추가", - "add_custom_redemption": "사용자 지정 상환 추가", - "add_fund_to_card": "카드에 선불 금액 추가(최대 ${value})", + "add": "추가", + "add_contact": "연락처 추가", + "add_contact_to_address_book": "이 연락처를 주소록에 추가하시겠습니까?", + "add_custom_node": "새 사용자 지정 노드 추가", + "add_custom_redemption": "사용자 지정 사용 추가", + "add_fund_to_card": "카드에 선불 자금 추가 (${value}까지)", "add_new_node": "새 노드 추가", - "add_new_word": "새로운 단어 추가", - "add_passphrase": "암호를 추가하십시오", - "add_passphrase_warning_text": "과거 에이 지갑에 사용한 경우에만 암호를 입력하십시오. 이 지갑에서 잘못된 암호를 입력하거나 암호를 사용하지 않은 경우 기존 자금이나 이력이 보이지 않을 것입니다.", - "add_receiver": "다른 수신기 추가(선택 사항)", - "add_secret_code": "또는 이 비밀 코드를 인증 앱에 추가하세요.", + "add_new_word": "새 단어 추가", + "add_passphrase": "암호 구문 추가", + "add_passphrase_warning_text": "과거에 이 지갑에 암호 구문을 사용한 경우에만 입력하십시오. 잘못된 암호 구문을 입력하거나 이 지갑에서 암호 구문을 사용한 적이 없다면 기존 자금이나 내역이 보이지 않습니다.", + "add_receiver": "다른 수신자 추가 (선택 사항)", + "add_secret_code": "또는 이 비밀 코드를 인증 앱에 추가하세요", "add_tip": "팁 추가", - "add_token_disclaimer_check": "신뢰할 수 있는 출처를 통해 토큰 컨트랙트 주소와 정보를 확인했습니다. 악의적이거나 잘못된 정보를 추가하면 자금 손실이 발생할 수 있습니다.", - "add_token_warning": "사기꾼의 지시에 따라 토큰을 편집하거나 추가하지 마십시오.\n항상 신뢰할 수 있는 출처를 통해 토큰 주소를 확인하세요!", + "add_token_disclaimer_check": "신뢰할 수 있는 출처를 통해 토큰 계약 주소와 정보를 확인했습니다. 악의적이거나 잘못된 정보를 추가하면 자금 손실이 발생할 수 있습니다.", + "add_token_warning": "사기꾼의 지시에 따라 토큰을 편집하거나 추가하지 마십시오.\n항상 신뢰할 수 있는 출처를 통해 토큰 주소를 확인하십시오!", "add_value": "값 추가", "added_message_for_ata_error": "거래를 완료하기에 충분한 SOL 잔액이 있는지 확인하십시오.", "address": "주소", "address_book": "주소록", "address_book_menu": "주소록", - "address_detected": "주소 감지", - "address_from_domain": "이 주소는 ${domain} 의 주소입니다 Unstoppable Domains", - "address_from_yat": "이 주소는 ${emoji} 의 주소입니다 Yat", - "address_label": "Address label", - "address_remove_contact": "연락처 삭제", - "address_remove_content": "선택한 연락처를 삭제 하시겠습니까?", - "addresses": "구애", + "address_detected": "주소가 감지되었습니다", + "address_from_domain": "이 주소는 Unstoppable Domains의 ${domain}에서 온 것입니다", + "address_from_yat": "이 주소는 Yat의 ${emoji}에서 온 것입니다", + "address_label": "주소 라벨", + "address_remove_contact": "연락처 제거", + "address_remove_content": "선택한 연락처를 정말로 제거하시겠습니까?", + "addresses": "주소", "advanced_settings": "고급 설정", - "aggressive": "지나치게 열심인", - "agree": "동의하다", - "agree_and_continue": "동의 및 계속", - "agree_to": "계정을 생성하면 ", - "alert_notice": "알아채다", - "all": "모든", - "all_coins": "모든 동전", - "all_trades": "A모든 거래", - "all_transactions": "모든 거래 창구", + "aggressive": "적극적", + "agree": "동의", + "agree_and_continue": "동의하고 계속하기", + "agree_to": "계정을 생성함으로써 다음에 동의합니다 ", + "alert_notice": "알림", + "all": "전체", + "all_coins": "모든 코인", + "all_trades": "모든 거래", + "all_transactions": "모든 트랜잭션", "alphabetical": "알파벳순", - "already_have_account": "이미 계정이 있습니까?", - "always": "언제나", - "amount": "양: ", - "amount_is_below_minimum_limit": "수수료 후 잔액은 Exchange (${min})에 필요한 최소 금액보다 적습니다.", - "amount_is_estimate": "수신 금액은 견적입니다", - "amount_is_guaranteed": "수령 금액이 보장됩니다.", - "and": "그리고", - "anonpay_description": "${type} 생성. 수신자는 지원되는 모든 암호화폐로 ${method}할 수 있으며 이 지갑에서 자금을 받게 됩니다.", + "already_have_account": "이미 계정이 있으신가요?", + "always": "항상", + "amount": "금액: ", + "amount_is_below_minimum_limit": "수수료 차감 후 잔액이 교환에 필요한 최소 금액(${min})보다 적습니다", + "amount_is_estimate": "수령 금액은 예상치입니다", + "amount_is_guaranteed": "수령 금액은 보장됩니다", + "and": "및", + "anonpay_description": "${type} 생성. 수신자는 지원되는 모든 암호화폐로 ${method}할 수 있으며, 귀하는 이 지갑으로 자금을 받게 됩니다.", "apk_update": "APK 업데이트", - "approve": "승인하다", - "approve_request": "요청을 승인하십시오", + "approve": "승인", + "approve_request": "요청 승인", "arrive_in_this_address": "${currency} ${tag}이(가) 이 주소로 도착합니다", "ascending": "오름차순", - "ask_each_time": "매번 물어보십시오", - "auth_store_ban_timeout": "타임 아웃 금지", - "auth_store_banned_for": "금지", - "auth_store_banned_minutes": " 의사록", + "ask_each_time": "매번 묻기", + "auth_store_ban_timeout": "차단_시간 초과", + "auth_store_banned_for": "다음 시간 동안 차단됨 ", + "auth_store_banned_minutes": " 분", "auth_store_incorrect_password": "잘못된 PIN", - "authenticated": "인증", - "authentication": "입증", - "auto_generate_addresses": "자동 생성 주소", - "auto_generate_subaddresses": "자동 생성 서브 아드 드레스", - "automatic": "자동적 인", - "available_balance": "사용 가능한 잔액", - "available_balance_description": "이 지갑에서 사용할 수 있는 잔액입니다. 이 잔액은 블록체인에서 가져온 것이며, Cake Wallet이 사용할 수 없습니다.", - "avg_savings": "평균 절감액", - "awaitDAppProcessing": "dApp이 처리를 마칠 때까지 기다려주세요.", + "authenticated": "인증됨", + "authentication": "인증", + "auto_generate_addresses": "주소 자동 생성", + "auto_generate_subaddresses": "하위 주소 자동 생성", + "automatic": "자동", + "available_balance": "사용 가능 잔액", + "available_balance_description": "“사용 가능 잔액” 또는 “확정 잔액”은 즉시 사용할 수 있는 자금입니다. 자금이 아래 잔액에는 표시되지만 위 잔액에는 표시되지 않는 경우, 들어오는 자금이 더 많은 네트워크 확인을 받을 때까지 몇 분 정도 기다려야 합니다. 더 많은 확인을 받은 후 사용할 수 있게 됩니다.", + "avg_savings": "평균 절약액", + "awaitDAppProcessing": "dApp이 처리를 완료할 때까지 기다려 주십시오.", "awaiting_payment_confirmation": "결제 확인 대기 중", - "background_sync": "배경 동기화", + "background_sync": "백그라운드 동기화", "background_sync_mode": "백그라운드 동기화 모드", - "background_sync_on_battery_low": "낮은 배터리에서 동기화하십시오", - "background_sync_on_charging": "충전 할 때만 동기화하십시오", - "background_sync_on_device_idle": "장치를 사용하지 않을 때만 동기화하십시오", - "background_sync_on_unmetered_network": "충족되지 않은 네트워크가 필요합니다", - "backup": "지원", + "background_sync_on_battery_low": "배터리 부족 시 동기화", + "background_sync_on_charging": "충전 중일 때만 동기화", + "background_sync_on_device_idle": "기기를 사용하지 않을 때만 동기화", + "background_sync_on_unmetered_network": "무제한 네트워크 필요", + "backup": "백업", "backup_file": "백업 파일", "backup_password": "백업 비밀번호", - "balance": "균형", + "balance": "잔액", "balance_page": "잔액 페이지", "bill_amount": "청구 금액", - "billing_address_info": "청구서 수신 주소를 묻는 메시지가 표시되면 배송 주소를 입력하세요.", - "biometric_auth_reason": "지문을 스캔하여 인증", + "billing_address_info": "청구지 주소를 묻는 경우 배송 주소를 제공하세요", + "biometric_auth_reason": "인증하려면 지문을 스캔하세요", "bitcoin_dark_theme": "비트코인 다크 테마", "bitcoin_light_theme": "비트코인 라이트 테마", - "bitcoin_payments_require_1_confirmation": "비트코인 결제는 1번의 확인이 필요하며 20분 이상이 소요될 수 있습니다. 기다려 주셔서 감사합니다! 결제가 확인되면 이메일이 전송됩니다.", + "bitcoin_payments_require_1_confirmation": "비트코인 결제는 1번의 확인이 필요하며, 이는 20분 이상 소요될 수 있습니다. 기다려 주셔서 감사합니다! 결제가 확인되면 이메일로 알려드립니다.", "block_height": "블록 높이", - "block_remaining": "남은 블록 1 개", - "Blocks_remaining": "${status} 남은 블록", + "block_remaining": "1 블록 남음", + "Blocks_remaining": "${status} 블록 남음", "bluetooth": "블루투스", - "bright_theme": "선명한", - "bump_fee": "범프 요금", - "buy": "구입", - "buy_alert_content": "현재 Bitcoin, Ethereum, Litecoin 및 Monero 구매만 지원합니다. Bitcoin, Ethereum, Litecoin 또는 Monero 지갑을 생성하거나 전환하십시오.", - "buy_bitcoin": "비트 코인 구매", - "buy_now": "지금 구매하십시오", - "buy_provider_unavailable": "제공자는 현재 사용할 수 없습니다.", - "buy_sell_pair_is_not_supported_warning": "이 통화 쌍은 선택한 결제 방법을 제공하는 모든 공급자가 지원하지 않습니다. 다른 쌍을 선택하거나 결제 방법을 변경하십시오.", - "buy_with": "구매", - "by_cake_pay": "Cake Pay로", - "cake_2fa_preset": "케이크 2FA 프리셋", - "cake_dark_theme": "케이크 다크 테마", - "cake_pay_account_note": "이메일 주소로 가입하면 카드를 보고 구매할 수 있습니다. 일부는 할인된 가격으로 사용 가능합니다!", + "bright_theme": "밝은 테마", + "bump_fee": "수수료 인상", + "buy": "구매", + "buy_alert_content": "현재 비트코인, 이더리움, 라이트코인, 모네로만 구매를 지원합니다. 비트코인, 이더리움, 라이트코인 또는 모네로 지갑을 생성하거나 전환하세요.", + "buy_bitcoin": "비트코인 구매", + "buy_now": "지금 구매", + "buy_provider_unavailable": "제공 업체를 현재 사용할 수 없습니다.", + "buy_sell_pair_is_not_supported_warning": "선택한 결제 방법으로는 이 통화 쌍이 어떤 제공 업체에서도 지원되지 않습니다. 다른 쌍을 선택하거나 결제 방법을 변경해 보세요.", + "buy_with": "다음으로 구매", + "by_cake_pay": "Cake Pay 제공", + "cake_2fa_preset": "Cake 2FA 사전 설정", + "cake_dark_theme": "Cake 다크 테마", + "cake_pay_account_note": "이메일 주소만으로 가입하여 카드를 확인하고 구매하세요. 일부는 할인된 가격으로도 이용 가능합니다!", "cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.", - "cake_pay_save_order": "카드는 영업일 1 일 이내에 이메일로 전자 메일로 보내야합니다. \n 주문 ID 저장 :", - "cake_pay_subtitle": "전세계 선불 카드와 기프트 카드를 구입하십시오", + "cake_pay_save_order": "카드는 영업일 기준 1일 이내에 이메일로 발송됩니다.\n주문 ID를 저장하세요:", + "cake_pay_subtitle": "전 세계 선불 카드 및 기프트 카드 구매", "cake_pay_web_cards_subtitle": "전 세계 선불 카드 및 기프트 카드 구매", - "cake_pay_web_cards_title": "케이크페이 웹카드", - "cake_seeds_save_disclaimer": "이 단어를 안전한 장소에 저장하십시오! 새 장치에서 지갑을 복원하려면이 단어가 필요합니다.", + "cake_pay_web_cards_title": "Cake Pay 웹 카드", + "cake_seeds_save_disclaimer": "이 단어들을 안전한 곳에 저장하세요! 새 기기에서 지갑을 복구하려면 이 단어들이 필요합니다.", "cake_wallet": "Cake Wallet", - "cakepay_confirm_no_vpn": "프록시 나 VPN을 사용하지 않는지 확인합니다.", + "cakepay_confirm_no_vpn": "프록시 또는 VPN을 사용하고 있지 않음을 확인합니다", "cakepay_confirm_purchase": "구매 확인", - "cakepay_confirm_terms_agreed": "여기에 제시된 이용 약관에 동의합니다.", - "cakepay_confirm_voided_refund": "제한된 국가의 상환 시도는 환불을 무효화한다는 것을 이해합니다.", - "cakepay_ios_not_available": "죄송합니다.이 기프트 카드는 iOS에서 사용할 수 없습니다. Android 또는 웹 사이트를 통해 구매할 수 있습니다.", + "cakepay_confirm_terms_agreed": "여기에 제시된 이용 약관에 동의합니다:", + "cakepay_confirm_voided_refund": "제한된 국가에서의 사용 시도는 환불이 무효화됨을 이해합니다", + "cakepay_ios_not_available": "죄송합니다. 이 기프트 카드는 iOS에서 사용할 수 없습니다. 대신 Android나 저희 웹사이트를 통해 구매할 수 있습니다.", "cakepay_prepaid_card": "CakePay 선불 직불 카드", - "camera_consent": "귀하의 카메라는 ${provider}의 식별 목적으로 이미지를 캡처하는 데 사용됩니다. 자세한 내용은 해당 개인정보 보호정책을 확인하세요.", + "camera_consent": "${provider}에서 신원 확인 목적으로 이미지를 캡처하기 위해 카메라를 사용합니다. 자세한 내용은 해당 업체의 개인정보 보호정책을 확인하세요.", "camera_permission_is_required": "카메라 권한이 필요합니다.\n앱 설정에서 활성화해 주세요.", "cancel": "취소", - "cannot_verify": "확인할 수 없습니다", - "cannot_verify_description": "이 도메인은 확인할 수 없습니다. 승인하기 전에 요청을주의 깊게 확인하십시오.", + "cannot_verify": "확인할 수 없음", + "cannot_verify_description": "이 도메인은 확인할 수 없습니다. 승인하기 전에 요청을 주의 깊게 확인하세요.", "card_address": "주소:", - "cardholder_agreement": "카드 소유자 계약", + "cardholder_agreement": "카드 소지자 계약", "cards": "카드", "chain_id": "체인 ID", - "chains": "쇠사슬", - "change": "변화", - "change_backup_password_alert": "이전 백업 파일은 새 백업 암호로 가져올 수 없습니다. 새 백업 암호는 새 백업 파일에만 사용됩니다. 백업 비밀번호를 변경 하시겠습니까?", + "chains": "체인", + "change": "변경", + "change_backup_password_alert": "이전 백업 파일은 새 백업 비밀번호로 가져올 수 없습니다. 새 백업 비밀번호는 새 백업 파일에만 사용됩니다. 백업 비밀번호를 변경하시겠습니까?", "change_currency": "통화 변경", - "change_current_node": "현재 노드를 다음으로 변경 하시겠습니까 ${node}?", + "change_current_node": "현재 노드를 ${node}(으)로 변경하시겠습니까?", "change_current_node_title": "현재 노드 변경", - "change_exchange_provider": "스왑 제공 업체를 변경하십시오", + "change_exchange_provider": "스왑 제공 업체 변경", "change_language": "언어 변경", - "change_language_to": "언어를로 변경 ${language}?", + "change_language_to": "언어를 ${language}(으)로 변경하시겠습니까?", "change_password": "비밀번호 변경", - "change_rep": "대표를 변경하십시오", - "change_rep_message": "대표를 바꾸고 싶습니까?", - "change_rep_successful": "대리인이 성공적으로 변경되었습니다", - "change_selected_exchanges": "선택된 거래소 변경", - "change_selected_pair": "선택한 쌍을 변경하십시오", - "change_wallet_alert_content": "현재 지갑을 다음으로 변경 하시겠습니까 ${wallet_name}?", + "change_rep": "대표자 변경", + "change_rep_message": "정말로 대표자를 변경하시겠습니까?", + "change_rep_successful": "대표자가 성공적으로 변경되었습니다", + "change_selected_exchanges": "선택한 교환소 변경", + "change_selected_pair": "선택한 쌍 변경", + "change_wallet_alert_content": "현재 지갑을 ${wallet_name}(으)로 변경하시겠습니까?", "change_wallet_alert_title": "현재 지갑 변경", - "choose_a_payment_method": "결제 방법을 선택하십시오", - "choose_a_provider": "제공자를 선택하십시오", - "choose_account": "계정을 선택하십시오", - "choose_address": "\n\n주소를 선택하십시오:", - "choose_card_value": "카드 값을 선택하십시오", - "choose_derivation": "지갑 파생을 선택하십시오", - "choose_from_available_options": "사용 가능한 옵션에서 선택:", + "choose_a_payment_method": "결제 방법 선택", + "choose_a_provider": "제공 업체 선택", + "choose_account": "계정 선택", + "choose_address": "\n\n주소를 선택하세요:", + "choose_card_value": "카드 금액 선택", + "choose_derivation": "지갑 파생 경로 선택", + "choose_from_available_options": "사용 가능한 옵션 중에서 선택하세요:", "choose_one": "하나 선택", - "choose_relay": "사용할 릴레이를 선택해주세요", - "choose_wallet_currency": "지갑 통화를 선택하십시오:", - "choose_wallet_group": "지갑 그룹을 선택하십시오", - "clear": "명확한", + "choose_relay": "사용할 릴레이를 선택하세요", + "choose_wallet_currency": "지갑 통화를 선택하세요:", + "choose_wallet_group": "지갑 그룹 선택", + "clear": "지우기", "clearnet_link": "클리어넷 링크", - "close": "닫다", - "coin_control": "코인 제어 (옵션)", - "cold_or_recover_wallet": "Cupcake 또는 차가운 지갑에서 읽기 전용 지갑을 추가하거나 종이 지갑을 복구하십시오.", - "collection_address": "수집 주소", + "close": "닫기", + "coin_control": "코인 제어 (선택 사항)", + "cold_or_recover_wallet": "Cupcake의 읽기 전용 지갑 또는 콜드 월렛을 추가하거나 페이퍼 월렛을 복구합니다", + "collection_address": "컬렉션 주소", "collection_description": "컬렉션 설명", - "collection_name": "수집 이름", + "collection_name": "컬렉션 이름", "color_theme": "색상 테마", - "commit_transaction_amount_fee": "커밋 거래\n양: ${amount}\n보수: ${fee}", + "commit_transaction_amount_fee": "트랜잭션 커밋\n금액: ${amount}\n수수료: ${fee}", "confirm": "확인", - "confirm_delete_template": "이 작업은이 템플릿을 삭제합니다. 계속 하시겠습니까?", - "confirm_delete_wallet": "이 작업은이 지갑을 삭제합니다. 계속 하시겠습니까?", - "confirm_fee_deduction": "수수료 공제를 확인하십시오", - "confirm_fee_deduction_content": "출력에서 수수료를 공제하는 데 동의하십니까?", - "confirm_passphrase": "암호를 확인하십시오", + "confirm_delete_template": "이 작업은 이 템플릿을 삭제합니다. 계속하시겠습니까?", + "confirm_delete_wallet": "이 작업은 이 지갑을 삭제합니다. 계속하시겠습니까?", + "confirm_fee_deduction": "수수료 차감 확인", + "confirm_fee_deduction_content": "출금액에서 수수료를 차감하는 데 동의하십니까?", + "confirm_passphrase": "암호 구문 확인", "confirm_sending": "전송 확인", - "confirm_silent_payments_switch_node": "현재 노드는 무음 지불을 지원하지 않습니다 \\ ncake 지갑은 스캔을 위해 호환 가능한 노드로 전환됩니다.", - "confirm_transaction": "거래 확인", + "confirm_silent_payments_switch_node": "현재 노드는 사일런트 페이먼트를 지원하지 않습니다.\\n\\nCake Wallet은 스캔하는 동안 호환되는 노드로 전환됩니다.", + "confirm_transaction": "트랜잭션 확인", "confirmations": "확인", - "confirmed": "확인된 잔액", - "confirmed_tx": "확인", + "confirmed": "확정 잔액", + "confirmed_tx": "확정됨", "congratulations": "축하합니다!", "connect_an_existing_yat": "기존 Yat 연결", - "connect_yats": "야츠 연결", - "connect_your_hardware_wallet": "Bluetooth 또는 USB를 사용하여 하드웨어 지갑을 연결하십시오", - "connect_your_hardware_wallet_ios": "Bluetooth를 사용하여 하드웨어 지갑을 연결하십시오", - "connected": "연결", + "connect_yats": "Yat 연결", + "connect_your_hardware_wallet": "블루투스 또는 USB를 사용하여 하드웨어 지갑 연결", + "connect_your_hardware_wallet_ios": "블루투스를 사용하여 하드웨어 지갑 연결", + "connected": "연결됨", "connection_sync": "연결 및 동기화", - "connectWalletPrompt": "거래를 하려면 WalletConnect에 지갑을 연결하세요.", - "contact": "접촉", - "contact_list_contacts": "콘택트 렌즈", + "connectWalletPrompt": "거래를 하려면 WalletConnect로 지갑을 연결하세요", + "contact": "연락처", + "contact_list_contacts": "연락처", "contact_list_wallets": "내 지갑", - "contact_name": "담당자 이름", - "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요.", - "contact_support": "지원팀에 문의", - "continue_text": "잇다", - "contract_warning": "이 계약 주소는 잠재적으로 사기성으로 표시되었습니다. 주의해서 계속 진행하십시오.", - "contractName": "계약명", - "contractSymbol": "계약 기호", - "copied_key_to_clipboard": "복사 ${key} 클립 보드로", - "copied_to_clipboard": "클립 보드에 복사", - "copy": "부", + "contact_name": "연락처 이름", + "contact_name_exists": "해당 이름의 연락처가 이미 존재합니다. 다른 이름을 선택하십시오.", + "contact_support": "고객 지원 문의", + "continue_text": "계속", + "contract_warning": "이 계약 주소는 잠재적으로 사기일 수 있다는 플래그가 지정되었습니다. 주의하여 진행하십시오.", + "contractName": "계약 이름", + "contractSymbol": "계약 심볼", + "copied_key_to_clipboard": "${key} 클립보드에 복사됨", + "copied_to_clipboard": "클립보드에 복사됨", + "copy": "복사", "copy_address": "주소 복사", - "copy_id": "부 ID", - "copyWalletConnectLink": "dApp에서 WalletConnect 링크를 복사하여 여기에 붙여넣으세요.", - "corrupted_seed_notice": "이 지갑의 파일은 손상되어 열 수 없습니다. 씨앗 문구를보고 저장하고 지갑을 복원하십시오.\n\n값이 비어 있으면 씨앗을 올바르게 회수 할 수 없었습니다.", + "copy_id": "ID 복사", + "copyWalletConnectLink": "dApp에서 WalletConnect 링크를 복사하여 여기에 붙여넣으세요", + "corrupted_seed_notice": "이 지갑의 파일이 손상되어 열 수 없습니다. 시드 구문을 보고 저장한 다음 지갑을 복구하세요.\n\n값이 비어 있으면 시드를 올바르게 복구할 수 없었습니다.", "countries": "국가", - "create_account": "계정 만들기", + "create_account": "계정 생성", "create_backup": "백업 생성", - "create_donation_link": "기부 링크 만들기", + "create_donation_link": "기부 링크 생성", "create_invoice": "인보이스 생성", - "create_new": "새 월렛 만들기", - "create_new_account": "새 계정을 만들", - "create_new_seed": "새 씨앗을 만듭니다", - "creating_new_wallet": "새 지갑 생성", + "create_new": "새 지갑 생성", + "create_new_account": "새 계정 생성", + "create_new_seed": "새 시드 생성", + "creating_new_wallet": "새 지갑 생성 중", "creating_new_wallet_error": "오류: ${description}", - "creation_date": "생산 일", - "custom": "커스텀", - "custom_drag": "사용자 정의 (홀드 앤 드래그)", - "custom_redeem_amount": "사용자 지정 상환 금액", - "custom_value": "맞춤 가치", - "dark_theme": "어두운", + "creation_date": "생성 날짜", + "custom": "사용자 지정", + "custom_drag": "사용자 지정 (길게 누르고 드래그)", + "custom_redeem_amount": "사용자 지정 사용 금액", + "custom_value": "사용자 지정 값", + "dark_theme": "다크 테마", "debit_card": "직불 카드", - "debit_card_terms": "이 디지털 지갑에 있는 귀하의 지불 카드 번호(및 귀하의 지불 카드 번호에 해당하는 자격 증명)의 저장 및 사용은 부터 발효되는 지불 카드 발행자와의 해당 카드 소지자 계약의 이용 약관을 따릅니다. 수시로.", - "decimal_places_error": "소수점 이하 자릿수가 너무 많습니다.", - "decimals_cannot_be_zero": "토큰 소수점은 0이 될 수 없습니다.", - "decred_info_card_details": "Decred는 정상적인 비트 코인 지갑보다 더 오래 걸리는 \"SPV\"로 알려진 분산 및 개인 정보 보호 동기화 방법을 사용합니다. 자세한 내용은 아래를 누릅니다.", - "decred_info_title": "Decred의 동기화", - "default_buy_provider": "기본 구매 제공자", - "default_sell_provider": "기본 판매 공급자", - "delete": "지우다", + "debit_card_terms": "이 디지털 지갑에 결제 카드 번호(및 결제 카드 번호에 해당하는 자격 증명)를 저장하고 사용하는 것은 해당 카드 발급사와의 카드 소지자 계약 이용 약관(수시로 효력 발생)의 적용을 받습니다.", + "decimal_places_error": "소수 자릿수가 너무 많습니다", + "decimals_cannot_be_zero": "토큰 소수 자릿수는 0이 될 수 없습니다.", + "decred_info_card_details": "Decred는 “SPV”로 알려진 분산형 및 개인 정보 보호 동기화 방법을 사용하며, 이는 일반 비트코인 지갑보다 오래 걸립니다. 자세히 알아보려면 아래를 탭하세요.", + "decred_info_title": "Decred 동기화", + "default_buy_provider": "기본 구매 제공 업체", + "default_sell_provider": "기본 판매 제공 업체", + "delete": "삭제", "delete_account": "계정 삭제", "delete_wallet": "지갑 삭제", - "delete_wallet_confirm_message": "${wallet_name} 지갑을 삭제하시겠습니까?", - "deleteConnectionConfirmationPrompt": "다음 연결을 삭제하시겠습니까?", - "denominations": "교파", + "delete_wallet_confirm_message": "${wallet_name} 지갑을 정말로 삭제하시겠습니까?", + "deleteConnectionConfirmationPrompt": "연결을 삭제하시겠습니까?", + "denominations": "액면가", "derivationpath": "파생 경로", "descending": "내림차순", "description": "설명", "destination_tag": "목적지 태그:", - "dfx_option_description": "EUR & CHF로 암호화를 구입하십시오. 유럽의 소매 및 기업 고객을 위해", - "didnt_get_code": "코드를 받지 못하셨습니까?", - "digit_pin": "숫자 PIN", - "digital_and_physical_card": " 디지털 및 실제 선불 직불 카드", - "disable": "장애를 입히다", - "disable_bulletin": "서비스 상태 게시판을 비활성화합니다", - "disable_buy": "구매 행동 비활성화", - "disable_cake_2fa": "케이크 2FA 비활성화", + "dfx_option_description": "EUR 및 CHF로 암호화폐 구매. 유럽의 개인 및 기업 고객 대상", + "didnt_get_code": "코드를 받지 못했나요?", + "digit_pin": "자리 PIN", + "digital_and_physical_card": " 디지털 및 실물 선불 직불 카드", + "disable": "비활성화", + "disable_bulletin": "서비스 상태 게시판 비활성화", + "disable_buy": "구매 기능 비활성화", + "disable_cake_2fa": "Cake 2FA 비활성화", "disable_exchange": "교환 비활성화", - "disable_exchange_option": "교환 옵션을 비활성화합니다", - "disable_fee_api_warning": "이것을 끄면 경우에 따라 수수료가 부정확 할 수 있으므로 거래 수수료를 초과 지불하거나 지불 할 수 있습니다.", + "disable_exchange_option": "교환 옵션 비활성화", + "disable_fee_api_warning": "이 기능을 끄면 경우에 따라 수수료율이 정확하지 않을 수 있으며, 트랜잭션 수수료를 과다 또는 과소 지불할 수 있습니다.", "disable_fiat": "법정화폐 비활성화", - "disable_sell": "판매 조치 비활성화", + "disable_sell": "판매 기능 비활성화", "disable_trade_option": "거래 옵션 비활성화", - "disableBatteryOptimization": "배터리 최적화를 비활성화합니다", - "disableBatteryOptimizationDescription": "백그라운드 동기화를보다 자유롭고 매끄럽게 실행하기 위해 배터리 최적화를 비활성화하고 싶습니까?", - "disabled": "장애가 있는", - "disconnect_session": "분리 세션", + "disableBatteryOptimization": "배터리 최적화 비활성화", + "disableBatteryOptimizationDescription": "백그라운드 동기화가 더 자유롭고 원활하게 실행되도록 배터리 최적화를 비활성화하시겠습니까?", + "disabled": "비활성화됨", + "disconnect_session": "세션 연결 해제", "discount": "${value}% 절약", - "display_settings": "디스플레이 설정", + "display_settings": "표시 설정", "displayable": "표시 가능", - "do_not_have_enough_gas_asset": "현재 블록체인 네트워크 조건으로 거래를 하기에는 ${currency}이(가) 충분하지 않습니다. 다른 자산을 보내더라도 블록체인 네트워크 수수료를 지불하려면 ${currency}가 더 필요합니다.", - "do_not_send": "보내지 마세요", - "do_not_share_warning_text": "지원을 포함하여 다른 사람과 이러한 정보를 공유하지 마십시오.\n\n귀하의 자금은 도난당할 수 있고 도난당할 수 있습니다!", - "do_not_show_me": "나를 다시 표시하지 않음", + "do_not_have_enough_gas_asset": "현재 블록체인 네트워크 상태에서 트랜잭션을 생성하기에 ${currency}이(가) 충분하지 않습니다. 다른 자산을 보내는 경우에도 블록체인 네트워크 수수료를 지불하기 위해 더 많은 ${currency}이(가) 필요합니다.", + "do_not_send": "보내지 않음", + "do_not_share_warning_text": "지원을 포함하여 다른 누구와도 공유하지 마십시오.\n\n자금이 도난당할 수 있습니다!", + "do_not_show_me": "다시 표시하지 않음", "domain_looks_up": "도메인 조회", "domain_mismatch": "도메인 불일치", - "domain_mismatch_description": "이 웹 사이트에는이 요청의 발신자와 일치하지 않는 도메인이 있습니다. 승인은 자금 손실로 이어질 수 있습니다.", - "donation_link_details": "기부 링크 세부정보", + "domain_mismatch_description": "이 웹사이트의 도메인이 이 요청의 발신자와 일치하지 않습니다. 승인하면 자금 손실로 이어질 수 있습니다.", + "donation_link_details": "기부 링크 세부 정보", "e_sign_consent": "전자 서명 동의", - "edit": "편집하다", - "edit_backup_password": "편집 백업 암호", + "edit": "편집", + "edit_backup_password": "백업 비밀번호 편집", "edit_node": "노드 편집", "edit_token": "토큰 편집", - "electrum_address_disclaimer": "사용할 때마다 새 주소가 생성되지만 이전 주소는 계속 작동합니다.", + "electrum_address_disclaimer": "사용할 때마다 새 주소를 생성하지만 이전 주소도 계속 작동합니다", "email_address": "이메일 주소", - "enable": "할 수 있게 하다", - "enable_mempool_api": "정확한 수수료 및 날짜에 대한 Mempool API", - "enable_replace_by_fee": "대체별로 활성화하십시오", - "enable_silent_payments_scanning": "팁에 도달 할 때까지 사일런트 지불을 스캔하기 시작합니다.", - "enabled": "사용", + "enable": "활성화", + "enable_mempool_api": "정확한 수수료 및 날짜를 위한 Mempool API", + "enable_replace_by_fee": "RBF(Replace-By-Fee) 활성화", + "enable_silent_payments_scanning": "사일런트 페이먼트 주소로 전송된 트랜잭션 스캔 시작", + "enabled": "활성화됨", "enter_amount": "금액 입력", - "enter_backup_password": "여기에 백업 비밀번호를 입력하세요.", + "enter_backup_password": "여기에 백업 비밀번호 입력", "enter_code": "코드 입력", - "enter_seed_phrase": "시드 문구를 입력하십시오", + "enter_seed_phrase": "시드 구문 입력", "enter_totp_code": "TOTP 코드를 입력하세요.", - "enter_wallet_password": "지갑 암호를 입력하십시오", + "enter_wallet_password": "지갑 비밀번호 입력", "enter_your_note": "메모를 입력하세요…", - "enter_your_pin": "PIN을 입력하십시오", - "enter_your_pin_again": "다시 핀을 입력", - "enterTokenID": "토큰 ID를 입력하세요", - "enterWalletConnectURI": "WalletConnect URI를 입력하세요.", + "enter_your_pin": "PIN 입력", + "enter_your_pin_again": "PIN 다시 입력", + "enterTokenID": "토큰 ID 입력", + "enterWalletConnectURI": "WalletConnect URI 입력", "error": "오류", - "error_dialog_content": "죄송합니다. 오류가 발생했습니다.\n\n오류 보고서를 지원 팀에 보내 응용 프로그램을 개선하십시오.", - "error_text_account_name": "계정 이름은 문자, 숫자 만 포함 할 수 있습니다\n1 ~ 15 자 사이 여야합니다", - "error_text_address": "지갑 주소는 유형과 일치해야합니다\n암호 화폐", - "error_text_amount": "금액은 숫자 만 포함 할 수 있습니다", - "error_text_contact_name": "담당자 이름은 포함 할 수 없습니다 ` , ' \" 기호\n1 자에서 32 자 사이 여야합니다", - "error_text_crypto_currency": "소수 자릿수\n12 이하 여야합니다", - "error_text_fiat": "금액은 사용 가능한 잔액을 초과 할 수 없습니다.\n소수 자릿수는 2보다 작거나 같아야합니다", - "error_text_input_above_maximum_limit": "금액이 최대 값보다 많습니다.", - "error_text_input_below_minimum_limit": "금액이 최소보다 적습니다.", - "error_text_keys": "지갑 키는 16 진수로 64 자만 포함 할 수 있습니다", - "error_text_limits_loading_failed": "거래 ${provider} 가 생성되지 않습니다. 로딩 실패", - "error_text_maximum_limit": "거래 ${provider} 가 생성되지 않습니다. 금액이 최대 값보다 많습니다. ${max} ${currency}", - "error_text_minimal_limit": "거래 ${provider} 가 생성되지 않습니다. 금액이 최소보다 적습니다. ${min} ${currency}", - "error_text_node_address": "iPv4 주소를 입력하십시오", - "error_text_node_port": "노드 포트는 0에서 65535 사이의 숫자 만 포함 할 수 있습니다", - "error_text_node_proxy_address": ":<포트>를 입력하십시오(예: 127.0.0.1:9050).", - "error_text_payment_id": "지불 ID는 16 ~ 64 자의 16 진 문자 만 포함 할 수 있습니다", - "error_text_subaddress_name": "하위 주소 이름은 포함 할 수 없습니다 ` , ' \" 기호 \n1 ~ 20 자 사이 여야합니다", - "error_text_template": "템플릿 이름과 주소는 포함 할 수 없습니다 ` , ' \" 기호 \n1 ~ 106 자 사이 여야합니다", - "error_text_wallet_name": "지갑 이름은 문자, 숫자, _ - 기호만 포함할 수 있습니다.\n1~33자 사이여야 합니다.", - "error_text_xmr": "XMR 값은 사용 가능한 잔액을 초과 할 수 없습니다.\n소수 자릿수는 12 이하 여야합니다", - "error_while_processing": "전환하는 동안 오류가 발생했습니다", - "errorGettingCredentials": "실패: 자격 증명을 가져오는 중 오류가 발생했습니다.", - "errorSigningTransaction": "거래에 서명하는 동안 오류가 발생했습니다.", + "error_dialog_content": "죄송합니다. 오류가 발생했습니다.\n\n애플리케이션 개선을 위해 오류 보고서를 지원팀에 보내주세요.", + "error_text_account_name": "계정 이름은 문자, 숫자만 포함할 수 있으며\n1~15자 사이여야 합니다.", + "error_text_address": "지갑 주소는 암호화폐 유형과\n일치해야 합니다.", + "error_text_amount": "금액은 숫자만 포함할 수 있습니다.", + "error_text_contact_name": "연락처 이름에는 ` , ' \" 기호를 포함할 수 없으며\n1~32자 사이여야 합니다.", + "error_text_crypto_currency": "소수 자릿수는\n12자리 이하여야 합니다.", + "error_text_fiat": "금액 값이 사용 가능 잔액을 초과할 수 없습니다.\n소수 자릿수는 2자리 이하여야 합니다.", + "error_text_input_above_maximum_limit": "금액이 최대 한도를 초과합니다.", + "error_text_input_below_minimum_limit": "금액이 최소 한도 미만입니다.", + "error_text_keys": "지갑 키는 16진수로 64자만 포함할 수 있습니다.", + "error_text_limits_loading_failed": "${provider} 거래가 생성되지 않았습니다. 한도 로딩 실패", + "error_text_maximum_limit": "${provider} 거래가 생성되지 않았습니다. 금액이 최대값(${max} ${currency})보다 큽니다.", + "error_text_minimal_limit": "${provider} 거래가 생성되지 않았습니다. 금액이 최소값(${min} ${currency})보다 작습니다.", + "error_text_node_address": "IPv4 주소를 입력하세요.", + "error_text_node_port": "노드 포트는 0에서 65535 사이의 숫자만 포함할 수 있습니다.", + "error_text_node_proxy_address": ":<포트> 형식으로 입력하세요. 예: 127.0.0.1:9050", + "error_text_payment_id": "결제 ID는 16진수로 16~64자만 포함할 수 있습니다.", + "error_text_subaddress_name": "하위 주소 이름에는 ` , ' \" 기호를 포함할 수 없으며\n1~20자 사이여야 합니다.", + "error_text_template": "템플릿 이름과 주소에는 ` , ' \" 기호를 포함할 수 없으며\n1~106자 사이여야 합니다.", + "error_text_wallet_name": "지갑 이름은 문자, 숫자, _ - 기호만 포함할 수 있으며\n1~33자 사이여야 합니다.", + "error_text_xmr": "XMR 값이 사용 가능 잔액을 초과할 수 없습니다.\n소수 자릿수는 12자리 이하여야 합니다.", + "error_while_processing": "처리 중 오류가 발생했습니다", + "errorGettingCredentials": "실패: 자격 증명을 가져오는 중 오류 발생", + "errorSigningTransaction": "트랜잭션 서명 중 오류가 발생했습니다", "estimated": "예상", - "estimated_new_fee": "예상 새로운 수수료", - "estimated_receive_amount": "예상 수신 금액", - "etherscan_history": "이더스캔 역사", + "estimated_new_fee": "예상 새 수수료", + "estimated_receive_amount": "예상 수령 금액", + "etherscan_history": "Etherscan 내역", "event": "이벤트", "events": "이벤트", "exchange": "교환", - "exchange_incorrect_current_wallet_for_xmr": "케이크 지갑 Monero Balance에서 XMR을 교체하려면 먼저 Monero 지갑으로 전환하십시오.", + "exchange_incorrect_current_wallet_for_xmr": "Cake Wallet 모네로 잔액에서 XMR을 스왑하려면 먼저 모네로 지갑으로 전환하세요.", "exchange_new_template": "새 템플릿", - "exchange_provider_unsupported": "${providerName}은 더 이상 지원되지 않습니다!", - "exchange_result_confirm": "확인을 누르면 전송됩니다 ${fetchingLabel} ${from} 지갑에서 ${walletName} 아래 주소로. 또는 외부 지갑에서 아래 주소로 보낼 수 있습니다 / QR 코드로 보낼 수 있습니다.\n\n확인을 눌러 계속하거나 금액을 변경하려면 돌아가십시오.", - "exchange_result_description": "다음 페이지에 표시된 주소로 최소 ${fetchingLabel} ${from} 를 보내야합니다. ${fetchingLabel} ${from} 보다 적은 금액을 보내면 변환되지 않고 환불되지 않을 수 있습니다.", - "exchange_result_write_down_ID": "*위에 표시된 ID를 복사하거나 적어 두십시오.", - "exchange_result_write_down_trade_id": "계속하려면 거래 ID를 복사하거나 적어 두십시오..", - "exchange_sync_alert_content": "지갑이 동기화 될 때까지 기다리십시오", - "exchange_trade_result_confirm": "Cake 지갑에서 보내기를 누르면 ${walletName}이라는 지갑에서 아래에 표시된 주소로 ${fetchingLabel} ${from}을 보내는 것입니다. 또는 외부 지갑에서 외부 지갑 세부 정보 페이지에서 주소 / QR 코드로 보낼 수 있습니다.", - "expired": "만료", + "exchange_provider_unsupported": "${providerName}은(는) 더 이상 지원되지 않습니다!", + "exchange_result_confirm": "확인을 누르면 ${walletName} 지갑에서 아래 표시된 주소로 ${fetchingLabel} ${from}을(를) 보냅니다. 또는 외부 지갑에서 아래 주소/QR 코드로 보낼 수 있습니다.\n\n계속하려면 확인을 누르거나 뒤로 가서 금액을 변경하세요.", + "exchange_result_description": "다음 페이지에 표시된 주소로 최소 ${fetchingLabel} ${from}을(를) 보내야 합니다. ${fetchingLabel} ${from}보다 적은 금액을 보내면 전환되지 않거나 환불되지 않을 수 있습니다.", + "exchange_result_write_down_ID": "*위에 표시된 ID를 복사하거나 적어두세요.", + "exchange_result_write_down_trade_id": "계속하려면 거래 ID를 복사하거나 적어두세요.", + "exchange_sync_alert_content": "지갑이 동기화될 때까지 기다려 주세요.", + "exchange_trade_result_confirm": "Cake Wallet에서 보내기를 누르면 ${walletName} 지갑에서 아래 표시된 주소로 ${fetchingLabel} ${from}을(를) 보냅니다. 또는 외부 지갑에서 보내기 세부 정보 페이지의 주소/QR 코드로 보낼 수 있습니다.\n\n계속하려면 버튼 중 하나를 누르거나 뒤로 가서 금액을 변경하세요.", + "expired": "만료됨", "expires": "만료", - "expiresOn": "만료 날짜", - "expiry_and_validity": "만료와 타당성", + "expiresOn": "만료일", + "expiry_and_validity": "만료 및 유효성", "export_backup": "백업 내보내기", - "export_logs": "내보내기 로그", - "export_outputs": "내보내기 출력", - "extend_session": "세션을 확장하십시오", + "export_logs": "로그 내보내기", + "export_outputs": "출력 내보내기", + "extend_session": "세션 연장", "extra_id": "추가 ID:", - "extracted_address_content": "당신은에 자금을 보낼 것입니다\n${recipient_name}", + "extracted_address_content": "다음 주소로 자금을 보냅니다\n${recipient_name}", "failed_authentication": "인증 실패. ${state_error}", "faq": "FAQ", - "features": "특징", - "fee_less_than_min": "선택된 수수료는 최소값보다 적습니다. 거래를 보낼 수있는 수수료를 늘리십시오.", - "fee_rate": "수수료", - "fetching": "가져 오는 중", - "fiat_api": "명목 화폐 API", - "fiat_balance": "피아트 잔액", + "features": "기능", + "fee_less_than_min": "선택한 수수료가 최소값보다 작습니다. 트랜잭션을 보낼 수 있도록 수수료를 높여주세요.", + "fee_rate": "수수료율", + "fetching": "가져오는 중", + "fiat_api": "법정화폐 API", + "fiat_balance": "법정화폐 잔액", "field_required": "이 필드는 필수입니다", - "file_saved": "파일이 저장되었습니다", - "fill_code": "이메일에 제공된 인증 코드를 입력하세요.", - "filter_by": "필터링 기준", - "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin 및 Haven을 위한 멋진 지갑", - "fixed_pair_not_supported": "이 고정 쌍은 선택한 스왑 서비스에서 지원되지 않습니다.", - "fixed_rate": "고정 비율", - "fixed_rate_alert": "고정 금리 모드 체크시 수취 금액 입력이 가능합니다. 고정 속도 모드로 전환 하시겠습니까?", + "file_saved": "파일 저장됨", + "fill_code": "이메일로 제공된 인증 코드를 입력하세요", + "filter_by": "필터 기준", + "first_wallet_text": "모네로, 비트코인, 이더리움, 라이트코인, 헤이븐을 위한 멋진 지갑", + "fixed_pair_not_supported": "이 고정 쌍은 선택한 스왑 서비스에서 지원되지 않습니다", + "fixed_rate": "고정 환율", + "fixed_rate_alert": "고정 환율 모드가 선택되면 수령 금액을 입력할 수 있습니다. 고정 환율 모드로 전환하시겠습니까?", "forgot_password": "비밀번호 찾기", - "freeze": "얼다", + "freeze": "동결", "frequently_asked_questions": "자주 묻는 질문", - "from": "에서", - "frozen": "겨울 왕국", - "frozen_balance": "냉동 균형", - "full_balance": "풀 밸런스", - "gas_exceeds_allowance": "거래에 필요한 가스는 수당을 초과합니다.", + "from": "보내는 통화", + "frozen": "동결됨", + "frozen_balance": "동결 잔액", + "full_balance": "전체 잔액", + "gas_exceeds_allowance": "트랜잭션에 필요한 가스가 허용 한도를 초과합니다.", "gas_price": "가스 가격", "generate_name": "이름 생성", "generating_gift_card": "기프트 카드 생성 중", - "generating_transaction": "거래 생성", - "get_a": "가져오기", - "get_card_note": " 디지털 통화로 충전할 수 있습니다. 추가 정보가 필요하지 않습니다!", - "get_your_yat": "당신의 Yat를 얻으십시오", + "generating_transaction": "트랜잭션 생성 중", + "get_a": "받기 ", + "get_card_note": " 디지털 통화로 재충전할 수 있습니다. 추가 정보는 필요하지 않습니다!", + "get_your_yat": "Yat 받기", "gift_card_amount": "기프트 카드 금액", - "gift_card_balance_note": "잔액이 남아 있는 기프트 카드가 여기에 표시됩니다.", + "gift_card_balance_note": "잔액이 남은 기프트 카드가 여기에 표시됩니다", "gift_card_is_generated": "기프트 카드가 생성되었습니다", "gift_card_number": "기프트 카드 번호", - "gift_card_redeemed_note": "사용한 기프트 카드가 여기에 표시됩니다.", + "gift_card_redeemed_note": "사용한 기프트 카드가 여기에 표시됩니다", "gift_cards": "기프트 카드", - "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.", - "got_it": "알았다", + "gift_cards_unavailable": "기프트 카드는 현재 모네로, 비트코인, 라이트코인으로만 구매할 수 있습니다", + "got_it": "확인", "gross_balance": "총 잔액", - "group_by_type": "유형별 그룹", + "group_by_type": "유형별 그룹화", "haven_app": "Haven by Cake Wallet", - "haven_app_wallet_text": "Awesome wallet for Haven", - "help": "돕다", + "haven_app_wallet_text": "Haven을 위한 멋진 지갑", + "help": "도움말", "hidden_addresses": "숨겨진 주소", - "hidden_balance": "숨겨진 균형", - "hide": "숨다", + "hidden_balance": "숨겨진 잔액", + "hide": "숨기기", "hide_details": "세부 정보 숨기기", "high_contrast_theme": "고대비 테마", "home_screen_settings": "홈 화면 설정", - "how_to_use": "사용하는 방법", - "how_to_use_card": "이 카드를 사용하는 방법", + "how_to_use": "사용 방법", + "how_to_use_card": "이 카드 사용 방법", "id": "ID: ", - "if_you_dont_see_your_device": "위의 장치가 표시되지 않으면 원장이 깨어 있고 잠금 해제되었는지 확인하십시오!", - "ignor": "무시하다", - "import": "수입", + "if_you_dont_see_your_device": "위에 기기가 보이지 않으면 Ledger가 켜져 있고 잠금 해제되어 있는지 확인하세요!", + "ignor": "무시", + "import": "가져오기", "importNFTs": "NFT 가져오기", "in_store": "매장 내", - "incoming": "들어오는", - "incorrect_seed": "입력하신 텍스트가 유효하지 않습니다.", - "incorrect_seed_option": "잘못된. 다시 시도하십시오", - "incorrect_seed_option_back": "잘못된. 씨앗이 올바르게 저장되어 있는지 확인하고 다시 시도하십시오.", + "incoming": "수신", + "incorrect_seed": "입력한 텍스트가 유효하지 않습니다.", + "incorrect_seed_option": "잘못되었습니다. 다시 시도하세요.", + "incorrect_seed_option_back": "잘못되었습니다. 시드가 올바르게 저장되었는지 확인하고 다시 시도하세요.", "inputs": "입력", - "insufficient_funds_for_tx": "거래를 성공적으로 실행하기위한 자금이 충분하지 않습니다.", - "insufficient_lamport_for_tx": "거래 및 거래 수수료를 충당하기에 충분한 SOL이 없습니다. 지갑에 더 많은 솔을 추가하거나 보내는 솔을 줄입니다.", - "insufficient_lamports": "거래 및 거래 수수료를 충당하기에 충분한 SOL이 없습니다. 최소 ${solValueNeeded} sol이 필요합니다. 지갑에 더 많은 솔을 추가하거나 보내는 솔을 줄이십시오.", - "insufficientFundsForRentError": "거래 수수료와 계좌 임대료를 충당하기에 충분한 SOL이 없습니다. 지갑에 더 많은 솔을 추가하거나 보내는 솔을 줄이십시오.", - "introducing_cake_pay": "소개 Cake Pay!", + "insufficient_funds_for_tx": "트랜잭션을 성공적으로 실행하기에 자금이 부족합니다.", + "insufficient_lamport_for_tx": "트랜잭션 및 트랜잭션 수수료를 충당하기에 SOL이 부족합니다. 지갑에 SOL을 더 추가하거나 보내는 SOL 금액을 줄이세요.", + "insufficient_lamports": "트랜잭션 및 트랜잭션 수수료를 충당하기에 SOL이 부족합니다. 최소 ${solValueNeeded} SOL이 필요합니다. 지갑에 SOL을 더 추가하거나 보내는 SOL 금액을 줄이세요.", + "insufficientFundsForRentError": "계정의 트랜잭션 수수료 및 렌트를 충당하기에 SOL이 부족합니다. 지갑에 SOL을 더 추가하거나 보내는 SOL 금액을 줄이세요.", + "introducing_cake_pay": "Cake Pay를 소개합니다!", "invalid_input": "잘못된 입력", - "invalid_password": "유효하지 않은 비밀번호", - "invoice_details": "인보이스 세부정보", - "is_percentage": "이다", + "invalid_password": "잘못된 비밀번호", + "invoice_details": "인보이스 세부 정보", + "is_percentage": "는", "keys": "키", - "last_30_days": "지난 30일", + "last_30_days": "최근 30일", "learn_more": "더 알아보기", - "ledger_connection_error": "원장에 연결하지 못했습니다. 다시 시도하십시오.", - "ledger_error_device_locked": "원장이 잠겨 있습니다", - "ledger_error_tx_rejected_by_user": "장치에서 거래가 거부되었습니다", - "ledger_error_wrong_app": "원장에서 올바른 앱을 반대하는지 확인하십시오.", - "ledger_please_enable_bluetooth": "Bluetooth가 원장을 감지 할 수 있도록하십시오", - "legacy": "유산", - "light_theme": "빛", - "litecoin_enable_mweb_sync": "mweb 스캔을 활성화합니다", - "litecoin_mweb": "mweb", - "litecoin_mweb_allow_coins": "mweb 코인을 허용하십시오", - "litecoin_mweb_always_scan": "mweb는 항상 스캔을 설정합니다", - "litecoin_mweb_description": "MWEB는 Litecoin에 더 빠르고 저렴하며 개인 거래를 제공하는 새로운 프로토콜입니다.", - "litecoin_mweb_dismiss": "해고하다", - "litecoin_mweb_display_card": "mweb 카드를 보여주십시오", - "litecoin_mweb_enable": "mweb 활성화", - "litecoin_mweb_enable_later": "디스플레이 설정에서 MWEB를 다시 활성화하도록 선택할 수 있습니다.", - "litecoin_mweb_logs": "mweb 로그", - "litecoin_mweb_node": "mweb 노드", - "litecoin_mweb_pegin": "페그를 입력하십시오", - "litecoin_mweb_pegout": "죽다", - "litecoin_mweb_scanning": "mweb 스캔", - "litecoin_mweb_settings": "mweb 설정", - "litecoin_mweb_warning": "MWEB를 사용하면 처음에는 ~ 600MB의 데이터를 다운로드하며 네트워크 속도에 따라 최대 30 분이 소요될 수 있습니다. 이 초기 데이터는 한 번만 다운로드하여 모든 조명 지갑에 사용할 수 있습니다.", - "litecoin_what_is_mweb": "MWEB 란 무엇입니까?", - "live_fee_rates": "API를 통한 라이브 요금 요금", - "load_more": "더로드하십시오", - "loading_your_wallet": "지갑 넣기", + "ledger_connection_error": "Ledger 연결에 실패했습니다. 다시 시도하세요.", + "ledger_error_device_locked": "Ledger가 잠겨 있습니다", + "ledger_error_tx_rejected_by_user": "기기에서 트랜잭션 거부됨", + "ledger_error_wrong_app": "Ledger에서 올바른 앱을 열었는지 확인하세요.", + "ledger_please_enable_bluetooth": "Ledger를 감지하려면 블루투스를 활성화하세요.", + "legacy": "레거시", + "light_theme": "라이트 테마", + "litecoin_enable_mweb_sync": "MWEB 스캔 활성화", + "litecoin_mweb": "MWEB", + "litecoin_mweb_allow_coins": "MWEB 코인 허용", + "litecoin_mweb_always_scan": "MWEB 항상 스캔 설정", + "litecoin_mweb_description": "MWEB는 라이트코인에 더 빠르고, 저렴하며, 더 비공개적인 트랜잭션을 제공하는 새로운 프로토콜입니다.", + "litecoin_mweb_dismiss": "닫기", + "litecoin_mweb_display_card": "MWEB 카드 표시", + "litecoin_mweb_enable": "MWEB 활성화", + "litecoin_mweb_enable_later": "표시 설정에서 MWEB를 다시 활성화하도록 선택할 수 있습니다.", + "litecoin_mweb_logs": "MWEB 로그", + "litecoin_mweb_node": "MWEB 노드", + "litecoin_mweb_pegin": "페그 인", + "litecoin_mweb_pegout": "페그 아웃", + "litecoin_mweb_scanning": "MWEB 스캔 중", + "litecoin_mweb_settings": "MWEB 설정", + "litecoin_mweb_warning": "MWEB를 사용하면 초기에 약 600MB의 데이터가 다운로드되며 네트워크 속도에 따라 최대 30분이 소요될 수 있습니다. 이 초기 데이터는 한 번만 다운로드되며 모든 라이트코인 지갑에서 사용할 수 있습니다.", + "litecoin_what_is_mweb": "MWEB란 무엇인가요?", + "live_fee_rates": "API를 통한 실시간 수수료율", + "load_more": "더 로드", + "loading_your_wallet": "지갑 로딩 중", "login": "로그인", "logout": "로그아웃", "low_fee": "낮은 수수료", - "low_fee_alert": "현재 낮은 네트워크 요금 우선 순위를 사용하고 있습니다. 이로 인해 긴 대기 시간, 다른 요금 또는 취소된 거래가 발생할 수 있습니다. 더 나은 경험을 위해 더 높은 요금을 설정하는 것이 좋습니다.", + "low_fee_alert": "현재 낮은 네트워크 수수료 우선순위를 사용하고 있습니다. 이로 인해 대기 시간이 길어지거나, 환율이 달라지거나, 거래가 취소될 수 있습니다. 더 나은 경험을 위해 더 높은 수수료를 설정하는 것이 좋습니다.", "manage_nodes": "노드 관리", "manage_pow_nodes": "PoW 노드 관리", - "manage_yats": "관리하다 Yats", + "manage_yats": "Yat 관리", "mark_as_redeemed": "사용한 것으로 표시", "market_place": "마켓플레이스", "matrix_green_dark_theme": "매트릭스 그린 다크 테마", "max_amount": "최대: ${value}", - "max_value": "맥스: ${value} ${currency}", + "max_value": "최대: ${value} ${currency}", "memo": "메모:", "message": "메시지", "message_verified": "메시지가 성공적으로 확인되었습니다", "messages": "메시지", "method": "방법", - "methods": "행동 양식", + "methods": "방법", "min_amount": "최소: ${value}", "min_value": "최소: ${value} ${currency}", "mint_address": "민트 주소", - "minutes_to_pin_code": "${minute}분", - "mm": "mm", - "modify_2fa": "수정 케이크 2FA", + "minutes_to_pin_code": "${minute} 분", + "mm": "MM", + "modify_2fa": "Cake 2FA 수정", "monero_com": "Monero.com by Cake Wallet", - "monero_com_wallet_text": "Awesome wallet for Monero", + "monero_com_wallet_text": "모네로를 위한 멋진 지갑", "monero_dark_theme": "모네로 다크 테마", "monero_light_theme": "모네로 라이트 테마", - "moonpay_alert_text": "금액은 다음보다 크거나 같아야합니다 ${minAmount} ${fiatCurrency}", - "moralis_nft_error": "NFT를 가져 오는 동안 오류가 발생했습니다. 인터넷 연결을 친절하게 확인하고 다시 시도하십시오.", + "moonpay_alert_text": "금액 값은 ${minAmount} ${fiatCurrency} 이상이어야 합니다", + "moralis_nft_error": "NFT를 가져오는 중 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도하십시오.", "more_options": "추가 옵션", - "multiple_addresses_detected": "여러 주소가 감지되었습니다", - "mweb_confirmed": "확인 mweb", - "mweb_unconfirmed": "확인되지 않은 mweb", + "multiple_addresses_detected": "여러 주소 감지됨", + "mweb_confirmed": "확정된 MWEB", + "mweb_unconfirmed": "미확정 MWEB", "name": "이름", - "nano_current_rep": "현재 대표", - "nano_gpt_thanks_message": "Nanogpt를 사용해 주셔서 감사합니다! 거래가 완료된 후 브라우저로 돌아가는 것을 잊지 마십시오!", - "nano_pick_new_rep": "새로운 담당자를 선택하십시오", - "nanogpt_subtitle": "모든 최신 모델 (GPT-4, Claude). \\ nno 구독, Crypto로 지불하십시오.", - "narrow": "좁은", - "new_first_wallet_text": "암호화를 안전하게 유지하는 것은 케이크 조각입니다", - "new_node_testing": "새로운 노드 테스트", - "new_subaddress_create": "몹시 떠들어 대다", + "nano_current_rep": "현재 대표자", + "nano_gpt_thanks_message": "NanoGPT를 사용해 주셔서 감사합니다! 트랜잭션 완료 후 브라우저로 돌아가는 것을 잊지 마세요!", + "nano_pick_new_rep": "새 대표자 선택", + "nanogpt_subtitle": "모든 최신 모델 (GPT-4, Claude).\n구독 없이 암호화폐로 결제.", + "narrow": "좁게", + "new_first_wallet_text": "암호화폐를 안전하게 보관하는 것은 식은 죽 먹기", + "new_node_testing": "새 노드 테스트 중", + "new_subaddress_create": "생성", "new_subaddress_label_name": "라벨 이름", "new_subaddress_title": "새 주소", "new_template": "새 템플릿", - "new_transactions_notifications": "새로운 거래에 대한 알림을 보냅니다", - "new_wallet": "새 월렛", - "newConnection": "새로운 연결", - "no_cards_found": "카드를 찾지 못했습니다", - "no_extra_detail": "추가 세부 정보가 없습니다", - "no_id_needed": "ID가 필요하지 않습니다!", - "no_id_required": "신분증이 필요하지 않습니다. 충전하고 어디에서나 사용하세요", - "no_providers_available": "제공되는 제공자가 없습니다", - "no_relay_on_domain": "사용자 도메인에 릴레이가 없거나 릴레이를 사용할 수 없습니다. 사용할 릴레이를 선택해주세요.", + "new_transactions_notifications": "새 트랜잭션 알림 보내기", + "new_wallet": "새 지갑", + "newConnection": "새 연결", + "no_cards_found": "카드를 찾을 수 없습니다", + "no_extra_detail": "추가 세부 정보 없음", + "no_id_needed": "ID 필요 없음!", + "no_id_required": "ID 필요 없음. 충전하고 어디서나 사용하세요", + "no_providers_available": "사용 가능한 제공 업체 없음", + "no_relay_on_domain": "사용자 도메인에 릴레이가 없거나 릴레이를 사용할 수 없습니다. 사용할 릴레이를 선택하세요.", "no_relays": "릴레이 없음", - "no_relays_message": "이 사용자에 대한 Nostr NIP-05 레코드를 찾았지만 릴레이가 포함되어 있지 않습니다. 수신자에게 Nostr 기록에 릴레이를 추가하도록 지시하십시오.", + "no_relays_message": "이 사용자의 Nostr NIP-05 기록을 찾았지만 릴레이가 포함되어 있지 않습니다. 수신자에게 Nostr 기록에 릴레이를 추가하도록 지시하세요.", "node_address": "노드 주소", "node_connection_failed": "연결 실패", - "node_connection_successful": "성공적으로 연결되었습니다.", - "node_new": "새로운 노드", + "node_connection_successful": "연결 성공", + "node_new": "새 노드", "node_port": "노드 포트", "node_reset_settings_title": "설정 초기화", "node_test": "테스트", "nodes": "노드", - "nodes_list_reset_to_default_message": "설정을 기본값으로 재설정 하시겠습니까?", - "none_of_selected_providers_can_exchange": "선택한 공급자 중 어느 것도이 교환을 할 수 없습니다", - "noNFTYet": "아직 NFT가 없습니다", - "normal": "정상", - "note_optional": "참고 (선택 사항)", - "note_tap_to_change": "메모 (변경하려면 탭하세요)", - "notification_permission_denied": "알림 허가가 부패하게 거부되었습니다. 설정에서 수동으로 활성화하십시오.", - "nullURIError": "URI가 null입니다.", - "offer_expires_in": "쿠폰 만료일: ", + "nodes_list_reset_to_default_message": "설정을 기본값으로 초기화하시겠습니까?", + "none_of_selected_providers_can_exchange": "선택한 제공 업체 중 이 스왑을 할 수 있는 곳이 없습니다", + "noNFTYet": "아직 NFT 없음", + "normal": "보통", + "note_optional": "메모 (선택 사항)", + "note_tap_to_change": "메모 (탭하여 변경)", + "notification_permission_denied": "알림 권한이 영구적으로 거부되었습니다. 설정에서 수동으로 활성화하세요.", + "nullURIError": "URI가 null입니다", + "offer_expires_in": "오퍼 만료 시간: ", "offline": "오프라인", - "ok": "승인", - "old_fee": "옛 수수료", - "onion_link": "양파 링크", + "ok": "확인", + "old_fee": "이전 수수료", + "onion_link": "Onion 링크", "online": "온라인", - "onramper_option_description": "많은 결제 방법으로 암호화를 신속하게 구입하십시오. 대부분의 국가에서 사용할 수 있습니다. 스프레드와 수수료는 다양합니다.", + "onramper_option_description": "다양한 결제 방법으로 빠르게 암호화폐 구매. 대부분의 국가에서 사용 가능. 스프레드 및 수수료는 다양함.", "open_gift_card": "기프트 카드 열기", - "open_wallet": "오픈 지갑", + "open_wallet": "지갑 열기", "optional_description": "선택적 설명", "optional_email_hint": "선택적 수취인 알림 이메일", "optional_name": "선택적 수신자 이름", - "optionally_order_card": "선택적으로 실제 카드를 주문하십시오.", - "orbot_running_alert": "이 노드에 연결하기 전에 Orbot이 실행 중인지 확인하십시오.", - "order_by": "주문", + "optionally_order_card": "선택적으로 실물 카드 주문.", + "orbot_running_alert": "이 노드에 연결하기 전에 Orbot이 실행 중인지 확인하세요.", + "order_by": "정렬 기준", "order_id": "주문 ID", - "order_physical_card": "물리적 카드 주문", + "order_physical_card": "실물 카드 주문", "other_settings": "기타 설정", - "outdated_electrum_wallet_description": "Cake에서 생성 된 새로운 비트 코인 지갑에는 이제 24 단어 시드가 있습니다. 새로운 비트 코인 지갑을 생성하고 모든 자금을 새로운 24 단어 지갑으로 이체하고 12 단어 시드가있는 지갑 사용을 중지해야합니다. 자금을 확보하려면 즉시이 작업을 수행하십시오.", - "outdated_electrum_wallet_receive_warning": "이 지갑에 12 단어 시드가 있고 Cake에서 생성 된 경우이 지갑에 비트 코인을 입금하지 마십시오. 이 지갑으로 전송 된 모든 BTC는 손실 될 수 있습니다. 새로운 24 단어 지갑을 생성하고 (오른쪽 상단의 메뉴를 탭하고 지갑을 선택한 다음 새 지갑 생성을 선택한 다음 비트 코인을 선택하십시오) 즉시 BTC를 그곳으로 이동하십시오. Cake의 새로운 (24 단어) BTC 지갑은 안전합니다", - "outgoing": "나가는", + "outdated_electrum_wallet_description": "Cake에서 생성된 새 비트코인 지갑은 이제 24단어 시드를 사용합니다. 새 비트코인 지갑을 생성하고 모든 자금을 새 24단어 지갑으로 이전하고 12단어 시드를 사용하는 지갑 사용을 중단해야 합니다. 자금 보호를 위해 즉시 이 작업을 수행하십시오.", + "outdated_electrum_wallet_receive_warning": "이 지갑에 12단어 시드가 있고 Cake에서 생성된 경우 이 지갑에 비트코인을 입금하지 마십시오. 이 지갑으로 전송된 모든 BTC는 손실될 수 있습니다. 새 24단어 지갑을 생성(오른쪽 상단 메뉴 탭, 지갑 선택, 새 지갑 생성 선택, 비트코인 선택)하고 즉시 BTC를 그곳으로 옮기십시오. Cake의 새 (24단어) BTC 지갑은 안전합니다.", + "outgoing": "발신", "outputs": "출력", - "overshot": "오버 샷", - "overwrite_amount": "Overwrite amount", - "pairingInvalidEvent": "잘못된 이벤트 페어링", - "passphrase": "암호화 (선택 사항)", - "passphrases_doesnt_match": "패스 프레이즈가 일치하지 않습니다. 다시 시도하십시오", - "password": "암호", - "paste": "풀", - "pause_wallet_creation": "Haven Wallet 생성 기능이 현재 일시 중지되었습니다.", - "payment_id": "지불 ID: ", + "overshot": "초과", + "overwrite_amount": "금액 덮어쓰기", + "pairingInvalidEvent": "페어링 잘못된 이벤트", + "passphrase": "암호 구문 (선택 사항)", + "passphrase_view_keys": "암호 구문", + "passphrases_doesnt_match": "암호 구문이 일치하지 않습니다. 다시 시도하세요.", + "password": "비밀번호", + "paste": "붙여넣기", + "pause_wallet_creation": "현재 Haven 지갑 생성 기능이 일시 중지되었습니다.", + "payment_id": "결제 ID: ", "payment_was_received": "결제가 접수되었습니다.", - "pending": " (보류 중)", - "percentageOf": "${amount} 중", - "pin_at_top": "상단에 ${token} 고정", + "pending": " (대기 중)", + "percentageOf": "${amount}의", + "pin_at_top": "${token} 상단에 고정", "pin_is_incorrect": "PIN이 잘못되었습니다", "pin_number": "PIN 번호", "placeholder_contacts": "연락처가 여기에 표시됩니다", - "placeholder_transactions": "거래가 여기에 표시됩니다", - "please_choose_one": "하나를 선택하십시오", - "please_fill_totp": "다른 기기에 있는 8자리 코드를 입력하세요.", - "please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.", - "please_reference_document": "자세한 내용은 아래 문서를 참조하십시오.", - "please_select": "선택 해주세요:", - "please_select_backup_file": "백업 파일을 선택하고 백업 암호를 입력하십시오.", - "please_try_to_connect_to_another_node": "다른 노드에 연결을 시도하십시오", - "please_wait": "기다리세요", - "polygonscan_history": "다각형 스캔 기록", + "placeholder_transactions": "트랜잭션이 여기에 표시됩니다", + "please_choose_one": "하나를 선택하세요", + "please_fill_totp": "다른 기기에 표시된 8자리 코드를 입력하세요", + "please_make_selection": "지갑을 생성하거나 복구하려면 아래에서 선택하세요.", + "please_reference_document": "자세한 내용은 아래 문서를 참조하세요.", + "please_select": "선택하세요:", + "please_select_backup_file": "백업 파일을 선택하고 백업 비밀번호를 입력하세요.", + "please_try_to_connect_to_another_node": "다른 노드에 연결해 보세요", + "please_wait": "기다려 주세요", + "polygonscan_history": "PolygonScan 내역", "potential_scam": "잠재적 사기", - "powered_by": "에 의해 구동 ${title}", - "pre_seed_button_text": "이해 했어요. 내 씨앗을 보여줘", - "pre_seed_description": "다음 페이지에는 일련의 단어가 표시됩니다. 이것은 독특하고 개인적인 씨앗이며 손실이나 오작동의 경우 지갑을 회수하는 유일한 방법입니다. 케이크 지갑 앱 외부의 안전한 장소에 그것을 적어두고 보관하는 것은 귀하의 책임입니다.", - "pre_seed_title": "중대한", + "powered_by": "${title} 제공", + "pre_seed_button_text": "이해했습니다. 시드를 보여주세요", + "pre_seed_description": "다음 페이지에서 일련의 단어를 보게 됩니다. 이것은 귀하의 고유하고 비공개적인 시드이며, 분실 또는 오작동 시 지갑을 복구할 수 있는 유일한 방법입니다. Cake Wallet 앱 외부에 안전한 장소에 적어두고 보관하는 것은 귀하의 책임입니다.", + "pre_seed_title": "중요", "prepaid_cards": "선불 카드", "prevent_screenshots": "스크린샷 및 화면 녹화 방지", "primary_address": "기본 주소", - "privacy": "프라이버시", - "privacy_policy": "개인 정보 보호 정책", - "privacy_settings": "개인정보 설정", + "privacy": "개인 정보 보호", + "privacy_policy": "개인정보 보호정책", + "privacy_settings": "개인 정보 보호 설정", "private_key": "개인 키", "proceed_after_one_minute": "1분 후에도 화면이 진행되지 않으면 이메일을 확인하세요.", - "proceed_on_device": "장치를 진행하십시오", - "proceed_on_device_description": "하드웨어 지갑에 표시된 지침을 따르십시오", - "processing": "처리", + "proceed_on_device": "기기에서 진행", + "proceed_on_device_description": "하드웨어 지갑에 표시되는 지침을 따르세요.", + "processing": "처리 중", "profile": "프로필", "provider_error": "${provider} 오류", "public_key": "공개 키", "purchase_gift_card": "기프트 카드 구매", - "purple_dark_theme": "보라색 어두운 테마", - "qr_fullscreen": "전체 화면 QR 코드를 열려면 탭하세요.", - "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "purple_dark_theme": "보라색 다크 테마", + "qr_fullscreen": "전체 화면 QR 코드를 열려면 탭하세요", + "qr_payment_amount": "이 QR 코드에는 결제 금액이 포함되어 있습니다. 현재 값을 덮어쓰시겠습니까?", "quantity": "수량", - "question_to_disable_2fa": "Cake 2FA를 비활성화하시겠습니까? 지갑 및 특정 기능에 액세스하는 데 더 이상 2FA 코드가 필요하지 않습니다.", - "receivable_balance": "채권 잔액", - "receive": "받다", - "receive_amount": "양", - "received": "받았습니다", - "recipient_address": "받는 사람 주소", + "question_to_disable_2fa": "Cake 2FA를 비활성화하시겠습니까? 더 이상 지갑 및 특정 기능에 액세스하는 데 2FA 코드가 필요하지 않습니다.", + "receivable_balance": "수취 가능 잔액", + "receive": "받기", + "receive_amount": "금액", + "received": "받음", + "recipient_address": "수신자 주소", "reconnect": "다시 연결", - "reconnect_alert_text": "다시 연결 하시겠습니까?", - "reconnect_your_hardware_wallet": "하드웨어 지갑을 다시 연결하십시오", - "reconnection": "재 연결", - "red_dark_theme": "빨간 어두운 테마", - "red_light_theme": "빨간불 테마", - "redeemed": "구함", + "reconnect_alert_text": "정말 다시 연결하시겠습니까?", + "reconnect_your_hardware_wallet": "하드웨어 지갑 다시 연결", + "reconnection": "재연결", + "red_dark_theme": "빨간색 다크 테마", + "red_light_theme": "빨간색 라이트 테마", + "redeemed": "사용됨", "refund_address": "환불 주소", - "reject": "거부하다", - "remaining": "남은", - "remove": "없애다", + "reject": "거부", + "remaining": "남음", + "remove": "제거", "remove_node": "노드 제거", - "remove_node_message": "선택한 노드를 제거 하시겠습니까?", - "rename": "이름 바꾸기", - "rep_warning": "대표 경고", - "rep_warning_sub": "귀하의 대표는 양호한 상태가 아닌 것 같습니다. 새 것을 선택하려면 여기를 탭하십시오", - "repeat_wallet_password": "지갑 암호를 반복하십시오", - "repeated_password_is_incorrect": "반복 된 비밀번호가 올바르지 않습니다. 지갑 암호를 다시 반복하십시오.", - "requested": "요청", - "require_for_adding_contacts": "연락처 추가에 필요", + "remove_node_message": "선택한 노드를 정말로 제거하시겠습니까?", + "rename": "이름 변경", + "rep_warning": "대표자 경고", + "rep_warning_sub": "귀하의 대표자가 정상 상태가 아닌 것 같습니다. 여기를 탭하여 새 대표자를 선택하세요.", + "repeat_wallet_password": "지갑 비밀번호 다시 입력", + "repeated_password_is_incorrect": "반복 입력한 비밀번호가 잘못되었습니다. 지갑 비밀번호를 다시 입력하세요.", + "requested": "요청됨", + "require_for_adding_contacts": "연락처 추가 시 필요", "require_for_all_security_and_backup_settings": "모든 보안 및 백업 설정에 필요", - "require_for_assessing_wallet": "지갑 접근을 위해 필요", - "require_for_creating_new_wallets": "새 지갑 생성에 필요", - "require_for_exchanges_to_external_wallets": "외부 지갑으로의 교환을 위해 필요", - "require_for_exchanges_to_internal_wallets": "내부 지갑으로의 교환에 필요", - "require_for_sends_to_contacts": "연락처로 보내기에 필요", - "require_for_sends_to_internal_wallets": "내부 지갑으로 보내는 데 필요", - "require_for_sends_to_non_contacts": "비접촉자에게 보내는 데 필요", - "require_pin_after": "다음 이후에 PIN 필요", - "required_passphrase": "암호", - "rescan": "재검색", + "require_for_assessing_wallet": "지갑 접근 시 필요", + "require_for_creating_new_wallets": "새 지갑 생성 시 필요", + "require_for_exchanges_to_external_wallets": "외부 지갑으로 교환 시 필요", + "require_for_exchanges_to_internal_wallets": "내부 지갑으로 교환 시 필요", + "require_for_sends_to_contacts": "연락처로 전송 시 필요", + "require_for_sends_to_internal_wallets": "내부 지갑으로 전송 시 필요", + "require_for_sends_to_non_contacts": "연락처 외 대상으로 전송 시 필요", + "require_pin_after": "다음 시간 후 PIN 필요", + "required_passphrase": "암호 구문", + "rescan": "재스캔", "resend_code": "다시 보내주세요", - "reset": "다시 놓기", + "reset": "초기화", "reset_password": "비밀번호 재설정", - "restore": "복원하다", - "restore_active_seed": "활성 종자", + "restore": "복구", + "restore_active_seed": "활성 시드", "restore_address": "주소", - "restore_bitcoin_description_from_keys": "개인 키에서 생성 된 WIF 문자열에서 지갑 복원", - "restore_bitcoin_description_from_seed": "24 단어 조합 코드에서 지갑 복원", - "restore_bitcoin_title_from_keys": "WIF에서 복원", - "restore_description_from_backup": "백업 파일에서 전체 Cake Wallet 앱을 복원 할 수 있습니다.", - "restore_description_from_hardware_wallet": "원장 하드웨어 지갑에서 복원하십시오", - "restore_description_from_keys": "개인 키에서 저장된 생성 된 키 스트로크에서 월렛 복원", - "restore_description_from_seed": "25 단어 또는 13 단어 조합 코드에서 지갑을 복원하십시오.", - "restore_description_from_seed_keys": "안전한 장소에 저장 한 종자 / 키로 지갑을 되 찾으십시오.", - "restore_existing_wallet": "기존 지갑을 복원하십시오", - "restore_from_date_or_blockheight": "이 지갑을 생성하기 며칠 전에 날짜를 입력하십시오. 또는 블록 높이를 알고있는 경우 대신 입력하십시오.", - "restore_from_seed_placeholder": "여기에 코드 문구를 입력하거나 붙여 넣으십시오.", - "restore_new_seed": "새로운 씨앗", - "restore_next": "다음 것", - "restore_recover": "다시 덮다", - "restore_restore_wallet": "월렛 복원", - "restore_seed_keys_restore": "종자 / 키 복원", - "restore_spend_key_private": "지출 키 (은밀한)", - "restore_title_from_backup": "백업 파일에서 복원", - "restore_title_from_hardware_wallet": "하드웨어 지갑에서 복원하십시오", - "restore_title_from_keys": "키에서 복원", - "restore_title_from_seed": "종자에서 복원", - "restore_title_from_seed_keys": "시드 / 키에서 복원", - "restore_view_key_private": "키보기 (은밀한)", - "restore_wallet": "월렛 복원", + "restore_bitcoin_description_from_keys": "개인 키에서 생성된 WIF 문자열로 지갑 복구", + "restore_bitcoin_description_from_seed": "24단어 조합 코드로 지갑 복구", + "restore_bitcoin_title_from_keys": "WIF에서 복구", + "restore_description_from_backup": "백업 파일에서 전체 Cake Wallet 앱을 복구할 수 있습니다", + "restore_description_from_hardware_wallet": "Ledger 하드웨어 지갑에서 복구", + "restore_description_from_keys": "개인 키에서 저장된 키 입력으로 지갑 복구", + "restore_description_from_seed": "25단어 또는 13단어 조합 코드로 지갑 복구", + "restore_description_from_seed_keys": "안전한 장소에 저장한 시드/키로 지갑 복구", + "restore_existing_wallet": "기존 지갑 복구", + "restore_from_date_or_blockheight": "이 지갑을 생성하기 며칠 전의 날짜를 입력하세요. 또는 블록 높이를 아는 경우 대신 입력하세요", + "restore_from_seed_placeholder": "여기에 시드를 입력하거나 붙여넣으세요", + "restore_new_seed": "새 시드", + "restore_next": "다음", + "restore_recover": "복구", + "restore_restore_wallet": "지갑 복구", + "restore_seed_keys_restore": "시드/키 복구", + "restore_spend_key_private": "지출 키 (개인)", + "restore_title_from_backup": "백업에서 복구", + "restore_title_from_hardware_wallet": "하드웨어 지갑에서 복구", + "restore_title_from_keys": "키에서 복구", + "restore_title_from_seed": "시드에서 복구", + "restore_title_from_seed_keys": "시드/키에서 복구", + "restore_view_key_private": "보기 키 (개인)", + "restore_wallet": "지갑 복구", "restore_wallet_name": "지갑 이름", - "restore_wallet_restore_description": "월렛 복원 설명", - "robinhood_option_description": "직불 카드, 은행 계좌 또는 Robinhood 잔액을 사용하여 즉시 구매 및 양도하십시오. 미국 만.", - "router_no_route": "에 정의 된 경로가 없습니다 ${name}", - "save": "구하다", - "save_backup_password": "백업 암호를 저장했는지 확인하십시오. 그것 없이는 백업 파일을 가져올 수 없습니다.", + "restore_wallet_restore_description": "지갑 복구 설명", + "robinhood_option_description": "직불 카드, 은행 계좌 또는 Robinhood 잔액을 사용하여 즉시 구매 및 이체. 미국 전용.", + "router_no_route": "${name}에 대해 정의된 경로 없음", + "save": "저장", + "save_backup_password": "백업 비밀번호를 저장했는지 확인하세요. 없으면 백업 파일을 가져올 수 없습니다.", "save_backup_password_alert": "백업 비밀번호 저장", "save_to_downloads": "다운로드에 저장", "saved_the_trade_id": "거래 ID를 저장했습니다", - "scan_one_block": "하나의 블록을 스캔하십시오", + "scan_one_block": "블록 하나 스캔", "scan_qr_code": "QR 코드 스캔", - "scan_qr_code_to_get_address": "QR 코드를 스캔하여 주소를 얻습니다.", - "scan_qr_on_device": "다른 기기에서 이 QR 코드를 스캔하세요.", - "search": "찾다", - "search_add_token": "검색 / 토큰 추가", - "search_category": "검색 카테고리", + "scan_qr_code_to_get_address": "주소를 얻으려면 QR 코드를 스캔하세요", + "scan_qr_on_device": "다른 기기에서 이 QR 코드 스캔", + "search": "검색", + "search_add_token": "토큰 검색 / 추가", + "search_category": "카테고리 검색", "search_currency": "통화 검색", - "search_language": "검색 언어", - "second_intro_content": "귀하의 Yat는 귀하의 모든 통화에 대해 긴 16진수 주소를 모두 대체하는 고유한 단일 이모지 주소입니다.", - "second_intro_title": "그들을 모두 지배하는 하나의 이모티콘 주소", + "search_language": "언어 검색", + "second_intro_content": "Yat은 모든 통화에 대한 긴 16진수 주소를 대체하는 고유한 이모지 주소입니다.", + "second_intro_title": "하나의 이모지 주소로 모든 것을", "security_and_backup": "보안 및 백업", "security_risk": "보안 위험", - "security_risk_description": "이 도메인은 여러 보안 제공 업체가 안전하지 않은 것으로 표시됩니다. 자산을 보호하기 위해 즉시 떠나십시오.", - "seed_alert_back": "돌아 가기", - "seed_alert_content": "씨앗은 지갑을 복구하는 유일한 방법입니다. 적어 보셨나요?", + "security_risk_description": "이 도메인은 여러 보안 제공 업체에서 안전하지 않다고 플래그 지정되었습니다. 자산을 보호하려면 즉시 나가십시오.", + "seed_alert_back": "뒤로 가기", + "seed_alert_content": "시드는 지갑을 복구하는 유일한 방법입니다. 적어두셨습니까?", "seed_alert_title": "주의", - "seed_alert_yes": "네, 있어요", - "seed_choose": "시드 언어를 선택하십시오", - "seed_display_path": "메뉴 -> 보안 및 백업 -> 키/씨앗 표시", - "seed_hex_form": "지갑 씨앗 (16 진 양식)", + "seed_alert_yes": "네, 했습니다", + "seed_choose": "시드 언어 선택", + "seed_display_path": "메뉴 -> 보안 및 백업 -> 키/시드 표시", + "seed_hex_form": "지갑 시드 (16진수 형식)", "seed_key": "시드 키", - "seed_language": "종자 언어", - "seed_language_chinese": "중국말", - "seed_language_chinese_traditional": "중국 전통)", - "seed_language_czech": "체코 사람", - "seed_language_dutch": "네덜란드 사람", + "seed_language": "시드 언어", + "seed_language_chinese": "중국어", + "seed_language_chinese_traditional": "중국어 (번체)", + "seed_language_czech": "체코어", + "seed_language_dutch": "네덜란드어", "seed_language_english": "영어", - "seed_language_french": "프랑스 국민", - "seed_language_german": "독일 사람", - "seed_language_italian": "이탈리아 사람", + "seed_language_french": "프랑스어", + "seed_language_german": "독일어", + "seed_language_italian": "이탈리아어", "seed_language_japanese": "일본어", - "seed_language_korean": "한국인", - "seed_language_next": "다음 것", - "seed_language_portuguese": "포르투갈 인", - "seed_language_russian": "러시아인", - "seed_language_spanish": "스페인의", - "seed_phrase_length": "시드 문구 길이", - "seed_position_question_one": "무엇입니까", - "seed_position_question_two": "당신의 씨앗 문구의 말?", - "seed_reminder": "휴대 전화를 분실하거나 닦을 경우를 대비해 적어 두세요.", + "seed_language_korean": "한국어", + "seed_language_next": "다음", + "seed_language_portuguese": "포르투갈어", + "seed_language_russian": "러시아어", + "seed_language_spanish": "스페인어", + "seed_phrase_length": "시드 구문 길이", + "seed_position_question_one": "시드 구문의", + "seed_position_question_two": "번째 단어는 무엇입니까?", + "seed_reminder": "휴대폰을 분실하거나 초기화할 경우를 대비해 적어두세요.", "seed_share": "시드 공유", - "seed_title": "씨", - "seed_verified": "종자 확인", - "seed_verified_subtext": "나중에 저장된 씨앗을 사용하여 부패 또는 장치를 잃을 때이 지갑을 복원 할 수 있습니다.\n\n이 씨앗을 다시 볼 수 있습니다.", - "seedtype": "시드 타입", - "seedtype_alert_content": "다른 지갑과 씨앗을 공유하는 것은 BIP39 SeedType에서만 가능합니다.", - "seedtype_alert_title": "종자 경보", + "seed_title": "시드", + "seed_verified": "시드 확인됨", + "seed_verified_subtext": "저장된 시드를 사용하여 나중에 기기 손상 또는 분실 시 이 지갑을 복구할 수 있습니다.\n\n다음에서 이 시드를 다시 볼 수 있습니다.", + "seedtype": "시드 유형", + "seedtype_alert_content": "다른 지갑과의 시드 공유는 BIP39 시드 유형에서만 가능합니다.", + "seedtype_alert_title": "시드 유형 알림", "select_backup_file": "백업 파일 선택", - "select_buy_provider_notice": "위의 구매 제공자를 선택하십시오. 앱 설정에서 기본 구매 제공자를 설정 하여이 화면을 건너 뛸 수 있습니다.", - "select_destination": "백업 파일의 대상을 선택하십시오.", - "select_hw_account_below": "아래를 복원 할 계정을 선택하십시오.", - "select_sell_provider_notice": "위에서 판매 공급자를 선택하세요. 앱 설정에서 기본 판매 공급자를 설정하면 이 화면을 건너뛸 수 있습니다.", - "select_your_country": "국가를 선택하십시오", - "selected_trocador_provider": "선정 된 트로 코더 제공 업체", - "sell": "팔다", - "sell_alert_content": "현재 Bitcoin, Ethereum 및 Litecoin의 판매만 지원합니다. Bitcoin, Ethereum 또는 Litecoin 지갑을 생성하거나 전환하십시오.", - "sell_monero_com_alert_content": "지원되지 않습니다.", - "send": "보내다", + "select_buy_provider_notice": "위에서 구매 제공 업체를 선택하세요. 앱 설정에서 기본 구매 제공 업체를 설정하여 이 화면을 건너뛸 수 있습니다.", + "select_destination": "백업 파일의 대상을 선택하세요.", + "select_hw_account_below": "아래에서 복구할 계정을 선택하세요:", + "select_sell_provider_notice": "위에서 판매 제공 업체를 선택하세요. 앱 설정에서 기본 판매 제공 업체를 설정하여 이 화면을 건너뛸 수 있습니다.", + "select_your_country": "국가를 선택하세요", + "selected_trocador_provider": "선택된 Trocador 제공 업체", + "sell": "판매", + "sell_alert_content": "현재 비트코인, 이더리움, 라이트코인 판매만 지원합니다. 비트코인, 이더리움 또는 라이트코인 지갑을 생성하거나 전환하세요.", + "sell_monero_com_alert_content": "모네로 판매는 아직 지원되지 않습니다.", + "send": "보내기", "send_address": "${cryptoCurrency} 주소", - "send_amount": "양:", - "send_change_to_you": "당신에게 변경 :", - "send_creating_transaction": "거래 생성", - "send_error_currency": "통화는 숫자 만 포함 할 수 있습니다", - "send_error_minimum_value": "금액의 최소값은 0.01입니다", + "send_amount": "금액:", + "send_change_to_you": "잔돈, 귀하에게:", + "send_creating_transaction": "트랜잭션 생성 중", + "send_error_currency": "통화는 숫자만 포함할 수 있습니다", + "send_error_minimum_value": "최소 금액 값은 0.01입니다", "send_estimated_fee": "예상 수수료:", - "send_fee": "회비:", - "send_from_cake_wallet": "케이크 지갑에서 보내십시오", - "send_from_external_wallet": "외부 지갑에서 보내십시오", + "send_fee": "수수료:", + "send_from_cake_wallet": "Cake Wallet에서 보내기", + "send_from_external_wallet": "외부 지갑에서 보내기", "send_name": "이름", - "send_new": "새로운", - "send_payment_id": "지불 ID (optional)", - "send_priority": "현재 수수료는 ${transactionPriority} 우선 순위.\n거래 우선 순위는 설정에서 조정할 수 있습니다", - "send_sending": "배상...", - "send_success": "${crypto}가 성공적으로 전송되었습니다", + "send_new": "새로 만들기", + "send_payment_id": "결제 ID (선택 사항)", + "send_priority": "현재 수수료는 ${transactionPriority} 우선순위로 설정되어 있습니다.\n트랜잭션 우선순위는 설정에서 조정할 수 있습니다.", + "send_sending": "보내는 중...", + "send_success": "${crypto}이(가) 성공적으로 전송되었습니다", "send_templates": "템플릿", - "send_title": "보내다", - "send_to_this_address": "이 주소로 ${currency} ${tag}송금", - "send_xmr": "보내다 XMR", - "send_your_wallet": "지갑", - "sending": "배상", + "send_title": "보내기", + "send_to_this_address": "${currency} ${tag}을(를) 이 주소로 보내기", + "send_xmr": "XMR 보내기", + "send_your_wallet": "내 지갑", + "sending": "보내는 중", "sent": "보냄", - "service_health_disabled": "서비스 건강 게시판이 장애가되었습니다", - "service_health_disabled_message": "이것은 서비스 건강 게시판 페이지입니다. 설정 에서이 페이지를 활성화 할 수 있습니다 -> 개인 정보", - "set_a_pin": "핀을 설정하십시오", - "set_up_a_wallet": "지갑을 설정하십시오", + "service_health_disabled": "서비스 상태 게시판 비활성화됨", + "service_health_disabled_message": "이것은 서비스 상태 게시판 페이지이며, 설정 -> 개인 정보 보호에서 이 페이지를 활성화할 수 있습니다.", + "set_a_pin": "PIN 설정", + "set_up_a_wallet": "지갑 설정", "settings": "설정", - "settings_all": "모든", + "settings_all": "전체", "settings_allow_biometrical_authentication": "생체 인증 허용", "settings_can_be_changed_later": "이 설정은 나중에 앱 설정에서 변경할 수 있습니다.", "settings_change_language": "언어 변경", @@ -759,309 +760,308 @@ "settings_currency": "통화", "settings_current_node": "현재 노드", "settings_dark_mode": "다크 모드", - "settings_display_balance": "디스플레이 잔액", - "settings_display_on_dashboard_list": "대시 보드 목록에 표시", - "settings_fee_priority": "수수료 우선", + "settings_display_balance": "잔액 표시", + "settings_display_on_dashboard_list": "대시보드 목록에 표시", + "settings_fee_priority": "수수료 우선순위", "settings_nodes": "노드", "settings_none": "없음", - "settings_only_trades": "거래 만", - "settings_only_transactions": "거래 만", - "settings_personal": "개인적인", + "settings_only_trades": "거래만", + "settings_only_transactions": "트랜잭션만", + "settings_personal": "개인", "settings_save_recipient_address": "수신자 주소 저장", - "settings_support": "지원하다", + "settings_support": "지원", "settings_terms_and_conditions": "이용 약관", "settings_title": "설정", "settings_trades": "거래", - "settings_transactions": "업무", + "settings_transactions": "트랜잭션", "settings_wallets": "지갑", - "setup_2fa": "케이크 2FA 설정", - "setup_2fa_text": "Cake 2FA는 TOTP를 두 번째 인증 요소로 사용하여 작동합니다.\n\nCake 2FA의 TOTP에는 SHA-512 및 8자리 지원이 필요합니다. 이는 보안을 강화합니다. 자세한 정보와 지원되는 앱은 가이드에서 확인할 수 있습니다.", - "setup_pin": "설정 PIN", + "setup_2fa": "Cake 2FA 설정", + "setup_2fa_text": "Cake 2FA는 두 번째 인증 요소로 TOTP를 사용합니다.\n\nCake 2FA의 TOTP는 SHA-512 및 8자리 지원이 필요하며 이는 보안을 강화합니다. 자세한 정보 및 지원되는 앱은 가이드에서 찾을 수 있습니다.", + "setup_pin": "PIN 설정", "setup_successful": "PIN이 성공적으로 설정되었습니다!", "setup_totp_recommended": "TOTP 설정", - "setup_warning_2fa_text": "Cake 2FA는 지갑의 특정 작업에 대한 두 번째 인증입니다. 냉장 보관만큼 안전하지 않습니다.\n\n2FA 앱 또는 TOTP 키에 대한 액세스 권한을 상실하면 이 지갑에 대한 액세스 권한도 잃게 됩니다. 니모닉 시드에서 지갑을 복원해야 합니다.\n\n2FA 또는 니모닉 시드에 액세스할 수 없는 경우 Cake 지원팀에서 도움을 드릴 수 없습니다.\nCake 2FA를 사용하기 전에 가이드를 읽어 보시기 바랍니다.", - "setup_your_debit_card": "직불카드 설정", - "share": "공유하다", + "setup_warning_2fa_text": "Cake 2FA는 지갑의 특정 작업에 대한 두 번째 인증입니다. 콜드 스토리지처럼 안전하지는 않습니다.\n\n2FA 앱 또는 TOTP 키에 대한 액세스 권한을 잃으면 이 지갑에 대한 액세스 권한도 잃게 됩니다. 니모닉 시드에서 지갑을 복구해야 합니다.\n\n2FA 또는 니모닉 시드에 대한 액세스 권한을 잃으면 Cake 지원팀에서 도움을 드릴 수 없습니다.\nCake 2FA를 사용하기 전에 가이드를 읽어보는 것이 좋습니다.", + "setup_your_debit_card": "직불 카드 설정", + "share": "공유", "share_address": "주소 공유", - "shared_seed_wallet_groups": "공유 종자 지갑 그룹", - "show": "보여주다", - "show_address_book_popup": "주소록 팝업을 보여주십시오", - "show_balance": "균형을 보여주기 위해 긴 언론", - "show_balance_toast": "균형을 숨기거나 보여주기 위해 긴 누르십시오", - "show_details": "세부정보 표시", - "show_keys": "시드 / 키 표시", + "shared_seed_wallet_groups": "공유 시드 지갑 그룹", + "show": "표시", + "show_address_book_popup": "주소록 팝업 표시", + "show_balance": "길게 눌러 잔액 표시", + "show_balance_toast": "길게 눌러 잔액 숨기기 또는 표시", + "show_details": "세부 정보 표시", + "show_keys": "시드/키 표시", "show_market_place": "마켓플레이스 표시", - "show_seed": "종자 표시", - "sign_all": "모두 서명하십시오", - "sign_message": "서명 메시지", - "sign_one": "하나를 서명하십시오", + "show_seed": "시드 표시", + "sign_all": "모두 서명", + "sign_message": "메시지 서명", + "sign_one": "하나 서명", "sign_up": "가입", - "sign_verify_message": "메시지에 서명하거나 확인하십시오", - "sign_verify_message_sub": "개인 키를 사용하여 메시지에 서명하거나 확인하십시오", - "sign_verify_title": "서명 / 확인", + "sign_verify_message": "서명 / 검증", + "sign_verify_message_sub": "개인 키를 사용하여 메시지 서명 또는 검증", + "sign_verify_title": "서명 / 검증", "signature": "서명", - "signature_invalid_error": "서명은 주어진 메시지에 유효하지 않습니다", - "signTransaction": "거래 서명", - "signup_for_card_accept_terms": "카드에 가입하고 약관에 동의합니다.", - "silent_payment": "조용한 지불", - "silent_payments": "조용한 지불", - "silent_payments_always_scan": "무음금을 항상 스캔합니다", - "silent_payments_disclaimer": "새로운 주소는 새로운 정체성이 아닙니다. 다른 레이블로 기존 신원을 재사용하는 것입니다.", - "silent_payments_display_card": "사일런트 지불 카드 표시", - "silent_payments_scan_from_date": "날짜부터 스캔하십시오", - "silent_payments_scan_from_date_or_blockheight": "들어오는 사일런트 결제를 위해 스캔을 시작하려는 블록 높이를 입력하거나 대신 날짜를 사용하십시오. 지갑이 모든 블록을 계속 스캔하는지 여부를 선택하거나 지정된 높이 만 확인할 수 있습니다.", - "silent_payments_scan_from_height": "블록 높이에서 스캔하십시오", - "silent_payments_scanned_tip": "팁을 스캔했습니다! (${tip})", - "silent_payments_scanning": "조용한 지불 스캔", - "silent_payments_settings": "조용한 지불 설정", - "single_seed_wallets_group": "단일 씨앗 지갑", - "slidable": "슬라이딩 가능", - "solana_create_associated_token_account_exception": "오류 생성 관련 토큰 계정 수령자 주소에 대한 계정.", - "solana_no_associated_token_account_exception": "이 주소에는 관련 토큰 계정이 없습니다.", - "solana_sign_native_transaction_rent_exception": "거래는 완료 될 수 없습니다. 거래 후 SOL이 임대료로 남았습니다. SOL 밸런스를 친절하게 위로 올리거나 보내는 솔의 양을 줄입니다.", - "solana_sign_spl_token_transaction_rent_exception": "거래는 완료 될 수 없습니다. 거래 후 SOL이 임대료로 남았습니다. SOL 균형을 친절하게 위에 올리십시오.", + "signature_invalid_error": "주어진 메시지에 대한 서명이 유효하지 않습니다", + "signTransaction": "트랜잭션 서명", + "signup_for_card_accept_terms": "카드에 가입하고 약관에 동의하세요.", + "silent_payment": "사일런트 페이먼트", + "silent_payments": "사일런트 페이먼트", + "silent_payments_always_scan": "사일런트 페이먼트 항상 스캔 설정", + "silent_payments_disclaimer": "새 주소는 새 신원이 아닙니다. 다른 라벨이 있는 기존 신원의 재사용입니다.", + "silent_payments_display_card": "사일런트 페이먼트 카드 표시", + "silent_payments_scan_from_date": "날짜부터 스캔", + "silent_payments_scan_from_date_or_blockheight": "들어오는 사일런트 페이먼트 스캔을 시작할 블록 높이를 입력하거나 대신 날짜를 사용하세요. 지갑이 모든 블록을 계속 스캔할지 또는 지정된 높이만 확인할지 선택할 수 있습니다.", + "silent_payments_scan_from_height": "블록 높이부터 스캔", + "silent_payments_scanned_tip": "팁까지 스캔됨! (${tip})", + "silent_payments_scanning": "사일런트 페이먼트 스캔 중", + "silent_payments_settings": "사일런트 페이먼트 설정", + "single_seed_wallets_group": "단일 시드 지갑", + "slidable": "슬라이드 가능", + "solana_create_associated_token_account_exception": "수신자 주소에 대한 연관 토큰 계정 생성 오류.", + "solana_no_associated_token_account_exception": "이 주소에 대한 연관 토큰 계정이 없습니다.", + "solana_sign_native_transaction_rent_exception": "트랜잭션을 완료할 수 없습니다. 트랜잭션 후 렌트에 필요한 SOL이 부족합니다. SOL 잔액을 충전하거나 보내는 SOL 금액을 줄이세요.", + "solana_sign_spl_token_transaction_rent_exception": "트랜잭션을 완료할 수 없습니다. 트랜잭션 후 렌트에 필요한 SOL이 부족합니다. SOL 잔액을 충전하세요.", "sort_by": "정렬 기준", - "spend_key_private": "지출 키 (은밀한)", - "spend_key_public": "지출 키 (공공의)", - "status": "지위: ", - "string_default": "기본", + "spend_key_private": "지출 키 (개인)", + "spend_key_public": "지출 키 (공개)", + "status": "상태: ", + "string_default": "기본값", "subaddress_title": "하위 주소 목록", "subaddresses": "하위 주소", - "submit_request": "요청을 제출", + "submit_request": "요청 제출", "success": "성공", - "successful": "성공적인", - "support_description_guides": "일반적인 문제에 대한 문서화 및 지원", - "support_description_live_chat": "자유롭고 빠릅니다! 훈련 된 지원 담당자가 지원할 수 있습니다", - "support_description_other_links": "다른 방법을 통해 커뮤니티에 가입하거나 파트너에게 연락하십시오.", - "support_title_guides": "케이크 지갑 문서", + "successful": "성공적", + "support_description_guides": "일반적인 문제에 대한 문서 및 지원", + "support_description_live_chat": "무료 및 빠름! 숙련된 지원 담당자가 도움을 드릴 수 있습니다", + "support_description_other_links": "커뮤니티에 가입하거나 다른 방법을 통해 저희 또는 파트너에게 연락하세요", + "support_title_guides": "Cake Wallet 문서", "support_title_live_chat": "실시간 지원", - "support_title_other_links": "다른 지원 링크", - "supported": "지원", - "swap": "교환", - "sweeping_wallet": "스위핑 지갑", - "sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마십시오. 그렇지 않으면 스웹트 자금이 손실될 수 있습니다.", - "switchToETHWallet": "이더리움 지갑으로 전환한 후 다시 시도해 주세요.", - "switchToEVMCompatibleWallet": "EVM 호환 지갑으로 전환 후 다시 시도해 주세요. (이더리움, 폴리곤)", - "symbol": "상징", + "support_title_other_links": "기타 지원 링크", + "supported": "지원됨", + "swap": "스왑", + "sweeping_wallet": "지갑 스윕 중", + "sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나거나 스윕된 자금을 잃을 수 있습니다.", + "switchToETHWallet": "이더리움 지갑으로 전환하고 다시 시도하세요", + "switchToEVMCompatibleWallet": "EVM 호환 지갑(이더리움, 폴리곤)으로 전환하고 다시 시도하세요", + "symbol": "심볼", "sync_all_wallets": "모든 지갑 동기화", - "sync_status_attempting_scan": "스캔 시도", + "sync_status_attempting_scan": "스캔 시도 중", "sync_status_attempting_sync": "동기화 시도 중", "sync_status_connected": "연결됨", "sync_status_connecting": "연결 중", - "sync_status_failed_connect": "연결 해제", - "sync_status_not_connected": "연결되지 않은", - "sync_status_starting_scan": "시작 스캔 (${height} 에서)", - "sync_status_starting_sync": "동기화 시작", - "sync_status_syncronized": "동기화", - "sync_status_syncronizing": "동기화", + "sync_status_failed_connect": "연결 끊김", + "sync_status_not_connected": "연결되지 않음", + "sync_status_starting_scan": "스캔 시작 중 (${height}부터)", + "sync_status_starting_sync": "동기화 시작 중", + "sync_status_syncronized": "동기화됨", + "sync_status_syncronizing": "동기화 중", "sync_status_timed_out": "시간 초과", - "sync_status_unsupported": "지원되지 않은 노드", - "synchronizing": "동기화", - "syncing_wallet_alert_content": "상단에 \"동기화됨\"이라고 표시될 때까지 잔액 및 거래 목록이 완전하지 않을 수 있습니다. 자세히 알아보려면 클릭/탭하세요.", + "sync_status_unsupported": "지원되지 않는 노드", + "synchronizing": "동기화 중", + "syncing_wallet_alert_content": "상단에 “동기화됨”이라고 표시될 때까지 잔액 및 트랜잭션 목록이 완전하지 않을 수 있습니다. 자세히 알아보려면 클릭/탭하세요.", "syncing_wallet_alert_title": "지갑 동기화 중", - "template": "주형", + "template": "템플릿", "template_name": "템플릿 이름", - "testnet_coins_no_value": "Testnet 코인은 가치가 없습니다", - "third_intro_content": "Yats는 Cake Wallet 밖에서도 살고 있습니다. 지구상의 모든 지갑 주소는 Yat!", - "third_intro_title": "Yat는 다른 사람들과 잘 놉니다.", - "this_pair_is_not_supported_warning": "이 쌍은 현재 선택된 교환으로 지원되지 않습니다. 다른 교환을 선택하십시오.", - "thorchain_contract_address_not_supported": "Thorchain은 계약 주소로 보내는 것을 지원하지 않습니다", - "thorchain_taproot_address_not_supported": "Thorchain 제공 업체는 Taproot 주소를 지원하지 않습니다. 주소를 변경하거나 다른 공급자를 선택하십시오.", - "time": "${minutes}m ${seconds}s", + "testnet_coins_no_value": "테스트넷 코인은 가치가 없습니다", + "third_intro_content": "Yat은 Cake Wallet 외부에서도 사용됩니다. 지구상의 모든 지갑 주소는 Yat으로 대체될 수 있습니다!", + "third_intro_title": "Yat은 다른 것과 잘 어울립니다", + "this_pair_is_not_supported_warning": "현재 선택된 교환소에서는 이 쌍이 지원되지 않습니다. 다른 교환소를 선택하세요.", + "thorchain_contract_address_not_supported": "THORChain은 계약 주소로 보내는 것을 지원하지 않습니다", + "thorchain_taproot_address_not_supported": "ThorChain 제공 업체는 Taproot 주소를 지원하지 않습니다. 주소를 변경하거나 다른 제공 업체를 선택하세요.", + "time": "${minutes}분 ${seconds}초", "tip": "팁:", - "to": "에게", + "to": "받는 통화", "today": "오늘", "token_contract_address": "토큰 계약 주소", - "token_decimal": "토큰 십진수", - "token_name": "토큰 이름 예: Tether", - "token_symbol": "토큰 기호 예: USDT", + "token_decimal": "토큰 소수 자릿수", + "token_name": "토큰 이름 (예: Tether)", + "token_symbol": "토큰 심볼 (예: USDT)", "tokenID": "ID", - "tor_connection": "토르 연결", - "tor_only": "Tor 뿐", - "total": "총", - "total_saving": "총 절감액", - "totp_2fa_failure": "잘못된 코드입니다. 다른 코드를 시도하거나 새 비밀 키를 생성하십시오. 8자리 코드와 SHA512를 지원하는 호환되는 2FA 앱을 사용하세요.", - "totp_2fa_success": "성공! 이 지갑에 케이크 2FA가 활성화되었습니다. 지갑 액세스 권한을 잃을 경우를 대비하여 니모닉 시드를 저장하는 것을 잊지 마십시오.", + "tor_connection": "Tor 연결", + "tor_only": "Tor 전용", + "total": "합계", + "total_saving": "총 절약액", + "totp_2fa_failure": "잘못된 코드입니다. 다른 코드를 시도하거나 새 비밀 키를 생성하세요. 8자리 코드와 SHA512를 지원하는 호환 2FA 앱을 사용하세요.", + "totp_2fa_success": "성공! 이 지갑에 Cake 2FA가 활성화되었습니다. 지갑 액세스 권한을 잃을 경우를 대비하여 니모닉 시드를 저장하는 것을 잊지 마세요.", "totp_auth_url": "TOTP 인증 URL", "totp_code": "TOTP 코드", "totp_secret_code": "TOTP 비밀 코드", - "totp_verification_success": "확인 성공!", - "track": "길", - "trade_details_copied": "${title} 클립 보드에 복사", - "trade_details_created_at": "에 작성", - "trade_details_fetching": "가져 오는 중", + "totp_verification_success": "검증 성공!", + "track": "추적", + "trade_details_copied": "${title} 클립보드에 복사됨", + "trade_details_created_at": "생성 날짜", + "trade_details_fetching": "가져오는 중", "trade_details_id": "ID", "trade_details_pair": "쌍", - "trade_details_provider": "공급자", + "trade_details_provider": "제공 업체", "trade_details_state": "상태", - "trade_details_title": "거래 세부 사항", - "trade_for_not_created": "거래 ${title} 생성되지 않습니다.", - "trade_history_title": "무역 역사", - "trade_id": "무역 ID:", - "trade_id_not_found": "무역 ${tradeId} 의 ${title} 찾을 수 없습니다.", - "trade_is_powered_by": "이 거래는 ${provider}", - "trade_not_created": "거래가 생성되지 않았습니다", - "trade_not_found": "거래를 찾을 수 없습니다.", - "trade_state_btc_sent": "보냄", - "trade_state_complete": "완전한", + "trade_details_title": "거래 세부 정보", + "trade_for_not_created": "${title}에 대한 거래가 생성되지 않았습니다.", + "trade_history_title": "거래 내역", + "trade_id": "거래 ID:", + "trade_id_not_found": "${title}의 거래 ${tradeId}를 찾을 수 없습니다.", + "trade_is_powered_by": "이 거래는 ${provider}에서 제공합니다", + "trade_not_created": "거래가 생성되지 않음", + "trade_not_found": "거래를 찾을 수 없음.", + "trade_state_btc_sent": "BTC 전송됨", + "trade_state_complete": "완료", "trade_state_confirming": "확인 중", - "trade_state_created": "만들어진", - "trade_state_finished": "끝마친", - "trade_state_paid": "유료", - "trade_state_paid_unconfirmed": "미확인 유료", + "trade_state_created": "생성됨", + "trade_state_finished": "완료됨", + "trade_state_paid": "지불됨", + "trade_state_paid_unconfirmed": "지불됨 (미확인)", "trade_state_pending": "대기 중", - "trade_state_timeout": "타임 아웃", - "trade_state_to_be_created": "만들려면", - "trade_state_traded": "거래", - "trade_state_trading": "거래", - "trade_state_underpaid": "미지급", - "trade_state_unpaid": "미지급", + "trade_state_timeout": "시간 초과", + "trade_state_to_be_created": "생성 예정", + "trade_state_traded": "거래됨", + "trade_state_trading": "거래 중", + "trade_state_underpaid": "과소 지불됨", + "trade_state_unpaid": "미지불", "trades": "거래", - "transaction_cost": "거래 비용", - "transaction_details_amount": "양", - "transaction_details_copied": "${title} 클립 보드에 복사", + "transaction_cost": "트랜잭션 비용", + "transaction_details_amount": "금액", + "transaction_details_copied": "${title} 클립보드에 복사됨", "transaction_details_date": "날짜", - "transaction_details_fee": "회비", - "transaction_details_height": "신장", - "transaction_details_recipient_address": "받는 사람 주소", - "transaction_details_source_address": "소스 주소", - "transaction_details_title": "상세 거래 내역", + "transaction_details_fee": "수수료", + "transaction_details_height": "높이", + "transaction_details_recipient_address": "수신자 주소", + "transaction_details_source_address": "송신자 주소", + "transaction_details_title": "트랜잭션 세부 정보", "transaction_details_transaction_id": "트랜잭션 ID", - "transaction_key": "거래 키", - "transaction_priority_fast": "빠른", - "transaction_priority_fastest": "가장 빠른", - "transaction_priority_medium": "매질", - "transaction_priority_regular": "정규병", - "transaction_priority_slow": "느린", - "transaction_sent": "거래가 전송되었습니다!", - "transaction_sent_notice": "1분 후에도 화면이 진행되지 않으면 블록 익스플로러와 이메일을 확인하세요.", - "transactions": "업무", - "transactions_by_date": "날짜 별 거래", - "transport_type": "운송 유형", - "trongrid_history": "트롱 트리드 역사", - "trusted": "신뢰할 수 있는", - "tx_commit_exception_no_dust_on_change": "이 금액으로 거래가 거부되었습니다. 이 코인을 사용하면 거스름돈 없이 ${min}를 보내거나 거스름돈을 반환하는 ${max}를 보낼 수 있습니다.", - "tx_commit_failed": "거래 커밋이 실패했습니다. 지원에 연락하십시오.", - "tx_commit_failed_no_peers": "트랜잭션이 방송에 실패했는데 1 초 정도 후에 다시 시도하십시오.", - "tx_invalid_input": "이 유형의 지불에 잘못 입력 유형을 사용하고 있습니다.", - "tx_no_dust_exception": "너무 작은 금액을 보내면 거래가 거부됩니다. 금액을 늘리십시오.", - "tx_not_enough_inputs_exception": "사용 가능한 입력이 충분하지 않습니다. 코인 컨트롤에서 더 많은 것을 선택하십시오", - "tx_rejected_bip68_final": "거래는 확인되지 않은 입력을 받았으며 수수료로 교체하지 못했습니다.", - "tx_rejected_dust_change": "네트워크 규칙, 낮은 변경 금액 (먼지)에 의해 거부 된 거래. 전부를 보내거나 금액을 줄이십시오.", - "tx_rejected_dust_output": "네트워크 규칙, 낮은 출력 금액 (먼지)에 의해 거부 된 거래. 금액을 늘리십시오.", - "tx_rejected_dust_output_send_all": "네트워크 규칙, 낮은 출력 금액 (먼지)에 의해 거부 된 거래. 동전 제어에서 선택한 동전의 균형을 확인하십시오.", - "tx_rejected_vout_negative": "이 거래 수수료를 지불하기에 잔액이 충분하지 않습니다. 동전 통제하에 동전의 균형을 확인하십시오.", - "tx_wrong_balance_exception": "이 금액을 보내기에 충분한 ${currency}가 충분하지 않습니다.", - "tx_wrong_balance_with_amount_exception": "충분하지 않습니다 ${currency} 총 ${amount} 총 금액을 보내십시오.", - "tx_zero_fee_exception": "0 수수료로 거래를 보낼 수 없습니다. 최신 견적에 대해서는 속도를 높이거나 연결을 확인하십시오.", - "unavailable_balance": "사용할 수 없는 잔액", - "unavailable_balance_description": "사용할 수 없는 잔액: 이 총계에는 보류 중인 거래에 잠겨 있는 자금과 코인 관리 설정에서 적극적으로 동결된 자금이 포함됩니다. 잠긴 잔액은 해당 거래가 완료되면 사용할 수 있게 되며, 동결된 잔액은 동결을 해제하기 전까지 거래에 액세스할 수 없습니다.", - "unconfirmed": "확인되지 않은 잔액", - "understand": "이해 했어요", - "unlock": "터놓다", - "unmatched_currencies": "현재 지갑의 통화가 스캔한 QR의 통화와 일치하지 않습니다.", - "unrestricted_background_service": "무제한 배경 서비스", - "unrestricted_background_service_notice": "배경 동기화를 활성화하려면 무제한 배경 서비스를 활성화해야합니다.", - "unspent_change": "변화", - "unspent_coins_details_title": "사용하지 않은 동전 세부 정보", - "unspent_coins_title": "사용하지 않은 동전", - "unsupported_asset": "이 저작물에 대해 이 작업을 지원하지 않습니다. 지원되는 자산 유형의 지갑을 생성하거나 전환하십시오.", - "update_session": "업데이트 세션", + "transaction_key": "트랜잭션 키", + "transaction_priority_fast": "빠름", + "transaction_priority_fastest": "가장 빠름", + "transaction_priority_medium": "중간", + "transaction_priority_regular": "보통", + "transaction_priority_slow": "느림", + "transaction_sent": "트랜잭션 전송됨!", + "transaction_sent_notice": "1분 후에도 화면이 진행되지 않으면 블록 탐색기와 이메일을 확인하세요.", + "transactions": "트랜잭션", + "transactions_by_date": "날짜별 트랜잭션", + "transport_type": "전송 유형", + "trongrid_history": "TronGrid 내역", + "trusted": "신뢰됨", + "tx_commit_exception_no_dust_on_change": "이 금액으로는 트랜잭션이 거부됩니다. 이 코인으로는 잔돈 없이 ${min}을(를) 보내거나 잔돈이 반환되는 ${max}을(를) 보낼 수 있습니다.", + "tx_commit_failed": "트랜잭션 커밋 실패. 지원팀에 문의하세요.", + "tx_commit_failed_no_peers": "트랜잭션 전파 실패. 잠시 후 다시 시도하세요.", + "tx_invalid_input": "이 유형의 결제에 잘못된 입력 유형을 사용하고 있습니다.", + "tx_no_dust_exception": "너무 적은 금액을 보내 트랜잭션이 거부되었습니다. 금액을 늘려보세요.", + "tx_not_enough_inputs_exception": "사용 가능한 입력이 충분하지 않습니다. 코인 제어에서 더 많이 선택하세요.", + "tx_rejected_bip68_final": "트랜잭션에 미확인 입력이 있으며 수수료로 교체(RBF)하지 못했습니다.", + "tx_rejected_dust_change": "네트워크 규칙에 의해 트랜잭션 거부됨, 낮은 잔돈 금액 (더스트). 전체 보내기를 시도하거나 금액을 줄이세요.", + "tx_rejected_dust_output": "네트워크 규칙에 의해 트랜잭션 거부됨, 낮은 출력 금액 (더스트). 금액을 늘리세요.", + "tx_rejected_dust_output_send_all": "네트워크 규칙에 의해 트랜잭션 거부됨, 낮은 출력 금액 (더스트). 코인 제어에서 선택한 코인 잔액을 확인하세요.", + "tx_rejected_vout_negative": "이 트랜잭션 수수료를 지불하기에 잔액이 부족합니다. 코인 제어에서 코인 잔액을 확인하세요.", + "tx_wrong_balance_exception": "이 금액을 보내기에 ${currency}이(가) 충분하지 않습니다.", + "tx_wrong_balance_with_amount_exception": "총 금액 ${amount}을(를) 보내기에 ${currency}이(가) 충분하지 않습니다.", + "tx_zero_fee_exception": "수수료 0으로 트랜잭션을 보낼 수 없습니다. 요율을 높이거나 연결을 확인하여 최신 예상치를 확인하세요.", + "unavailable_balance": "사용 불가 잔액", + "unavailable_balance_description": "사용 불가 잔액: 이 총액에는 보류 중인 트랜잭션에 잠긴 자금과 코인 제어 설정에서 적극적으로 동결한 자금이 포함됩니다. 잠긴 잔액은 해당 트랜잭션이 완료되면 사용할 수 있게 되며, 동결된 잔액은 동결 해제를 결정할 때까지 트랜잭션에 액세스할 수 없습니다.", + "unconfirmed": "미확정 잔액", + "understand": "이해합니다", + "unlock": "잠금 해제", + "unmatched_currencies": "현재 지갑 통화가 스캔된 QR의 통화와 일치하지 않습니다.", + "unrestricted_background_service": "제한 없는 백그라운드 서비스", + "unrestricted_background_service_notice": "백그라운드 동기화를 활성화하려면 제한 없는 백그라운드 서비스를 활성화해야 합니다.", + "unspent_change": "잔돈", + "unspent_coins_details_title": "미사용 코인 세부 정보", + "unspent_coins_title": "미사용 코인", + "unsupported_asset": "이 자산에 대해 이 작업을 지원하지 않습니다. 지원되는 자산 유형의 지갑을 생성하거나 전환하세요.", + "update_session": "세션 업데이트", "uptime": "가동 시간", - "upto": "최대 ${value}", + "upto": "${value}까지", "usb": "USB", - "use": "사용하다 ", - "use_card_info_three": "디지털 카드를 온라인 또는 비접촉식 결제 수단으로 사용하십시오.", - "use_card_info_two": "디지털 화폐가 아닌 선불 계정에 보유하면 자금이 USD로 변환됩니다.", + "use": "다음으로 전환 ", + "use_card_info_three": "디지털 카드를 온라인 또는 비접촉 결제 방법으로 사용하세요.", + "use_card_info_two": "자금은 디지털 통화가 아닌 선불 계정에 보관될 때 USD로 변환됩니다.", "use_ssl": "SSL 사용", - "use_suggested": "추천 사용", - "use_testnet": "TestNet을 사용하십시오", - "user_rejected_method": "사용자 거부 방법", + "use_suggested": "제안 사용", + "use_testnet": "테스트넷 사용", + "user_rejected_method": "사용자가 방법 거부", "value": "값", - "value_type": "가치 유형", - "variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.", + "value_type": "값 유형", + "variable_pair_not_supported": "선택한 교환소에서는 이 변동 쌍이 지원되지 않습니다.", "verification": "검증", - "verify_message": "메시지를 확인하십시오", - "verify_seed": "씨앗을 확인하십시오", - "verify_with_2fa": "케이크 2FA로 확인", + "verify_message": "메시지 검증", + "verify_seed": "시드 검증", + "verify_with_2fa": "Cake 2FA로 검증", "version": "버전 ${currentVersion}", "view_all": "모두 보기", - "view_in_block_explorer": "View in Block Explorer", - "view_key_private": "키보기(은밀한)", - "view_key_public": "키보기 (공공의)", - "view_transaction_on": "View Transaction on ", - "voting_weight": "투표 중량", - "waitFewSecondForTxUpdate": "거래 내역에 거래가 반영될 때까지 몇 초 정도 기다려 주세요.", + "view_in_block_explorer": "블록 탐색기에서 보기", + "view_key_private": "보기 키 (개인)", + "view_key_public": "보기 키 (공개)", + "view_transaction_on": "다음에서 트랜잭션 보기 ", + "voting_weight": "투표 가중치", + "waitFewSecondForTxUpdate": "트랜잭션이 트랜잭션 내역에 반영될 때까지 몇 초간 기다려 주십시오", "wallet": "지갑", "wallet_group": "지갑 그룹", - "wallet_group_description_existing_seed": "이 지갑에 기존 씨앗을 사용하기로 선택했습니다. 확인하거나 작성 해야하는 경우 씨앗을 다시 확인할 수 있습니다.", - "wallet_group_description_four": "완전히 새로운 씨앗으로 지갑을 만듭니다.", - "wallet_group_description_one": "케이크 지갑에서는 a를 만들 수 있습니다", - "wallet_group_description_open_wallet": "그렇지 않으면 지갑을 계속 열 수 있습니다", - "wallet_group_description_three": "사용 가능한 지갑 및/또는 지갑 그룹 스크린을 볼 수 있습니다. 또는 선택하십시오", - "wallet_group_description_two": "씨앗을 공유 할 기존 지갑을 선택함으로써. 각 지갑 그룹은 각 통화 유형의 단일 지갑을 포함 할 수 있습니다. \n\n", - "wallet_group_description_view_seed": "이 씨앗을 언제든지 다시 볼 수 있습니다", - "wallet_group_empty_state_text_one": "호환 지갑 그룹이없는 것 같습니다 !\n\n TAP", - "wallet_group_empty_state_text_two": "아래에서 새로운 것을 만들기 위해.", - "wallet_has_passphrase": "이 지갑에는 암호가 있습니다", - "wallet_keys": "지갑 시드 / 키", - "wallet_list_create_new_wallet": "새 월렛 만들기", + "wallet_group_description_existing_seed": "이 지갑에 기존 시드를 사용하도록 선택했습니다. 확인하거나 적어두어야 하는 경우 시드를 다시 확인할 수 있습니다.", + "wallet_group_description_four": "완전히 새로운 시드로 지갑을 생성합니다.", + "wallet_group_description_one": "Cake Wallet에서는 다음을 생성할 수 있습니다", + "wallet_group_description_open_wallet": "그렇지 않으면 계속해서 지갑을 열 수 있습니다", + "wallet_group_description_three": "사용 가능한 지갑 및/또는 지갑 그룹 화면을 봅니다. 또는 다음을 선택합니다", + "wallet_group_description_two": "시드를 공유할 기존 지갑을 선택하여. 각 지갑 그룹에는 각 통화 유형의 단일 지갑이 포함될 수 있습니다.\n\n다음을 선택할 수 있습니다", + "wallet_group_description_view_seed": "다음에서 이 시드를 항상 다시 볼 수 있습니다", + "wallet_group_empty_state_text_one": "호환되는 지갑 그룹이 없는 것 같습니다!\n\n탭", + "wallet_group_empty_state_text_two": "아래에서 새 그룹을 만듭니다.", + "wallet_has_passphrase": "이 지갑에는 암호 구문이 있습니다", + "wallet_keys": "지갑 시드/키", + "wallet_list_create_new_wallet": "새 지갑 생성", "wallet_list_edit_group_name": "그룹 이름 편집", - "wallet_list_edit_wallet": "지갑 수정", - "wallet_list_failed_to_load": "불러 오지 못했습니다 ${wallet_name} 지갑. ${error}", - "wallet_list_failed_to_remove": "제거하지 못했습니다 ${wallet_name} 지갑. ${error}", - "wallet_list_load_wallet": "지갑로드", - "wallet_list_loading_wallet": "로딩 ${wallet_name} 지갑", - "wallet_list_removing_wallet": "풀이 ${wallet_name} 지갑", - "wallet_list_restore_wallet": "월렛 복원", - "wallet_list_title": "모네로 월렛", + "wallet_list_edit_wallet": "지갑 편집", + "wallet_list_failed_to_load": "${wallet_name} 지갑 로드 실패. ${error}", + "wallet_list_failed_to_remove": "${wallet_name} 지갑 제거 실패. ${error}", + "wallet_list_load_wallet": "지갑 로드", + "wallet_list_loading_wallet": "${wallet_name} 지갑 로딩 중", + "wallet_list_removing_wallet": "${wallet_name} 지갑 제거 중", + "wallet_list_restore_wallet": "지갑 복구", + "wallet_list_title": "모네로 지갑", "wallet_list_wallet_name": "지갑 이름", - "wallet_menu": "월렛 메뉴", + "wallet_menu": "메뉴", "wallet_name": "지갑 이름", - "wallet_name_exists": "해당 이름의 지갑이 이미 존재합니다.", - "wallet_password_is_empty": "지갑 암호는 비어 있습니다. 지갑 암호는 비어 있지 않아야합니다", + "wallet_name_exists": "해당 이름의 지갑이 이미 존재합니다. 다른 이름을 선택하거나 먼저 다른 지갑의 이름을 변경하십시오.", + "wallet_password_is_empty": "지갑 비밀번호가 비어 있습니다. 지갑 비밀번호는 비워둘 수 없습니다.", "wallet_recovery_height": "복구 높이", - "wallet_restoration_store_incorrect_seed_length": "시드 길이가 잘못되었습니다", + "wallet_restoration_store_incorrect_seed_length": "잘못된 시드 길이", "wallet_seed": "지갑 시드", "wallet_seed_legacy": "레거시 지갑 시드", - "wallet_store_monero_wallet": "모네로 월렛", - "walletConnect": "월렛커넥트", + "wallet_store_monero_wallet": "모네로 지갑", + "walletConnect": "WalletConnect", "wallets": "지갑", "warning": "경고", - "welcome": "환영 에", - "welcome_subtitle_new_wallet": "신선하게 시작하려면 아래에서 새 지갑을 만들면 레이스로 떠날 것입니다.", - "welcome_subtitle_restore_wallet": "케이크에 가져 오려는 기존 지갑이 있다면 기존 지갑 복원을 선택하면 프로세스를 안내해 드리겠습니다.", + "welcome": "환영합니다", + "welcome_subtitle_new_wallet": "새로 시작하려면 아래의 새 지갑 생성을 탭하면 바로 시작할 수 있습니다.", + "welcome_subtitle_restore_wallet": "Cake로 가져오려는 기존 지갑이 있는 경우 기존 지갑 복구를 선택하면 과정을 안내해 드립니다.", "welcome_to_cakepay": "Cake Pay에 오신 것을 환영합니다!", - "what_is_silent_payments": "조용한 지불이란 무엇입니까?", + "what_is_silent_payments": "사일런트 페이먼트란 무엇인가요?", "widgets_address": "주소", "widgets_or": "또는", - "widgets_restore_from_blockheight": "블록 높이에서 복원", - "widgets_restore_from_date": "날짜에서 복원", - "widgets_seed": "씨", + "widgets_restore_from_blockheight": "블록 높이에서 복구", + "widgets_restore_from_date": "날짜에서 복구", + "widgets_seed": "시드", "wouoldLikeToConnect": "연결하고 싶습니다", - "write_down_backup_password": "백업 파일 가져 오기에 사용되는 백업 암호를 적어 두십시오.", - "xlm_extra_info": "교환을 위해 XLM 거래를 보낼 때 메모 ID를 지정하는 것을 잊지 마십시오", - "xmr_available_balance": "사용 가능한 잔액", - "xmr_full_balance": "풀 밸런스", + "write_down_backup_password": "백업 파일 가져오기에 사용되는 백업 비밀번호를 적어두세요.", + "xlm_extra_info": "교환을 위해 XLM 트랜잭션을 보낼 때 메모 ID를 지정하는 것을 잊지 마세요.", + "xmr_available_balance": "사용 가능 잔액", + "xmr_full_balance": "전체 잔액", "xmr_hidden": "숨김", "xmr_to_error": "XMR.TO 오류", - "xmr_to_error_description": "금액이 잘못되었습니다. 소수점 이하 최대 8 자리", - "xrp_extra_info": "교환을 위해 XRP 트랜잭션을 보내는 동안 대상 태그를 지정하는 것을 잊지 마십시오", + "xmr_to_error_description": "잘못된 금액입니다. 소수점 이하 최대 8자리", + "xrp_extra_info": "교환을 위해 XRP 트랜잭션을 보낼 때 목적지 태그를 지정하는 것을 잊지 마세요.", "yat": "Yat", "yat_address": "Yat 주소", - "yat_alert_content": "Cake Wallet 사용자는 이제 독특한 이모티콘 기반 사용자 이름으로 좋아하는 모든 통화를 보내고 받을 수 있습니다.", - "yat_alert_title": "Yat으로 더 쉽게 암호화폐를 보내고 받으십시오.", + "yat_alert_content": "Cake Wallet 사용자는 이제 고유한 이모지 기반 사용자 이름으로 좋아하는 모든 통화를 보내고 받을 수 있습니다.", + "yat_alert_title": "Yat으로 암호화폐를 더 쉽게 보내고 받으세요", "yat_error": "Yat 오류", - "yat_error_content": "이 Yat와 연결된 주소가 없습니다. 다른 Yat 시도", - "yat_popup_content": "이제 Yat(이모티콘 기반의 짧은 사용자 이름)으로 Cake Wallet에서 암호화폐를 보내고 받을 수 있습니다. 설정 화면에서 언제든지 Yats 관리", - "yat_popup_title": "지갑 주소를 이모티콘으로 만들 수 있습니다.", + "yat_error_content": "이 Yat과 연결된 주소가 없습니다. 다른 Yat을 시도하세요", + "yat_popup_content": "이제 짧은 이모지 기반 사용자 이름인 Yat으로 Cake Wallet에서 암호화폐를 보내고 받을 수 있습니다. 설정 화면에서 언제든지 Yat을 관리하세요.", + "yat_popup_title": "지갑 주소를 이모지로 만들 수 있습니다.", "yesterday": "어제", - "you_now_have_debit_card": "이제 직불카드가 있습니다.", - "you_pay": "당신이 지불합니다", - "you_will_get": "로 변환하다", - "you_will_receive_estimated_amount": "(추정 )을 받게됩니다.", - "you_will_send": "다음에서 변환", - "youCanGoBackToYourDapp": "이제 DAPP로 돌아갈 수 있습니다", - "YY": "YY", + "you_now_have_debit_card": "이제 직불 카드가 있습니다", + "you_pay": "지불 금액", + "you_will_get": "전환 대상", + "you_will_receive_estimated_amount": "받게 될 금액 (예상)", + "you_will_send": "전환 출처", + "youCanGoBackToYourDapp": "이제 dApp으로 돌아갈 수 있습니다", "yy": "YY" -} \ No newline at end of file +} From cab47923884111f59076643c3feb2e245fd8318d Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 7 May 2025 17:22:03 +0300 Subject: [PATCH 14/90] fix: possible context null check issue in exchange trade (#2253) --- lib/src/screens/exchange_trade/exchange_trade_page.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 4b7b8ace9..d68a63f9c 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -307,9 +307,7 @@ class ExchangeTradeState extends State { if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!context.mounted) { - return; - } + if (!mounted) return; await showModalBottomSheet( context: context, @@ -323,7 +321,7 @@ class ExchangeTradeState extends State { actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'), actionButton: () { Navigator.of(bottomSheetContext).pop(); - if (context.mounted) { + if (mounted) { Navigator.of(context).pushNamedAndRemoveUntil( Routes.dashboard, (route) => false, From c6f9d12a8e58d08b22cc4c7b8d9e1c49f6303fb5 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 7 May 2025 16:46:17 +0200 Subject: [PATCH 15/90] fix: selection of the wordlist for seed validation on the restore from seed page (#2255) --- lib/src/screens/restore/wallet_restore_from_seed_form.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index af8261662..431574b38 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -215,7 +215,7 @@ class WalletRestoreFromSeedFormState extends State { items: _getItems(), selectedAtIndex: isPolyseed ? 1 - : seedTypeController.value.text.contains("14") && widget.type == WalletType.wownero + : (seedTypeController.value.text.contains("14") && widget.type == WalletType.wownero) || isBip39 ? 2 : 0, mainAxisAlignment: MainAxisAlignment.start, @@ -325,9 +325,9 @@ class WalletRestoreFromSeedFormState extends State { '${language.replaceAll("POLYSEED_", "")} (Seed language)'; void _changeSeedType(MoneroSeedType item) { - _setSeedType(item); - _changeLanguage('English'); widget.seedSettingsViewModel.setMoneroSeedType(item); + _setSeedType(item); + _changeLanguage('English', isPolyseed || isBip39); } void _setSeedType(MoneroSeedType item) { From a9e05073a5995a9a18384656aae23f2dc2b18e25 Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 7 May 2025 17:48:13 +0300 Subject: [PATCH 16/90] fix: include destination parameter in payment method fetch request (#2254) --- lib/buy/onramper/onramper_buy_provider.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 6b903342f..dc9812d1d 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -60,9 +60,15 @@ class OnRamperBuyProvider extends BuyProvider { Future> getAvailablePaymentTypes( String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { - final params = {'type': isBuyAction ? 'buy' : 'sell'}; + final normalizedCryptoCurrency = + cryptoCurrency.title + _getNormalizeNetwork(cryptoCurrency); - final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$fiatCurrency', params); + final sourceCurrency = (isBuyAction ? fiatCurrency : normalizedCryptoCurrency).toLowerCase(); + final destinationCurrency = (isBuyAction ? normalizedCryptoCurrency : fiatCurrency).toLowerCase(); + + final params = {'type': isBuyAction ? 'buy' : 'sell', 'destination' : destinationCurrency}; + + final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$sourceCurrency', params); try { final response = @@ -75,7 +81,9 @@ class OnRamperBuyProvider extends BuyProvider { .map((item) => PaymentMethod.fromOnramperJson(item as Map)) .toList(); } else { - printV('Failed to fetch available payment types'); + final responseBody = + jsonDecode(response.body) as Map; + printV('Failed to fetch available payment types: ${responseBody['message']}'); return []; } } catch (e) { From 5b43cb5373295bbae521117a90a1af59755a2ea6 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 8 May 2025 23:47:34 +0300 Subject: [PATCH 17/90] fix: hide balance for monero accounts (#2256) --- lib/di.dart | 2 +- .../monero_account_list_view_model.dart | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/di.dart b/lib/di.dart index 067fd7c4f..f6de95c4d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -839,7 +839,7 @@ Future setup({ if (wallet.type == WalletType.monero || wallet.type == WalletType.wownero || wallet.type == WalletType.haven) { - return MoneroAccountListViewModel(wallet); + return MoneroAccountListViewModel(wallet,getIt.get()); } throw Exception( 'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel'); diff --git a/lib/view_model/monero_account_list/monero_account_list_view_model.dart b/lib/view_model/monero_account_list/monero_account_list_view_model.dart index 4ecffcfb3..37b28980a 100644 --- a/lib/view_model/monero_account_list/monero_account_list_view_model.dart +++ b/lib/view_model/monero_account_list/monero_account_list_view_model.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_type.dart'; @@ -12,7 +14,9 @@ class MoneroAccountListViewModel = MoneroAccountListViewModelBase with _$MoneroAccountListViewModel; abstract class MoneroAccountListViewModelBase with Store { - MoneroAccountListViewModelBase(this._wallet) : scrollOffsetFromTop = 0; + MoneroAccountListViewModelBase(this._wallet,this.settingsStore) : scrollOffsetFromTop = 0; + + final SettingsStore settingsStore; @observable double scrollOffsetFromTop; @@ -26,13 +30,14 @@ abstract class MoneroAccountListViewModelBase with Store { @computed List get accounts { + final hideBalance = settingsStore.balanceDisplayMode == BalanceDisplayMode.hiddenBalance; if (_wallet.type == WalletType.monero) { return monero !.getAccountList(_wallet) .accounts.map((acc) => AccountListItem( label: acc.label, id: acc.id, - balance: acc.balance, + balance: hideBalance ? '●●●●●●' : acc.balance, isSelected: acc.id == monero!.getCurrentAccount(_wallet).id)) .toList(); } @@ -43,7 +48,7 @@ abstract class MoneroAccountListViewModelBase with Store { .accounts.map((acc) => AccountListItem( label: acc.label, id: acc.id, - balance: acc.balance, + balance: hideBalance ? '●●●●●●' : acc.balance, isSelected: acc.id == wownero!.getCurrentAccount(_wallet).id)) .toList(); } From b28ea4199c0ac28b3a504ab4ad25253754e8434f Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Fri, 9 May 2025 15:01:09 +0300 Subject: [PATCH 18/90] Remove SwapTrade --- lib/exchange/provider/swaptrade_exchange_provider.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart index d3f64b712..b86482335 100644 --- a/lib/exchange/provider/swaptrade_exchange_provider.dart +++ b/lib/exchange/provider/swaptrade_exchange_provider.dart @@ -47,10 +47,10 @@ class SwapTradeExchangeProvider extends ExchangeProvider { String get title => 'SwapTrade'; @override - bool get isAvailable => true; + bool get isAvailable => false; @override - bool get isEnabled => true; + bool get isEnabled => false; @override bool get supportsFixedRate => false; @@ -59,7 +59,7 @@ class SwapTradeExchangeProvider extends ExchangeProvider { ExchangeProviderDescription get description => ExchangeProviderDescription.swapTrade; @override - Future checkIsAvailable() async => true; + Future checkIsAvailable() async => false; @override Future fetchLimits({ From b1fc9ba2e538f03d44c33d6b2b9e980825c06712 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Sun, 11 May 2025 01:04:47 +0300 Subject: [PATCH 19/90] add words count next to seed type --- lib/entities/seed_type.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/entities/seed_type.dart b/lib/entities/seed_type.dart index 0548d9cee..0c5632875 100644 --- a/lib/entities/seed_type.dart +++ b/lib/entities/seed_type.dart @@ -8,10 +8,10 @@ class MoneroSeedType extends EnumerableItem with Serializable { static const defaultSeedType = polyseed; - static const legacy = MoneroSeedType(raw: 0, title: 'Legacy'); - static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed'); + static const legacy = MoneroSeedType(raw: 0, title: 'Legacy (25 words)'); + static const polyseed = MoneroSeedType(raw: 1, title: 'Polyseed (16 words)'); static const wowneroSeed = MoneroSeedType(raw: 2, title: 'Wownero'); - static const bip39 = MoneroSeedType(raw: 3, title: 'BIP39'); + static const bip39 = MoneroSeedType(raw: 3, title: 'BIP39 (12 words)'); static MoneroSeedType deserialize({required int raw}) { switch (raw) { From b4e16a05b1a7061044451aca942752a954dae33d Mon Sep 17 00:00:00 2001 From: eveneast <166489430+eveneast@users.noreply.github.com> Date: Sun, 11 May 2025 23:38:00 +0800 Subject: [PATCH 20/90] chore: fix some typos in comment (#2258) Signed-off-by: eveneast --- cw_tron/lib/tron_client.dart | 2 +- cw_zano/lib/api/model/destination.dart | 2 +- lib/core/node_address_validator.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index 8eca02af6..ee93fbd53 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -428,7 +428,7 @@ class TronClient { if (!request.isSuccess) { log("Tron TRC20 error: ${request.error} \n ${request.respose}"); throw Exception( - 'An error occured while creating the transfer request. Please try again.', + 'An error occurred while creating the transfer request. Please try again.', ); } diff --git a/cw_zano/lib/api/model/destination.dart b/cw_zano/lib/api/model/destination.dart index 3db4f6652..13f4673ab 100644 --- a/cw_zano/lib/api/model/destination.dart +++ b/cw_zano/lib/api/model/destination.dart @@ -1,5 +1,5 @@ class Destination { - final BigInt amount; // transfered as string + final BigInt amount; // transferred as string final String address; final String assetId; diff --git a/lib/core/node_address_validator.dart b/lib/core/node_address_validator.dart index 68478d501..ce8709a3c 100644 --- a/lib/core/node_address_validator.dart +++ b/lib/core/node_address_validator.dart @@ -19,7 +19,7 @@ class NodePathValidator extends TextValidator { } // NodeAddressValidatorDecredBlankException allows decred to send a blank ip -// address which effectively clears the current set persistant peer. +// address which effectively clears the current set persistent peer. class NodeAddressValidatorDecredBlankException extends TextValidator { NodeAddressValidatorDecredBlankException() : super( From e7fd1582a875c8c8a2222ae3320e8fe0da27c4b0 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 12 May 2025 18:08:43 +0300 Subject: [PATCH 21/90] enable macos deps in prebuilts download --- tool/download_moneroc_prebuilds.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tool/download_moneroc_prebuilds.dart b/tool/download_moneroc_prebuilds.dart index 8889a1bc1..fd9cd6dd6 100644 --- a/tool/download_moneroc_prebuilds.dart +++ b/tool/download_moneroc_prebuilds.dart @@ -22,6 +22,8 @@ final List triplets = [ // "aarch64-apple-darwin11", // apple silicon macbooks (contrib) - not used by cake // "host-apple-darwin", // not available on CI (yet) // "x86_64-host-apple-darwin", // not available on CI (yet) + "aarch64-apple-darwin", // apple silicon macbooks + "x86_64-apple-darwin", // intel macbooks "aarch64-host-apple-darwin", // apple silicon macbooks (local builds) "aarch64-apple-ios", "aarch64-apple-iossimulator", From 82e3ebf4fa9839e56da02ca53e55c64098c66394 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 12 May 2025 19:33:14 +0200 Subject: [PATCH 22/90] implement-payjoin (#1949) * Initial Payjoin * Initial Payjoin * More payjoin stuff * Minor fixes * Minor fixes * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix minor bug causes by data inconsistency in the btc utxos * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Initial Payjoin * Initial Payjoin * More payjoin stuff * Minor fixes * Minor fixes * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix minor bug causes by data inconsistency in the btc utxos * Minor cleanup * Minor cleanup * Minor cleanup * Minor cleanup * Fix Rebase issues * Move PJ Receiver to isolate * Add Payjoin Setting * Payjoin Sender are now isolated * Added Payjoin sessions to tx overview. Fix Fee issue with payjoin * Clean up code * Fix taproot for payjoin * Fix CI Errors * Add Payjoin UI elements and details page * Add Payjoin UI elements and details page * Fix Translations * feat: Detect Payjoin URIs in pasted text and show to the User sending Payjoin * feat: rename pjUri to payjoinURI for more code clarity * Update res/values/strings_pl.arb Co-authored-by: cyan * Update cw_bitcoin/lib/payjoin/manager.dart Co-authored-by: cyan * Update cw_bitcoin/lib/payjoin/manager.dart Co-authored-by: cyan * feat: Disable Payjoin per default * feat: Disable Payjoin fully if disabled or no Inputs available * feat: Resume Payjoin if app comes back to foreground * chore: Revert overly aggressive code formats * feat: show correct Payjoin amount for receivers * feat: Improved payjoin status * feat: Show payjoin errors on payjoin details screen * deps: update flutter to 3.27.4 * feat: Revert localisations * bug: Remove duplicate transaction id on payjoin details * style: remove double await in payjoin sender * refactor(cw_bitcoin): Refactor method signatures and convert constructor to factory * refactor(cw_bitcoin): Refactor wallet service and PSBT signer for cleaner code Removed unnecessary `CakeHive` dependency and refactored `BitcoinWallet` initialization to use `payjoinSessionSource`. Improved code readability in `PsbtSigner` by reformatting lines and simplifying constructor methods for `UtxoWithPrivateKey`. * fix: Resume Payjoin Sessions and load PJUri after sleep * feat: Add "Copy Payjoin URL button" to receive screen * fix: Add "Payjoin enabled"-Box below QR Code on the receive screen * fix: Set payjoin_enabled color to black independent of the theme * refactor: Payjoin session management and cleanup unused code. --------- Co-authored-by: Omar Hatem Co-authored-by: cyan --- assets/images/payjoin.png | Bin 0 -> 30662 bytes cw_bitcoin/lib/address_from_output.dart | 44 ++- .../lib/bitcoin_transaction_credentials.dart | 10 +- cw_bitcoin/lib/bitcoin_wallet.dart | 227 ++++++++++--- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 26 ++ cw_bitcoin/lib/bitcoin_wallet_service.dart | 12 +- cw_bitcoin/lib/electrum_wallet.dart | 6 + cw_bitcoin/lib/payjoin/manager.dart | 298 ++++++++++++++++++ .../lib/payjoin/payjoin_receive_worker.dart | 219 +++++++++++++ .../lib/payjoin/payjoin_send_worker.dart | 119 +++++++ .../lib/payjoin/payjoin_session_errors.dart | 16 + cw_bitcoin/lib/payjoin/storage.dart | 95 ++++++ .../lib/pending_bitcoin_transaction.dart | 9 + cw_bitcoin/lib/psbt/signer.dart | 263 ++++++++++++++++ .../transaction_builder.dart} | 0 cw_bitcoin/lib/psbt/utils.dart | 41 +++ cw_bitcoin/lib/psbt/v0_deserialize.dart | 52 +++ cw_bitcoin/lib/psbt/v0_finalizer.dart | 143 +++++++++ cw_bitcoin/pubspec.lock | 53 +++- cw_bitcoin/pubspec.yaml | 5 + cw_core/lib/hive_type_ids.dart | 1 + cw_core/lib/payjoin_session.dart | 67 ++++ cw_core/pubspec.lock | 2 +- cw_decred/pubspec.lock | 2 +- cw_monero/pubspec.lock | 2 +- cw_nano/pubspec.lock | 2 +- cw_wownero/pubspec.lock | 2 +- cw_zano/pubspec.lock | 2 +- lib/bitcoin/cw_bitcoin.dart | 82 +++-- lib/di.dart | 25 ++ lib/entities/preferences_key.dart | 1 + lib/main.dart | 9 + lib/reactions/on_current_wallet_change.dart | 5 + lib/router.dart | 9 + lib/routes.dart | 1 + .../dashboard/pages/transactions_page.dart | 21 ++ .../widgets/payjoin_transaction_row.dart | 99 ++++++ .../payjoin_details/payjoin_details_page.dart | 77 +++++ .../screens/receive/widgets/qr_widget.dart | 86 +++-- lib/src/screens/root/root.dart | 10 +- lib/src/screens/send/send_page.dart | 2 +- lib/src/screens/send/widgets/send_card.dart | 30 +- lib/src/screens/settings/privacy_page.dart | 8 + .../trade_details_status_item.dart | 4 +- lib/src/widgets/address_text_field.dart | 34 +- lib/src/widgets/standard_list_status_row.dart | 6 +- .../dashboard/payjoin_transactions_store.dart | 47 +++ lib/store/settings_store.dart | 11 + lib/utils/payment_request.dart | 11 +- .../dashboard/dashboard_view_model.dart | 41 ++- .../payjoin_transaction_list_item.dart | 34 ++ .../payjoin_details_view_model.dart | 122 +++++++ lib/view_model/send/send_view_model.dart | 7 + .../settings/privacy_settings_view_model.dart | 38 ++- .../wallet_address_list_view_model.dart | 25 +- res/values/strings_ar.arb | 9 +- res/values/strings_bg.arb | 9 +- res/values/strings_cs.arb | 9 +- res/values/strings_de.arb | 7 + res/values/strings_en.arb | 7 + res/values/strings_es.arb | 7 + res/values/strings_fr.arb | 9 +- res/values/strings_ha.arb | 9 +- res/values/strings_hi.arb | 9 +- res/values/strings_hr.arb | 9 +- res/values/strings_hy.arb | 9 +- res/values/strings_id.arb | 9 +- res/values/strings_it.arb | 9 +- res/values/strings_ja.arb | 9 +- res/values/strings_ko.arb | 7 + res/values/strings_my.arb | 9 +- res/values/strings_nl.arb | 9 +- res/values/strings_pl.arb | 9 +- res/values/strings_pt.arb | 9 +- res/values/strings_ru.arb | 9 +- res/values/strings_th.arb | 9 +- res/values/strings_tl.arb | 9 +- res/values/strings_tr.arb | 9 +- res/values/strings_uk.arb | 9 +- res/values/strings_ur.arb | 9 +- res/values/strings_vi.arb | 9 +- res/values/strings_yo.arb | 9 +- res/values/strings_zh.arb | 9 +- tool/configure.dart | 13 +- 84 files changed, 2622 insertions(+), 198 deletions(-) create mode 100644 assets/images/payjoin.png create mode 100644 cw_bitcoin/lib/payjoin/manager.dart create mode 100644 cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart create mode 100644 cw_bitcoin/lib/payjoin/payjoin_send_worker.dart create mode 100644 cw_bitcoin/lib/payjoin/payjoin_session_errors.dart create mode 100644 cw_bitcoin/lib/payjoin/storage.dart create mode 100644 cw_bitcoin/lib/psbt/signer.dart rename cw_bitcoin/lib/{psbt_transaction_builder.dart => psbt/transaction_builder.dart} (100%) create mode 100644 cw_bitcoin/lib/psbt/utils.dart create mode 100644 cw_bitcoin/lib/psbt/v0_deserialize.dart create mode 100644 cw_bitcoin/lib/psbt/v0_finalizer.dart create mode 100644 cw_core/lib/payjoin_session.dart create mode 100644 lib/src/screens/dashboard/widgets/payjoin_transaction_row.dart create mode 100644 lib/src/screens/payjoin_details/payjoin_details_page.dart create mode 100644 lib/store/dashboard/payjoin_transactions_store.dart create mode 100644 lib/view_model/dashboard/payjoin_transaction_list_item.dart create mode 100644 lib/view_model/payjoin_details_view_model.dart diff --git a/assets/images/payjoin.png b/assets/images/payjoin.png new file mode 100644 index 0000000000000000000000000000000000000000..1ba3dccdbfa7799cd0cd78560ecb23048e0c1cd2 GIT binary patch literal 30662 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 outputs; final BitcoinTransactionPriority? priority; final int? feeRate; final UnspentCoinType coinTypeToSpendFrom; + final String? payjoinUri; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 7135d0a7a..a23b72660 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -3,22 +3,33 @@ import 'dart:convert'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/psbt_transaction_builder.dart'; -import 'package:cw_core/encryption_file_utils.dart'; -import 'package:cw_bitcoin/electrum_derivations.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cw_bitcoin/payjoin/manager.dart'; +import 'package:cw_bitcoin/payjoin/storage.dart'; +import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cw_bitcoin/psbt/signer.dart'; +import 'package:cw_bitcoin/psbt/transaction_builder.dart'; +import 'package:cw_bitcoin/psbt/v0_deserialize.dart'; +import 'package:cw_bitcoin/psbt/v0_finalizer.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/payjoin_session.dart'; +import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; +import 'package:ledger_bitcoin/psbt.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; @@ -31,6 +42,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required Box payjoinBox, required EncryptionFileUtils encryptionFileUtils, Uint8List? seedBytes, String? mnemonic, @@ -71,20 +83,21 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // String derivationPath = walletInfo.derivationInfo!.derivationPath!; // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); - walletAddresses = BitcoinWalletAddresses( - walletInfo, - initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - initialSilentAddresses: initialSilentAddresses, - initialSilentAddressIndex: initialSilentAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), - network: networkParam ?? network, - masterHd: - seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, - isHardwareWallet: walletInfo.isHardwareWallet, - ); + + payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this); + walletAddresses = BitcoinWalletAddresses(walletInfo, + initialAddresses: initialAddresses, + initialRegularAddressIndex: initialRegularAddressIndex, + initialChangeAddressIndex: initialChangeAddressIndex, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, + mainHd: hd, + sideHd: accountHD.childKey(Bip32KeyIndex(1)), + network: networkParam ?? network, + masterHd: + seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, + isHardwareWallet: walletInfo.isHardwareWallet, + payjoinManager: payjoinManager); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = @@ -100,6 +113,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required Box payjoinBox, required EncryptionFileUtils encryptionFileUtils, String? passphrase, String? addressPageType, @@ -122,9 +136,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { break; case DerivationType.electrum: default: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = + await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; } + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -141,6 +157,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, + payjoinBox: payjoinBox, ); } @@ -148,6 +165,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String name, required WalletInfo walletInfo, required Box unspentCoinsInfo, + required Box payjoinBox, required String password, required EncryptionFileUtils encryptionFileUtils, required bool alwaysScan, @@ -204,7 +222,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { switch (walletInfo.derivationInfo!.derivationType) { case DerivationType.electrum: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = + await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); break; case DerivationType.bip39: default: @@ -217,24 +236,24 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } return BitcoinWallet( - mnemonic: mnemonic, - xpub: keysData.xPub, - password: password, - passphrase: passphrase, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp?.addresses, - initialSilentAddresses: snp?.silentAddresses, - initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, - initialBalance: snp?.balance, - encryptionFileUtils: encryptionFileUtils, - seedBytes: seedBytes, - initialRegularAddressIndex: snp?.regularAddressIndex, - initialChangeAddressIndex: snp?.changeAddressIndex, - addressPageType: snp?.addressPageType, - networkParam: network, - alwaysScan: alwaysScan, - ); + mnemonic: mnemonic, + xpub: keysData.xPub, + password: password, + passphrase: passphrase, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + initialAddresses: snp?.addresses, + initialSilentAddresses: snp?.silentAddresses, + initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, + initialBalance: snp?.balance, + encryptionFileUtils: encryptionFileUtils, + seedBytes: seedBytes, + initialRegularAddressIndex: snp?.regularAddressIndex, + initialChangeAddressIndex: snp?.changeAddressIndex, + addressPageType: snp?.addressPageType, + networkParam: network, + alwaysScan: alwaysScan, + payjoinBox: payjoinBox); } LedgerConnection? _ledgerConnection; @@ -247,20 +266,25 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { derivationPath: walletInfo.derivationInfo!.derivationPath!); } - @override - Future buildHardwareWalletTransaction({ + late final PayjoinManager payjoinManager; + + bool get isPayjoinAvailable => unspentCoinsInfo.values + .where((element) => + element.walletId == id && element.isSending && !element.isFrozen) + .isNotEmpty; + + Future buildPsbt({ required List outputs, required BigInt fee, required BasedUtxoNetwork network, required List utxos, required Map publicKeys, + required Uint8List masterFingerprint, String? memo, bool enableRBF = false, BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async { - final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(); - final psbtReadyInputs = []; for (final utxo in utxos) { final rawTx = @@ -278,13 +302,128 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )); } - final psbt = PSBTTransactionBuild( - inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); + return PSBTTransactionBuild( + inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF) + .psbt; + } - final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt); + @override + Future buildHardwareWalletTransaction({ + required List outputs, + required BigInt fee, + required BasedUtxoNetwork network, + required List utxos, + required Map publicKeys, + String? memo, + bool enableRBF = false, + BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, + BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, + }) async { + final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(); + + final psbt = await buildPsbt( + outputs: outputs, + fee: fee, + network: network, + utxos: utxos, + publicKeys: publicKeys, + masterFingerprint: masterFingerprint, + memo: memo, + enableRBF: enableRBF, + inputOrdering: inputOrdering, + outputOrdering: outputOrdering, + ); + + final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt); return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex)); } + @override + Future createTransaction(Object credentials) async { + credentials = credentials as BitcoinTransactionCredentials; + + final tx = (await super.createTransaction(credentials)) + as PendingBitcoinTransaction; + + final payjoinUri = credentials.payjoinUri; + if (payjoinUri == null) return tx; + + final transaction = await buildPsbt( + utxos: tx.utxos, + outputs: tx.outputs + .map((e) => BitcoinOutput( + address: addressFromScript(e.scriptPubKey), + value: e.amount, + isSilentPayment: e.isSilentPayment, + isChange: e.isChange, + )) + .toList(), + fee: BigInt.from(tx.fee), + network: network, + memo: credentials.outputs.first.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: true, + publicKeys: tx.publicKeys!, + masterFingerprint: Uint8List(0)); + + final originalPsbt = await signPsbt( + base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); + + tx.commitOverride = () async { + final sender = await payjoinManager.initSender( + payjoinUri, originalPsbt, int.parse(tx.feeRate)); + payjoinManager.spawnNewSender( + sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount)); + }; + + return tx; + } + + List getUtxoWithPrivateKeys() => unspentCoins + .where((e) => (e.isSending && !e.isFrozen)) + .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this)) + .toList(); + + Future commitPsbt(String finalizedPsbt) { + final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt)); + + final btcTx = + BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract())); + + return PendingBitcoinTransaction( + btcTx, + type, + electrumClient: electrumClient, + amount: 0, + fee: 0, + feeRate: "", + network: network, + hasChange: true, + ).commit(); + } + + Future signPsbt( + String preProcessedPsbt, List utxos) async { + final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt)); + + await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) { + return utxo.utxo.isP2tr() + ? key.signTapRoot( + txDigest, + sighash: sighash, + tweak: utxo.utxo.isSilentPayment != true, + ) + : key.signInput(txDigest, sigHash: sighash); + }, (txId, vout) async { + final txHex = await electrumClient.getTransactionHex(hash: txId); + final output = BtcTransaction.fromRaw(txHex).outputs[vout]; + return TaprootAmountScriptPair(output.amount, output.scriptPubKey); + }); + + psbt.finalizeV0(); + return base64Encode(psbt.asPsbtV0()); + } + @override Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 1a122ef9e..0fefe4e57 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,10 +1,13 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin/payjoin/manager.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/unspent_coin_type.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; +import 'package:payjoin_flutter/receive.dart' as payjoin; part 'bitcoin_wallet_addresses.g.dart'; @@ -17,6 +20,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required super.sideHd, required super.network, required super.isHardwareWallet, + required this.payjoinManager, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, @@ -25,6 +29,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S super.masterHd, }) : super(walletInfo); + final PayjoinManager payjoinManager; + + @observable + payjoin.Receiver? currentPayjoinReceiver; + + @computed + String? get payjoinEndpoint => + currentPayjoinReceiver?.pjUriBuilder().build().pjEndpoint(); + @override String getAddress( {required int index, @@ -45,4 +58,17 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return generateP2WPKHAddress(hd: hd, index: index, network: network); } + + Future initPayjoin() async { + currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); + + payjoinManager.resumeSessions(); + } + + Future newPayjoinReceiver() async { + currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress); + + printV("Initializing new Payjoin Receiver"); + payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!); + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 7ee1534bf..317b25bcd 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -5,6 +5,7 @@ import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_service.dart'; @@ -21,10 +22,12 @@ class BitcoinWalletService extends WalletService< BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { - BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); + BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, + this.payjoinSessionSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; + final Box payjoinSessionSource; final bool alwaysScan; final bool isDirect; @@ -55,6 +58,7 @@ class BitcoinWalletService extends WalletService< passphrase: credentials.passphrase, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -79,6 +83,7 @@ class BitcoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -92,6 +97,7 @@ class BitcoinWalletService extends WalletService< name: name, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -126,6 +132,7 @@ class BitcoinWalletService extends WalletService< name: currentName, walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -147,7 +154,6 @@ class BitcoinWalletService extends WalletService< credentials.walletInfo?.network = network.value; credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath; - final wallet = await BitcoinWallet( password: credentials.password!, xpub: credentials.hwAccountData.xpub, @@ -155,6 +161,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + payjoinBox: payjoinSessionSource, ); await wallet.save(); await wallet.init(); @@ -182,6 +189,7 @@ class BitcoinWalletService extends WalletService< mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, + payjoinBox: payjoinSessionSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 14fd1d3a9..35c15682c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1188,6 +1188,7 @@ abstract class ElectrumWalletBase isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, utxos: estimatedTx.utxos, + publicKeys: estimatedTx.publicKeys )..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { @@ -1965,6 +1966,11 @@ abstract class ElectrumWalletBase } } + bool isMine(Script script) { + final derivedAddress = addressFromOutputScript(script, network); + return addressesSet.contains(derivedAddress); + } + @override Future> fetchTransactions() async { try { diff --git a/cw_bitcoin/lib/payjoin/manager.dart b/cw_bitcoin/lib/payjoin/manager.dart new file mode 100644 index 000000000..b80fa777c --- /dev/null +++ b/cw_bitcoin/lib/payjoin/manager.dart @@ -0,0 +1,298 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:math'; +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_receive_worker.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_send_worker.dart'; +import 'package:cw_bitcoin/payjoin/payjoin_session_errors.dart'; +import 'package:cw_bitcoin/payjoin/storage.dart'; +import 'package:cw_bitcoin/psbt/signer.dart'; +import 'package:cw_bitcoin/psbt/utils.dart'; +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/uri.dart' as PayjoinUri; + +class PayjoinManager { + PayjoinManager(this._payjoinStorage, this._wallet); + + final PayjoinStorage _payjoinStorage; + final BitcoinWalletBase _wallet; + final Map _activePollers = {}; + + static const List ohttpRelayUrls = [ + 'https://pj.bobspacebkk.com', + 'https://ohttp.achow101.com', + ]; + + static Future randomOhttpRelayUrl() => PayjoinUri.Url.fromStr( + ohttpRelayUrls[Random.secure().nextInt(ohttpRelayUrls.length)]); + + static const payjoinDirectoryUrl = 'https://payjo.in'; + + Future resumeSessions() async { + final allSessions = _payjoinStorage.readAllOpenSessions(_wallet.id); + + final spawnedSessions = allSessions.map((session) { + if (session.isSenderSession) { + printV("Resuming Payjoin Sender Session ${session.pjUri!}"); + return _spawnSender( + sender: Sender.fromJson(session.sender!), + pjUri: session.pjUri!, + ); + } + final receiver = Receiver.fromJson(session.receiver!); + printV("Resuming Payjoin Receiver Session ${receiver.id()}"); + return _spawnReceiver(receiver: receiver); + }); + + printV("Resumed ${spawnedSessions.length} Payjoin Sessions"); + await Future.wait(spawnedSessions); + } + + Future initSender( + String pjUriString, String originalPsbt, int networkFeesSatPerVb) async { + try { + final pjUri = + (await PayjoinUri.Uri.fromStr(pjUriString)).checkPjSupported(); + final minFeeRateSatPerKwu = BigInt.from(networkFeesSatPerVb * 250); + final senderBuilder = await SenderBuilder.fromPsbtAndUri( + psbtBase64: originalPsbt, + pjUri: pjUri, + ); + return senderBuilder.buildRecommended(minFeeRate: minFeeRateSatPerKwu); + } catch (e) { + throw Exception('Error initializing Payjoin Sender: $e'); + } + } + + Future spawnNewSender({ + required Sender sender, + required String pjUrl, + required BigInt amount, + bool isTestnet = false, + }) async { + final pjUri = Uri.parse(pjUrl).queryParameters['pj']!; + await _payjoinStorage.insertSenderSession( + sender, pjUri, _wallet.id, amount); + + return _spawnSender(isTestnet: isTestnet, sender: sender, pjUri: pjUri); + } + + Future _spawnSender({ + required Sender sender, + required String pjUri, + bool isTestnet = false, + }) async { + final completer = Completer(); + final receivePort = ReceivePort(); + + receivePort.listen((message) async { + if (message is Map) { + try { + switch (message['type'] as PayjoinSenderRequestTypes) { + case PayjoinSenderRequestTypes.requestPosted: + return; + case PayjoinSenderRequestTypes.psbtToSign: + final proposalPsbt = message['psbt'] as String; + final utxos = _wallet.getUtxoWithPrivateKeys(); + final finalizedPsbt = await _wallet.signPsbt(proposalPsbt, utxos); + final txId = getTxIdFromPsbtV0(finalizedPsbt); + _wallet.commitPsbt(finalizedPsbt); + + _cleanupSession(pjUri); + await _payjoinStorage.markSenderSessionComplete(pjUri, txId); + completer.complete(); + } + } catch (e) { + _cleanupSession(pjUri); + printV(e); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri); + completer.completeError(e); + } + } else if (message is PayjoinSessionError) { + _cleanupSession(pjUri); + if (message is UnrecoverableError) { + printV(message.message); + await _payjoinStorage.markSenderSessionUnrecoverable(pjUri); + completer.complete(); + } else if (message is RecoverableError) { + completer.complete(); + } else { + completer.completeError(message); + } + } + }); + + final isolate = await Isolate.spawn( + PayjoinSenderWorker.run, + [receivePort.sendPort, sender.toJson(), pjUri], + ); + + _activePollers[pjUri] = PayjoinPollerSession(isolate, receivePort); + + return completer.future; + } + + 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, + ); + + final receiver = await Receiver.create( + address: address, + network: isTestnet ? Network.testnet : Network.bitcoin, + directory: payjoinDirectory, + ohttpKeys: ohttpKeys, + ohttpRelay: await randomOhttpRelayUrl(), + ); + + await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); + + return receiver; + } catch (e) { + throw Exception('Error initializing Payjoin Receiver: $e'); + } + } + + Future spawnNewReceiver({ + required Receiver receiver, + bool isTestnet = false, + }) async { + await _payjoinStorage.insertReceiverSession(receiver, _wallet.id); + return _spawnReceiver(isTestnet: isTestnet, receiver: receiver); + } + + Future _spawnReceiver({ + required Receiver receiver, + bool isTestnet = false, + }) async { + final completer = Completer(); + final receivePort = ReceivePort(); + + SendPort? mainToIsolateSendPort; + List utxos = []; + String rawAmount = '0'; + + receivePort.listen((message) async { + if (message is Map) { + try { + switch (message['type'] as PayjoinReceiverRequestTypes) { + case PayjoinReceiverRequestTypes.processOriginalTx: + final tx = message['tx'] as String; + rawAmount = getOutputAmountFromTx(tx, _wallet); + break; + case PayjoinReceiverRequestTypes.checkIsOwned: + (_wallet.walletAddresses as BitcoinWalletAddresses).newPayjoinReceiver(); + _payjoinStorage.markReceiverSessionInProgress(receiver.id()); + + final inputScript = message['input_script'] as Uint8List; + final isOwned = + _wallet.isMine(Script.fromRaw(byteData: inputScript)); + mainToIsolateSendPort?.send({ + 'requestId': message['requestId'], + 'result': isOwned, + }); + break; + + case PayjoinReceiverRequestTypes.checkIsReceiverOutput: + final outputScript = message['output_script'] as Uint8List; + final isReceiverOutput = + _wallet.isMine(Script.fromRaw(byteData: outputScript)); + mainToIsolateSendPort?.send({ + 'requestId': message['requestId'], + 'result': isReceiverOutput, + }); + break; + + case PayjoinReceiverRequestTypes.getCandidateInputs: + utxos = _wallet.getUtxoWithPrivateKeys(); + mainToIsolateSendPort?.send({ + 'requestId': message['requestId'], + 'result': utxos, + }); + break; + + case PayjoinReceiverRequestTypes.processPsbt: + final psbt = message['psbt'] as String; + final signedPsbt = await _wallet.signPsbt(psbt, utxos); + mainToIsolateSendPort?.send({ + 'requestId': message['requestId'], + 'result': signedPsbt, + }); + break; + + case PayjoinReceiverRequestTypes.proposalSent: + _cleanupSession(receiver.id()); + final psbt = message['psbt'] as String; + await _payjoinStorage.markReceiverSessionComplete( + receiver.id(), getTxIdFromPsbtV0(psbt), rawAmount); + completer.complete(); + } + } catch (e) { + _cleanupSession(receiver.id()); + await _payjoinStorage.markReceiverSessionUnrecoverable( + receiver.id(), e.toString()); + completer.completeError(e); + } + } else if (message is PayjoinSessionError) { + _cleanupSession(receiver.id()); + if (message is UnrecoverableError) { + await _payjoinStorage.markReceiverSessionUnrecoverable( + receiver.id(), message.message); + completer.complete(); + } else if (message is RecoverableError) { + completer.complete(); + } else { + completer.completeError(message); + } + } else if (message is SendPort) { + mainToIsolateSendPort = message; + } + }); + + final isolate = await Isolate.spawn( + PayjoinReceiverWorker.run, + [receivePort.sendPort, receiver.toJson()], + ); + + _activePollers[receiver.id()] = PayjoinPollerSession(isolate, receivePort); + + return completer.future; + } + + void cleanupSessions() { + final sessionIds = _activePollers.keys.toList(); + for (final sessionId in sessionIds) { + _cleanupSession(sessionId); + } + } + + void _cleanupSession(String sessionId) { + _activePollers[sessionId]?.close(); + _activePollers.remove(sessionId); + } +} + +class PayjoinPollerSession { + final Isolate isolate; + final ReceivePort port; + + PayjoinPollerSession(this.isolate, this.port); + + void close() { + isolate.kill(); + port.close(); + } +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart new file mode 100644 index 000000000..a499660b0 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_receive_worker.dart @@ -0,0 +1,219 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:typed_data'; + +import 'package:blockchain_utils/blockchain_utils.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: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; + +enum PayjoinReceiverRequestTypes { + processOriginalTx, + proposalSent, + getCandidateInputs, + checkIsOwned, + checkIsReceiverOutput, + processPsbt; +} + +class PayjoinReceiverWorker { + final SendPort sendPort; + final pendingRequests = >{}; + + PayjoinReceiverWorker._(this.sendPort); + + static Future run(List args) async { + await pj.core.init(); + + final sendPort = args[0] as SendPort; + final receiverJson = args[1] as String; + + final worker = PayjoinReceiverWorker._(sendPort); + final receivePort = ReceivePort(); + + sendPort.send(receivePort.sendPort); + receivePort.listen(worker.handleMessage); + + try { + final httpClient = http.Client(); + final receiver = Receiver.fromJson(receiverJson); + + final uncheckedProposal = + await worker.receiveUncheckedProposal(httpClient, receiver); + + final originalTx = await uncheckedProposal.extractTxToScheduleBroadcast(); + sendPort.send({ + 'type': PayjoinReceiverRequestTypes.processOriginalTx, + 'tx': BytesUtils.toHexString(originalTx), + }); + + final payjoinProposal = await worker.processPayjoinProposal( + uncheckedProposal, + ); + final psbt = await worker.sendFinalProposal(httpClient, payjoinProposal); + sendPort.send({ + 'type': PayjoinReceiverRequestTypes.proposalSent, + 'psbt': psbt, + }); + } catch (e) { + if (e is HttpException || + (e is http.ClientException && + e.message.contains("Software caused connection abort"))) { + sendPort.send(PayjoinSessionError.recoverable(e.toString())); + } else { + sendPort.send(PayjoinSessionError.unrecoverable(e.toString())); + } + } + } + + void handleMessage(dynamic message) async { + if (message is Map) { + final requestId = message['requestId'] as String?; + if (requestId != null && pendingRequests.containsKey(requestId)) { + pendingRequests[requestId]!.complete(message['result']); + pendingRequests.remove(requestId); + } + } + } + + Future _sendRequest(PayjoinReceiverRequestTypes type, + [Map data = const {}]) async { + final completer = Completer(); + final requestId = DateTime.now().millisecondsSinceEpoch.toString(); + pendingRequests[requestId] = completer; + + sendPort.send({ + ...data, + 'type': type, + 'requestId': requestId, + }); + + return completer.future; + } + + Future receiveUncheckedProposal( + http.Client httpClient, Receiver session) async { + while (true) { + printV("Polling for Proposal (${session.id()})"); + final extractReq = await session.extractReq(); + final request = extractReq.$1; + + final url = Uri.parse(request.url.asString()); + final httpRequest = await httpClient.post(url, + headers: {'Content-Type': request.contentType}, body: request.body); + + final proposal = await session.processRes( + body: httpRequest.bodyBytes, ctx: extractReq.$2); + if (proposal != null) return proposal; + } + } + + Future sendFinalProposal( + http.Client httpClient, PayjoinProposal finalProposal) async { + final req = await finalProposal.extractV2Req(); + final proposalReq = req.$1; + final proposalCtx = req.$2; + + final request = await httpClient.post( + Uri.parse(proposalReq.url.asString()), + headers: {"Content-Type": proposalReq.contentType}, + body: proposalReq.body, + ); + + await finalProposal.processRes( + res: request.bodyBytes, + ohttpContext: proposalCtx, + ); + + return await finalProposal.psbt(); + } + + Future processPayjoinProposal( + UncheckedProposal proposal) async { + await proposal.extractTxToScheduleBroadcast(); + // TODO Handle this. send to the main port on a timer? + + try { + // Receive Check 1: can broadcast + final pj1 = await proposal.assumeInteractiveReceiver(); + + // Receive Check 2: original PSBT has no receiver-owned inputs + final pj2 = await pj1.checkInputsNotOwned( + isOwned: (inputScript) async { + final result = await _sendRequest( + PayjoinReceiverRequestTypes.checkIsOwned, + {'input_script': inputScript}, + ); + return result as bool; + }, + ); + // Receive Check 3: sender inputs have not been seen before (prevent probing attacks) + final pj3 = await pj2.checkNoInputsSeenBefore(isKnown: (input) => false); + + // Identify receiver outputs + final pj4 = await pj3.identifyReceiverOutputs( + isReceiverOutput: (outputScript) async { + final result = await _sendRequest( + PayjoinReceiverRequestTypes.checkIsReceiverOutput, + {'output_script': outputScript}, + ); + return result as bool; + }, + ); + final pj5 = await pj4.commitOutputs(); + + final listUnspent = + await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs); + final unspent = listUnspent as List; + if (unspent.isEmpty) throw Exception('No unspent outputs available'); + + final selectedUtxo = await _inputPairFromUtxo(unspent[0]); + final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]); + final pj7 = await pj6.commitInputs(); + + // Finalize proposal + final payjoinProposal = await pj7.finalizeProposal( + processPsbt: (String psbt) async { + final result = await _sendRequest( + PayjoinReceiverRequestTypes.processPsbt, {'psbt': psbt}); + return result as String; + }, + // TODO set maxFeeRateSatPerVb + maxFeeRateSatPerVb: BigInt.from(10000), + ); + return payjoinProposal; + } catch (e) { + printV('Error occurred while finalizing proposal: $e'); + rethrow; + } + } + + Future _inputPairFromUtxo(UtxoWithPrivateKey utxo) async { + final txout = TxOut( + value: utxo.utxo.value, + scriptPubkey: Uint8List.fromList( + utxo.ownerDetails.address.toScriptPubKey().toBytes()), + ); + + final psbtin = + PsbtInput(witnessUtxo: txout, redeemScript: null, witnessScript: null); + + final previousOutput = + OutPoint(txid: utxo.utxo.txHash, vout: utxo.utxo.vout); + + final txin = TxIn( + previousOutput: previousOutput, + scriptSig: await Script.newInstance(rawOutputScript: []), + witness: [], + sequence: 0, + ); + + return InputPair.newInstance(txin, psbtin); + } +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart new file mode 100644 index 000000000..f720bac01 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_send_worker.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +import 'dart:io'; +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:payjoin_flutter/common.dart'; +import 'package:payjoin_flutter/send.dart'; +import 'package:payjoin_flutter/src/generated/frb_generated.dart' as pj; + +enum PayjoinSenderRequestTypes { + requestPosted, + psbtToSign; +} + +class PayjoinSenderWorker { + final SendPort sendPort; + final pendingRequests = >{}; + final String pjUrl; + + PayjoinSenderWorker._(this.sendPort, this.pjUrl); + + static Future run(List args) async { + await pj.core.init(); + + final sendPort = args[0] as SendPort; + final senderJson = args[1] as String; + final pjUrl = args[2] as String; + + final sender = Sender.fromJson(senderJson); + final worker = PayjoinSenderWorker._(sendPort, pjUrl); + + try { + final proposalPsbt = await worker.runSender(sender); + sendPort.send({ + 'type': PayjoinSenderRequestTypes.psbtToSign, + 'psbt': proposalPsbt, + }); + } catch (e) { + sendPort.send(e); + } + } + + /// 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); + } 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) { + return await _runSenderV1(sender, httpClient); + } else if (e is HttpException) { + printV(e); + throw Exception(PayjoinSessionError.recoverable(e.toString())); + } else { + throw Exception(PayjoinSessionError.unrecoverable(e.toString())); + } + } + } + + /// Attempt to send payjoin using the V2 of the protocol. + Future _runSenderV2(Sender sender, http.Client httpClient) async { + try { + final postRequest = await sender.extractV2( + ohttpProxyUrl: await PayjoinManager.randomOhttpRelayUrl(), + ); + + final postResult = await _postRequest(httpClient, postRequest.$1); + final getContext = + await postRequest.$2.processResponse(response: postResult); + + sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted, "pj": pjUrl}); + + while (true) { + printV('Polling V2 Proposal Request (${pjUrl})'); + + final getRequest = await getContext.extractReq( + ohttpRelay: await PayjoinManager.randomOhttpRelayUrl(), + ); + final getRes = await _postRequest(httpClient, getRequest.$1); + final proposalPsbt = await getContext.processResponse( + response: getRes, + ohttpCtx: getRequest.$2, + ); + printV("$proposalPsbt"); + if (proposalPsbt != null) return proposalPsbt; + } + } catch (e) { + rethrow; + } + } + + /// Attempt to send payjoin using the V1 of the protocol. + Future _runSenderV1(Sender sender, http.Client httpClient) async { + try { + final postRequest = await sender.extractV1(); + final response = await _postRequest(httpClient, postRequest.$1); + + sendPort.send({'type': PayjoinSenderRequestTypes.requestPosted}); + + return await postRequest.$2.processResponse(response: response); + } catch (e) { + throw PayjoinSessionError.unrecoverable('Send V1 payjoin error: $e'); + } + } + + Future> _postRequest(http.Client client, Request req) async { + final httpRequest = await client.post(Uri.parse(req.url.asString()), + headers: {'Content-Type': req.contentType}, body: req.body); + + return httpRequest.bodyBytes; + } +} diff --git a/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart b/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart new file mode 100644 index 000000000..06e0a5431 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/payjoin_session_errors.dart @@ -0,0 +1,16 @@ +class PayjoinSessionError { + final String message; + + const PayjoinSessionError._(this.message); + + factory PayjoinSessionError.recoverable(String message) = RecoverableError; + factory PayjoinSessionError.unrecoverable(String message) = UnrecoverableError; +} + +class RecoverableError extends PayjoinSessionError { + const RecoverableError(super.message) : super._(); +} + +class UnrecoverableError extends PayjoinSessionError { + const UnrecoverableError(super.message) : super._(); +} diff --git a/cw_bitcoin/lib/payjoin/storage.dart b/cw_bitcoin/lib/payjoin/storage.dart new file mode 100644 index 000000000..9c1c83253 --- /dev/null +++ b/cw_bitcoin/lib/payjoin/storage.dart @@ -0,0 +1,95 @@ +import 'package:cw_core/payjoin_session.dart'; +import 'package:hive/hive.dart'; +import 'package:payjoin_flutter/receive.dart'; +import 'package:payjoin_flutter/send.dart'; + +class PayjoinStorage { + PayjoinStorage(this._payjoinSessionSources); + + final Box _payjoinSessionSources; + + static const String _receiverPrefix = 'pj_recv_'; + static const String _senderPrefix = 'pj_send_'; + + Future insertReceiverSession( + Receiver receiver, + String walletId, + ) => + _payjoinSessionSources.put( + "$_receiverPrefix${receiver.id()}", + PayjoinSession( + walletId: walletId, + receiver: receiver.toJson(), + ), + ); + + Future markReceiverSessionComplete( + String sessionId, String txId, String amount) async { + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; + + session.status = PayjoinSessionStatus.success.name; + session.txId = txId; + session.rawAmount = amount; + await session.save(); + } + + Future markReceiverSessionUnrecoverable( + String sessionId, String reason) async { + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; + + session.status = PayjoinSessionStatus.unrecoverable.name; + session.error = reason; + await session.save(); + } + + Future markReceiverSessionInProgress(String sessionId) async { + final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!; + + session.status = PayjoinSessionStatus.inProgress.name; + session.inProgressSince = DateTime.now(); + await session.save(); + } + + Future insertSenderSession( + Sender sender, + String pjUrl, + String walletId, + BigInt amount, + ) => + _payjoinSessionSources.put( + "$_senderPrefix$pjUrl", + PayjoinSession( + walletId: walletId, + pjUri: pjUrl, + sender: sender.toJson(), + status: PayjoinSessionStatus.inProgress.name, + inProgressSince: DateTime.now(), + rawAmount: amount.toString(), + ), + ); + + Future markSenderSessionComplete(String pjUrl, String txId) async { + final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; + + session.status = PayjoinSessionStatus.success.name; + session.txId = txId; + await session.save(); + } + + Future markSenderSessionUnrecoverable(String pjUrl) async { + final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!; + + session.status = PayjoinSessionStatus.unrecoverable.name; + await session.save(); + } + + List readAllOpenSessions(String walletId) => + _payjoinSessionSources.values + .where((session) => + session.walletId == walletId && + ![ + PayjoinSessionStatus.success.name, + PayjoinSessionStatus.unrecoverable.name + ].contains(session.status)) + .toList(); +} diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 411c7de16..6930524eb 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,3 +1,4 @@ +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; @@ -25,6 +26,8 @@ class PendingBitcoinTransaction with PendingTransaction { this.hasTaprootInputs = false, this.isMweb = false, this.utxos = const [], + this.publicKeys, + this.commitOverride, }) : _listeners = []; final WalletType type; @@ -43,6 +46,8 @@ class PendingBitcoinTransaction with PendingTransaction { String? idOverride; String? hexOverride; List? outputAddresses; + final Map? publicKeys; + Future Function()? commitOverride; @override String get id => idOverride ?? _tx.txId(); @@ -129,6 +134,10 @@ class PendingBitcoinTransaction with PendingTransaction { @override Future commit() async { + if (commitOverride != null) { + return commitOverride?.call(); + } + if (isMweb) { await _ltcCommit(); } else { diff --git a/cw_bitcoin/lib/psbt/signer.dart b/cw_bitcoin/lib/psbt/signer.dart new file mode 100644 index 000000000..1d0ceba8b --- /dev/null +++ b/cw_bitcoin/lib/psbt/signer.dart @@ -0,0 +1,263 @@ +import 'dart:typed_data'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/utils.dart'; +import 'package:ledger_bitcoin/psbt.dart'; +import 'package:ledger_bitcoin/src/utils/buffer_writer.dart'; + +extension PsbtSigner on PsbtV2 { + Uint8List extractUnsignedTX({bool getSegwit = true}) { + final tx = BufferWriter()..writeUInt32(getGlobalTxVersion()); + + final isSegwit = getInputWitnessUtxo(0) != null; + if (isSegwit && getSegwit) { + tx.writeSlice(Uint8List.fromList([0, 1])); + } + + final inputCount = getGlobalInputCount(); + tx.writeVarInt(inputCount); + + for (var i = 0; i < inputCount; i++) { + tx + ..writeSlice(getInputPreviousTxid(i)) + ..writeUInt32(getInputOutputIndex(i)) + ..writeVarSlice(Uint8List(0)) + ..writeUInt32(getInputSequence(i)); + } + + final outputCount = getGlobalOutputCount(); + tx.writeVarInt(outputCount); + for (var i = 0; i < outputCount; i++) { + tx.writeUInt64(getOutputAmount(i)); + tx.writeVarSlice(getOutputScript(i)); + } + tx.writeUInt32(getGlobalFallbackLocktime() ?? 0); + return tx.buffer(); + } + + Future signWithUTXO( + List utxos, UTXOSignerCallBack signer, + [UTXOGetterCallBack? getTaprootPair]) async { + final raw = BytesUtils.toHexString(extractUnsignedTX(getSegwit: false)); + final tx = BtcTransaction.fromRaw(raw); + + /// when the transaction is taproot and we must use getTaproot transaction + /// digest we need all of inputs amounts and owner script pub keys + List taprootAmounts = []; + List